diff --git a/Cargo.lock b/Cargo.lock
index dc7a57f..fcc809a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -256,9 +256,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
+checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
@@ -353,6 +353,17 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+[[package]]
+name = "async-trait"
+version = "0.1.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -413,6 +424,15 @@ dependencies = [
"wyz",
]
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -422,6 +442,17 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "blowfish"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b"
+dependencies = [
+ "byteorder",
+ "cipher",
+ "opaque-debug",
+]
+
[[package]]
name = "bmcd"
version = "1.3.0"
@@ -429,6 +460,8 @@ dependencies = [
"actix-files",
"actix-web",
"anyhow",
+ "async-trait",
+ "base64",
"build-time",
"clap",
"futures",
@@ -436,6 +469,7 @@ dependencies = [
"log",
"nix 0.26.4",
"openssl",
+ "pwhash",
"rand 0.8.5",
"serde",
"serde_json",
@@ -486,6 +520,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
[[package]]
name = "bytes"
version = "1.5.0"
@@ -529,20 +569,29 @@ dependencies = [
"windows-targets 0.48.5",
]
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "clap"
-version = "4.4.4"
+version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
+checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.4.4"
+version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
+checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
dependencies = [
"anstream",
"anstyle",
@@ -639,6 +688,16 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "crypto-mac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
[[package]]
name = "deranged"
version = "0.3.8"
@@ -658,13 +717,22 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "block-buffer",
+ "block-buffer 0.10.4",
"crypto-common",
]
@@ -951,6 +1019,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+]
+
[[package]]
name = "http"
version = "0.2.9"
@@ -1162,6 +1240,17 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+[[package]]
+name = "md-5"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
+dependencies = [
+ "block-buffer 0.9.0",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
[[package]]
name = "memchr"
version = "2.6.3"
@@ -1313,6 +1402,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
[[package]]
name = "openssl"
version = "0.10.57"
@@ -1429,6 +1524,21 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pwhash"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56"
+dependencies = [
+ "blowfish",
+ "byteorder",
+ "hmac",
+ "md-5",
+ "rand 0.8.5",
+ "sha-1",
+ "sha2",
+]
+
[[package]]
name = "quote"
version = "1.0.33"
@@ -1575,9 +1685,9 @@ dependencies = [
[[package]]
name = "rockfile"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e492654d9f624d21df4f0eb765c9174a4a577c4d47680803ffe09b6ae55b3d02"
+checksum = "7e89194db9752ec1f052472debd3990f77bec4ca0732d9dd28163f95fed1455a"
dependencies = [
"bytes",
]
@@ -1622,9 +1732,9 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.13"
+version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
+checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno",
@@ -1647,9 +1757,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.5"
+version = "0.101.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
+checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
dependencies = [
"ring",
"untrusted",
@@ -1688,9 +1798,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.18"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
[[package]]
name = "serde"
@@ -1748,15 +1858,41 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "sha-1"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
[[package]]
name = "sha1"
-version = "0.10.5"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
]
[[package]]
@@ -1791,9 +1927,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "socket2"
@@ -1817,6 +1953,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -1877,9 +2019,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
+checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
dependencies = [
"deranged",
"itoa",
@@ -1892,15 +2034,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.1"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
-version = "0.2.14"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
+checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
@@ -1964,9 +2106,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.8"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
diff --git a/bmcd/Cargo.toml b/bmcd/Cargo.toml
index e368561..607a746 100644
--- a/bmcd/Cargo.toml
+++ b/bmcd/Cargo.toml
@@ -15,6 +15,8 @@ serde_yaml = "0.9.25"
tpi_rs = { path = "../tpi_rs" }
clap = { version = "4.4.2", features = ["cargo"] }
openssl = "0.10.57"
+rand = "0.8.5"
+pwhash = "1.0.0"
anyhow.workspace = true
log.workspace = true
@@ -23,4 +25,5 @@ tokio.workspace = true
tokio-util.workspace = true
futures.workspace = true
serde.workspace = true
-rand = "0.8.5"
+base64 = "0.21.4"
+async-trait = "0.1.73"
diff --git a/bmcd/src/authentication/authentication_context.rs b/bmcd/src/authentication/authentication_context.rs
new file mode 100644
index 0000000..9c7e71c
--- /dev/null
+++ b/bmcd/src/authentication/authentication_context.rs
@@ -0,0 +1,277 @@
+use super::authentication_errors::AuthenticationError;
+use super::passwd_validator::PasswordValidator;
+use super::passwd_validator::UnixValidator;
+use base64::{engine::general_purpose, Engine as _};
+use rand::distributions::Alphanumeric;
+use rand::thread_rng;
+use rand::Rng;
+use serde::Deserialize;
+use serde::Serialize;
+use std::collections::HashMap;
+use std::marker::PhantomData;
+use tokio::sync::Mutex;
+use tokio::time::{Duration, Instant};
+
+pub struct AuthenticationContext
+where
+ P: PasswordValidator + 'static,
+{
+ token_store: Mutex>,
+ passwds: HashMap,
+ password_validator: PhantomData,
+ expire_timeout: Duration,
+}
+
+impl
AuthenticationContext
+where
+ P: PasswordValidator + 'static,
+{
+ pub fn with_unix_validator(
+ password_entries: impl Iterator- ,
+ expire_timeout: Duration,
+ ) -> AuthenticationContext {
+ AuthenticationContext:: {
+ token_store: Mutex::new(HashMap::new()),
+ passwds: HashMap::from_iter(password_entries),
+ password_validator: PhantomData::,
+ expire_timeout,
+ }
+ }
+
+ /// This function piggy-backs removes of expired tokens on an authentication
+ /// request. This imposes a small penalty on each request. Its deemed not
+ /// significant enough to justify optimalization given the expected volume
+ /// of incoming requests.
+ async fn new_and_remove_expired_tokens(&self, key: String) {
+ let mut store = self.token_store.lock().await;
+
+ store.retain(|_, last_access| {
+ let duration = Instant::now().saturating_duration_since(*last_access);
+ duration <= self.expire_timeout
+ });
+
+ store.insert(key, Instant::now());
+ }
+
+ async fn authorize_bearer(&self, token: &str) -> Result<(), AuthenticationError> {
+ let mut store = self.token_store.lock().await;
+ let Some(last_access) = store.get_mut(token) else {
+ return Err(AuthenticationError::NoMatch(token.to_string()));
+ };
+
+ let instant = *last_access;
+ let duration = Instant::now().saturating_duration_since(instant);
+ if duration < self.expire_timeout {
+ *last_access = Instant::now();
+ return Ok(());
+ }
+
+ store.remove(token);
+ Err(AuthenticationError::TokenExpired(instant))
+ }
+
+ fn validate_credentials(
+ &self,
+ username: &str,
+ password: &str,
+ ) -> Result<(), AuthenticationError> {
+ let Some(pass) = self.passwds.get(username) else {
+ log::debug!("user {} not in database", username);
+ return Err(AuthenticationError::IncorrectCredentials);
+ };
+
+ P::validate(pass, password)
+ }
+
+ async fn authorize_basic(&self, credentials: &str) -> Result<(), AuthenticationError> {
+ let decoded = general_purpose::STANDARD.decode(credentials)?;
+ let utf8 = std::str::from_utf8(&decoded)?;
+ let Some((user, pass)) = utf8.split_once(':') else {
+ return Err(AuthenticationError::IncorrectCredentials);
+ };
+
+ self.validate_credentials(user, pass)
+ }
+
+ pub async fn authorize_request(
+ &self,
+ http_authorization_line: &str,
+ ) -> Result<(), AuthenticationError> {
+ match http_authorization_line.split_once(' ') {
+ Some(("Bearer", token)) => self.authorize_bearer(token).await,
+ Some(("Basic", credentials)) => self.authorize_basic(credentials).await,
+ Some((auth, _)) => Err(AuthenticationError::SchemeNotSupported(auth.to_string())),
+ None => Err(AuthenticationError::HttpParseError(
+ http_authorization_line.to_string(),
+ )),
+ }
+ }
+
+ pub async fn authenticate_request(&self, body: &[u8]) -> Result {
+ let credentials = serde_json::from_slice::(body)?;
+
+ self.validate_credentials(&credentials.username, &credentials.password)?;
+
+ let token: String = thread_rng()
+ .sample_iter(&Alphanumeric)
+ .take(64)
+ .map(char::from)
+ .collect();
+ self.new_and_remove_expired_tokens(token.clone()).await;
+
+ Ok(Session {
+ id: token, // according Redfish spec, id refers to the session id.
+ // which is not equal to the access-token. for now use
+ // the token.
+ name: "User Session".to_string(),
+ description: "User Session".to_string(),
+ username: credentials.username,
+ })
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Session {
+ pub id: String,
+ name: String,
+ description: String,
+ username: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct Login {
+ username: String,
+ password: String,
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use std::ops::Sub;
+
+ pub struct DummyValidator {}
+ impl PasswordValidator for DummyValidator {
+ fn validate(
+ _: &str,
+ _: &str,
+ ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> {
+ Ok(())
+ }
+ }
+
+ pub struct FalseValidator {}
+ impl PasswordValidator for FalseValidator {
+ fn validate(
+ _: &str,
+ _: &str,
+ ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> {
+ Err(AuthenticationError::IncorrectCredentials)
+ }
+ }
+
+ pub fn build_test_context(
+ token_data: impl IntoIterator
- ,
+ user_data: impl IntoIterator
- ,
+ ) -> AuthenticationContext {
+ AuthenticationContext {
+ token_store: Mutex::new(HashMap::from_iter(token_data)),
+ passwds: HashMap::from_iter(user_data),
+ password_validator: PhantomData::,
+ expire_timeout: Duration::from_secs(20),
+ }
+ }
+
+ #[actix_web::test]
+ async fn test_token_failures() {
+ let now = Instant::now();
+ let twenty_sec_ago = now.sub(Duration::from_secs(20));
+ let context = build_test_context(
+ [("123".to_string(), now), ("2".to_string(), twenty_sec_ago)],
+ Vec::new(),
+ );
+
+ assert_eq!(
+ context.authorize_request("Bearer 1234").await.unwrap_err(),
+ AuthenticationError::NoMatch("1234".to_string())
+ );
+
+ assert_eq!(
+ context.authorize_request("Bearer 2").await.unwrap_err(),
+ AuthenticationError::TokenExpired(twenty_sec_ago)
+ );
+
+ // After expired error, the token gets removed. Subsequent calls for that token will
+ // therefore return "NoMatch"
+ assert_eq!(
+ context.authorize_request("Bearer 2").await.unwrap_err(),
+ AuthenticationError::NoMatch("2".to_string())
+ );
+ }
+
+ #[actix_web::test]
+ async fn test_happy_flow() {
+ let context = build_test_context(
+ [
+ ("123".to_string(), Instant::now()),
+ ("2".to_string(), Instant::now().sub(Duration::from_secs(20))),
+ ],
+ Vec::new(),
+ );
+ assert_eq!(Ok(()), context.authorize_request("Bearer 123").await);
+ }
+
+ #[actix_web::test]
+ async fn authentication_errors() {
+ let context = build_test_context(
+ Vec::new(),
+ [("test_user".to_string(), "password".to_string())],
+ );
+
+ assert!(matches!(
+ context
+ .authenticate_request(b"{not a valid json")
+ .await
+ .unwrap_err(),
+ AuthenticationError::ParseError(_)
+ ));
+
+ let json = serde_json::to_vec(&Login {
+ username: "John".to_string(),
+ password: "1234".to_string(),
+ })
+ .unwrap();
+
+ assert_eq!(
+ context.authenticate_request(&json).await.unwrap_err(),
+ AuthenticationError::IncorrectCredentials
+ );
+ let json = serde_json::to_vec(&Login {
+ username: "test_user".to_string(),
+ password: "1234".to_string(),
+ })
+ .unwrap();
+
+ assert_eq!(
+ context.authenticate_request(&json).await.unwrap_err(),
+ AuthenticationError::IncorrectCredentials
+ );
+ }
+
+ #[actix_web::test]
+ async fn pass_authentication() {
+ let context = build_test_context(
+ Vec::new(),
+ [("test_user".to_string(), "password".to_string())],
+ );
+ let json = serde_json::to_vec(&Login {
+ username: "test_user".to_string(),
+ password: "password".to_string(),
+ })
+ .unwrap();
+
+ assert_eq!(
+ context.authenticate_request(&json).await.unwrap_err(),
+ AuthenticationError::IncorrectCredentials
+ );
+ }
+}
diff --git a/bmcd/src/authentication/authentication_errors.rs b/bmcd/src/authentication/authentication_errors.rs
new file mode 100644
index 0000000..7fab00b
--- /dev/null
+++ b/bmcd/src/authentication/authentication_errors.rs
@@ -0,0 +1,57 @@
+use std::{fmt::Display, str::Utf8Error};
+use tokio::time::Instant;
+
+#[derive(Debug, PartialEq)]
+pub enum AuthenticationError {
+ ParseError(String),
+ IncorrectCredentials,
+ TokenExpired(Instant),
+ NoMatch(String),
+ HttpParseError(String),
+ SchemeNotSupported(String),
+ Empty,
+}
+
+impl Display for AuthenticationError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ AuthenticationError::ParseError(e) => {
+ write!(f, "error trying to parse credentials: {}", e)
+ }
+ AuthenticationError::IncorrectCredentials => write!(f, "credentials incorrect"),
+ AuthenticationError::TokenExpired(instant) => write!(
+ f,
+ "token expired {}s ago",
+ Instant::now().duration_since(*instant).as_secs()
+ ),
+ AuthenticationError::NoMatch(token) => write!(f, "token {} is not registered", token),
+ AuthenticationError::Empty => write!(f, "no authorization header provided"),
+ AuthenticationError::HttpParseError(token) => {
+ write!(f, "cannot parse authorization header: {}", token)
+ }
+ AuthenticationError::SchemeNotSupported(scheme) => {
+ write!(f, "{} authentication not supported", scheme)
+ }
+ }
+ }
+}
+
+impl std::error::Error for AuthenticationError {}
+
+impl From for AuthenticationError {
+ fn from(value: serde_json::Error) -> Self {
+ Self::ParseError(value.to_string())
+ }
+}
+
+impl From for AuthenticationError {
+ fn from(value: base64::DecodeError) -> Self {
+ Self::ParseError(value.to_string())
+ }
+}
+
+impl From for AuthenticationError {
+ fn from(value: Utf8Error) -> Self {
+ Self::ParseError(value.to_string())
+ }
+}
diff --git a/bmcd/src/authentication/authentication_service.rs b/bmcd/src/authentication/authentication_service.rs
new file mode 100644
index 0000000..a480b8d
--- /dev/null
+++ b/bmcd/src/authentication/authentication_service.rs
@@ -0,0 +1,144 @@
+use super::{
+ authentication_context::AuthenticationContext, authentication_errors::AuthenticationError,
+ passwd_validator::UnixValidator,
+};
+use actix_web::{
+ body::{EitherBody, MessageBody},
+ dev::{Service, ServiceRequest, ServiceResponse},
+ http::header::{self},
+ Error, HttpRequest, HttpResponse,
+};
+use futures::future::LocalBoxFuture;
+use futures::StreamExt;
+use serde::Serialize;
+use std::{rc::Rc, sync::Arc};
+
+/// This authentication service is designed to prepare for implementing "Redfish
+/// Session Login Authentication" as good as possible. Redfish is not yet
+/// implemented in this product, until then this session based, token
+/// authentication service is used to provide authentication.
+#[derive(Clone)]
+pub struct AuthenticationService
{
+ service: Rc,
+ context: Arc>,
+ authentication_path: &'static str,
+}
+
+impl AuthenticationService {
+ pub fn new(
+ service: Rc,
+ context: Arc>,
+ authentication_path: &'static str,
+ ) -> Self {
+ AuthenticationService {
+ service,
+ context,
+ authentication_path,
+ }
+ }
+}
+
+impl Service for AuthenticationService
+where
+ S: Service, Error = Error> + 'static,
+ S::Future: 'static,
+ B: MessageBody + 'static,
+{
+ type Response = ServiceResponse>;
+ type Error = Error;
+ type Future = LocalBoxFuture<'static, Result>;
+
+ actix_web::dev::forward_ready!(service);
+
+ fn call(&self, mut request: ServiceRequest) -> Self::Future {
+ let service = self.service.clone();
+ let context = self.context.clone();
+ let auth_path = self.authentication_path;
+
+ Box::pin(async move {
+ let request_clone = request.request().clone();
+
+ if request.request().uri().path() == auth_path {
+ log::debug!("authentication request");
+ let mut buffer = Vec::new();
+ while let Some(Ok(bytes)) = request.parts_mut().1.next().await {
+ buffer.extend_from_slice(&bytes);
+ }
+
+ let response = match context.authenticate_request(&buffer).await {
+ Ok(session) => {
+ authenticated_response(request_clone, session.id.clone(), session)
+ }
+ Err(error) => forbidden_response(request_clone, error),
+ };
+
+ return response;
+ }
+
+ log::debug!("authorize request");
+ let parse_result = request
+ .headers()
+ .get(header::AUTHORIZATION)
+ .ok_or(AuthenticationError::Empty)
+ .and_then(|auth| {
+ auth.to_str()
+ .map_err(|e| AuthenticationError::HttpParseError(e.to_string()))
+ });
+
+ let auth = match parse_result {
+ Ok(p) => p,
+ Err(e) => return unauthorized_response(request_clone, e),
+ };
+
+ if let Err(e) = context.authorize_request(auth).await {
+ unauthorized_response(request_clone, e)
+ } else {
+ service
+ .call(request)
+ .await
+ .map(ServiceResponse::map_into_left_body)
+ }
+ })
+ }
+}
+
+fn forbidden_response(
+ request: HttpRequest,
+ response_text: E,
+) -> Result>, Error> {
+ Ok(ServiceResponse::new(
+ request,
+ HttpResponse::Forbidden()
+ .body(response_text.to_string())
+ .map_into_right_body(),
+ ))
+}
+
+fn authenticated_response(
+ request: HttpRequest,
+ token: String,
+ body: impl Serialize,
+) -> Result>, Error> {
+ let text = serde_json::to_string(&body)?;
+ Ok(ServiceResponse::new(
+ request,
+ HttpResponse::Ok()
+ .insert_header(("X-Auth-Token", token))
+ .body(text)
+ .map_into_right_body(),
+ ))
+}
+
+fn unauthorized_response(
+ request: HttpRequest,
+ response_text: E,
+) -> Result>, Error> {
+ let bearer_str = format!(
+ "Bearer error=invalid_token, error_description={}",
+ response_text.to_string()
+ );
+ let response = HttpResponse::Unauthorized()
+ .insert_header((header::WWW_AUTHENTICATE, bearer_str))
+ .body(response_text.to_string());
+ Ok(ServiceResponse::new(request, response)).map(ServiceResponse::map_into_right_body)
+}
diff --git a/bmcd/src/authentication/linux_authenticator.rs b/bmcd/src/authentication/linux_authenticator.rs
new file mode 100644
index 0000000..0e6b863
--- /dev/null
+++ b/bmcd/src/authentication/linux_authenticator.rs
@@ -0,0 +1,90 @@
+use super::{
+ authentication_context::AuthenticationContext, authentication_service::AuthenticationService,
+ passwd_validator::UnixValidator,
+};
+use actix_web::{
+ body::{EitherBody, MessageBody},
+ dev::{Service, ServiceRequest, ServiceResponse, Transform},
+ Error,
+};
+use std::{
+ future::{ready, Ready},
+ io,
+ time::Duration,
+};
+use std::{rc::Rc, sync::Arc};
+use tokio::{
+ fs::OpenOptions,
+ io::{AsyncBufReadExt, BufReader},
+};
+
+type LinuxContext = AuthenticationContext;
+
+pub struct LinuxAuthenticator {
+ context: Arc,
+ authentication_path: &'static str,
+}
+
+impl LinuxAuthenticator {
+ pub async fn new(authentication_path: &'static str) -> io::Result {
+ let password_entries = Self::parse_shadow_file().await?;
+ Ok(Self {
+ context: Arc::new(LinuxContext::with_unix_validator(
+ password_entries,
+ Duration::from_secs(24 * 60 * 60),
+ )),
+ authentication_path,
+ })
+ }
+}
+
+impl LinuxAuthenticator {
+ async fn parse_shadow_file() -> io::Result> {
+ let file = OpenOptions::new().read(true).open("/etc/shadow").await?;
+
+ let mut password_hashes: Vec<(String, String)> = Vec::new();
+ let mut read_buffer = BufReader::new(file);
+
+ loop {
+ let mut line = String::new();
+ let bytes_read = read_buffer.read_line(&mut line).await?;
+ if bytes_read == 0 {
+ break;
+ }
+
+ let mut items = line.splitn(3, ':');
+ let username = items.next();
+ let password = items.next();
+ let (Some(user), Some(pass)) = (username, password) else {
+ break;
+ };
+
+ if !pass.starts_with('*') {
+ password_hashes.push((user.to_string(), pass.to_string()));
+ log::debug!("loaded user {user}");
+ }
+ }
+ Ok(password_hashes.into_iter())
+ }
+}
+
+impl Transform for LinuxAuthenticator
+where
+ S: Service, Error = Error> + 'static,
+ S::Future: 'static,
+ B: MessageBody + 'static,
+{
+ type Response = ServiceResponse>;
+ type Error = Error;
+ type InitError = ();
+ type Transform = AuthenticationService;
+ type Future = Ready>;
+
+ fn new_transform(&self, service: S) -> Self::Future {
+ ready(Ok(AuthenticationService::new(
+ Rc::new(service),
+ self.context.clone(),
+ self.authentication_path,
+ )))
+ }
+}
diff --git a/bmcd/src/authentication/mod.rs b/bmcd/src/authentication/mod.rs
new file mode 100644
index 0000000..1d78965
--- /dev/null
+++ b/bmcd/src/authentication/mod.rs
@@ -0,0 +1,5 @@
+pub mod authentication_context;
+pub mod authentication_errors;
+pub mod authentication_service;
+pub mod linux_authenticator;
+pub mod passwd_validator;
diff --git a/bmcd/src/authentication/passwd_validator.rs b/bmcd/src/authentication/passwd_validator.rs
new file mode 100644
index 0000000..70887f2
--- /dev/null
+++ b/bmcd/src/authentication/passwd_validator.rs
@@ -0,0 +1,21 @@
+use super::authentication_errors::AuthenticationError;
+
+pub trait PasswordValidator {
+ fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError>;
+}
+
+pub struct UnixValidator {}
+
+impl PasswordValidator for UnixValidator {
+ fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError> {
+ log::debug!(
+ "computed={}",
+ pwhash::unix::crypt(password, hash).unwrap_or_default()
+ );
+ if !pwhash::unix::verify(password, hash) {
+ Err(AuthenticationError::IncorrectCredentials)
+ } else {
+ Ok(())
+ }
+ }
+}
diff --git a/bmcd/src/main.rs b/bmcd/src/main.rs
index 1a073a2..88ad732 100644
--- a/bmcd/src/main.rs
+++ b/bmcd/src/main.rs
@@ -1,4 +1,8 @@
-use crate::{flash_service::FlashService, legacy::info_config};
+use crate::config::Config;
+use crate::{
+ authentication::linux_authenticator::LinuxAuthenticator, flash_service::FlashService,
+ legacy::info_config,
+};
use actix_files::Files;
use actix_web::{
http::{self, KeepAlive},
@@ -9,15 +13,21 @@ use actix_web::{
use anyhow::Context;
use clap::{command, value_parser, Arg};
use log::LevelFilter;
-use openssl::ssl::SslAcceptorBuilder;
-use std::path::{Path, PathBuf};
+use openssl::pkey::{PKey, Private};
+use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslMethod};
+use openssl::x509::X509;
+use std::fs::OpenOptions;
+use std::io::Read;
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
use tpi_rs::app::{bmc_application::BmcApplication, event_application::run_event_listener};
+pub mod authentication;
pub mod config;
mod flash_service;
mod into_legacy_response;
mod legacy;
-use crate::config::Config;
-use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
const HTTPS_PORT: u16 = 443;
const HTTP_PORT: u16 = 80;
@@ -25,24 +35,21 @@ const HTTP_PORT: u16 = 80;
#[actix_web::main]
async fn main() -> anyhow::Result<()> {
init_logger();
-
- let config = Config::try_from(config_path()).context("Error parsing config file")?;
- let tls = load_tls_configuration(&config.tls.private_key, &config.tls.certificate)?;
- let tls6 = load_tls_configuration(&config.tls.private_key, &config.tls.certificate)?;
-
+ let (tls, tls6) = load_config()?;
let bmc = Data::new(BmcApplication::new().await?);
run_event_listener(bmc.clone().into_inner())?;
let flash_service = Data::new(FlashService::new());
+ let authentication = Arc::new(LinuxAuthenticator::new("/api/bmc/authenticate").await?);
let run_server = HttpServer::new(move || {
App::new()
- // Shared state: BmcApplication instance
.app_data(bmc.clone())
.app_data(flash_service.clone())
- // Legacy API
- .configure(legacy::config)
// Enable logger
.wrap(middleware::Logger::default())
+ .wrap(authentication.clone())
+ // Legacy API
+ .configure(legacy::config)
// Serve a static tree of files of the web UI. Must be the last item.
.service(Files::new("/", "/mnt/var/www/").index_file("index.html"))
})
@@ -111,12 +118,34 @@ fn config_path() -> PathBuf {
.into()
}
-fn load_tls_configuration>(
+fn load_keys_from_pem>(
private_key: P,
certificate: P,
-) -> anyhow::Result {
- let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
- builder.set_private_key_file(private_key.as_ref(), SslFiletype::PEM)?;
- builder.set_certificate_chain_file(certificate.as_ref())?;
- Ok(builder)
+) -> anyhow::Result<(PKey, X509)> {
+ let mut pkey = Vec::new();
+ let mut cert = Vec::new();
+ OpenOptions::new()
+ .read(true)
+ .open(private_key)?
+ .read_to_end(&mut pkey)?;
+ OpenOptions::new()
+ .read(true)
+ .open(certificate)?
+ .read_to_end(&mut cert)?;
+
+ let rsa_key = PKey::private_key_from_pem(&pkey)?;
+ let x509 = X509::from_pem(&cert)?;
+ Ok((rsa_key, x509))
+}
+
+fn load_config() -> anyhow::Result<(SslAcceptorBuilder, SslAcceptorBuilder)> {
+ let config = Config::try_from(config_path()).context("Error parsing config file")?;
+ let (private_key, cert) = load_keys_from_pem(&config.tls.private_key, &config.tls.certificate)?;
+ let mut tls = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
+ tls.set_private_key(&private_key)?;
+ tls.set_certificate(&cert)?;
+ let mut tls6 = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
+ tls6.set_private_key(&private_key)?;
+ tls6.set_certificate(&cert)?;
+ Ok((tls, tls6))
}