From 4e046dbbc4448059553a9b9954ed9ba40dbe0e53 Mon Sep 17 00:00:00 2001 From: Sven Rademakers Date: Tue, 19 Sep 2023 08:17:42 +0100 Subject: [PATCH] Implement token-based authentication --- Cargo.lock | 354 ++++++++++++++++-- bmcd/Cargo.toml | 7 +- .../authentication/authentication_context.rs | 315 ++++++++++++++++ .../authentication/authentication_errors.rs | 70 ++++ .../authentication/authentication_service.rs | 127 +++++++ .../src/authentication/linux_authenticator.rs | 84 +++++ bmcd/src/authentication/mod.rs | 6 + bmcd/src/authentication/passwd_validator.rs | 19 + bmcd/src/authentication/token_generator.rs | 34 ++ bmcd/src/main.rs | 53 ++- 10 files changed, 1030 insertions(+), 39 deletions(-) create mode 100644 bmcd/src/authentication/authentication_context.rs create mode 100644 bmcd/src/authentication/authentication_errors.rs create mode 100644 bmcd/src/authentication/authentication_service.rs create mode 100644 bmcd/src/authentication/linux_authenticator.rs create mode 100644 bmcd/src/authentication/mod.rs create mode 100644 bmcd/src/authentication/passwd_validator.rs create mode 100644 bmcd/src/authentication/token_generator.rs diff --git a/Cargo.lock b/Cargo.lock index dc7a57f..7bd1b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ "actix-tls", "actix-utils", "ahash", - "base64", + "base64 0.21.4", "bitflags 2.4.0", "brotli", "bytes", @@ -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", ] @@ -374,6 +374,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.4" @@ -413,6 +419,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 +437,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" @@ -433,9 +459,12 @@ dependencies = [ "clap", "futures", "if-addrs", + "jsonwebtoken", "log", + "mockall", "nix 0.26.4", "openssl", + "pwhash", "rand 0.8.5", "serde", "serde_json", @@ -486,6 +515,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,6 +564,15 @@ 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" @@ -639,6 +683,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,16 +712,43 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[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", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -738,6 +819,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -768,6 +858,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -951,6 +1047,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" @@ -1069,6 +1175,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1093,6 +1208,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.4", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1162,6 +1291,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" @@ -1223,6 +1363,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nix" version = "0.23.2" @@ -1249,6 +1416,33 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1313,6 +1507,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" @@ -1380,6 +1580,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1410,6 +1619,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1429,6 +1668,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 +1829,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 +1876,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 +1901,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 +1942,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 +2002,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]] @@ -1768,6 +2048,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "simple_logger" version = "4.2.0" @@ -1791,9 +2083,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 +2109,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" @@ -1855,6 +2153,12 @@ dependencies = [ "remove_dir_all", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.48" @@ -1877,9 +2181,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 +2196,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 +2268,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..b39e016 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,7 @@ tokio.workspace = true tokio-util.workspace = true futures.workspace = true serde.workspace = true -rand = "0.8.5" +jsonwebtoken = "8.3.0" + +[dev-dependencies] +mockall = "0.11.4" diff --git a/bmcd/src/authentication/authentication_context.rs b/bmcd/src/authentication/authentication_context.rs new file mode 100644 index 0000000..a23a09d --- /dev/null +++ b/bmcd/src/authentication/authentication_context.rs @@ -0,0 +1,315 @@ +use super::authentication_errors::AuthenticationError; +use super::authentication_errors::TokenError; +use super::passwd_validator::BcryptValidator; +use super::passwd_validator::PasswordValidator; +use super::token_generator::Hs512Generator; +use super::token_generator::TokenGenerator; +use actix_web::dev::ServiceRequest; +use actix_web::http::header; +use actix_web::http::header::HeaderValue; +use futures::StreamExt; +use jsonwebtoken::EncodingKey; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use std::marker::PhantomData; +use tokio::{ + sync::RwLock, + time::{Duration, Instant}, +}; + +pub const EXPIRATION: Duration = Duration::from_secs(60 * 60); + +pub struct AuthenticationContext +where + G: TokenGenerator, + P: PasswordValidator + 'static, +{ + token_store: RwLock>, + passwds: HashMap, + token_generator: G, + password_validator: PhantomData

, +} + +impl AuthenticationContext +where + G: TokenGenerator, + P: PasswordValidator, +{ + pub fn with_hs_512_generator( + password_entries: impl Iterator, + encoding_key: EncodingKey, + ) -> AuthenticationContext { + AuthenticationContext:: { + token_store: RwLock::new(HashMap::new()), + passwds: HashMap::from_iter(password_entries), + token_generator: Hs512Generator::new(encoding_key), + password_validator: PhantomData::, + } + } + + fn parse_http_token(auth_string: &str) -> Result<&str, TokenError> { + auth_string + .split_once(' ') + .ok_or(TokenError::HttpParseError(auth_string.to_string())) + .and_then(|(_, token)| { + (!token.is_empty()) + .then_some(token) + .ok_or(TokenError::Empty) + }) + } + + async fn verify_token(&self, auth_string: &str) -> Result<(), TokenError> { + let token = Self::parse_http_token(auth_string)?; + + let store = self.token_store.read().await; + let Some(expiration) = store.get(token).copied() else { + return Err(TokenError::NoMatch(token.to_string())); + }; + + let duration = Instant::now().saturating_duration_since(expiration); + if duration > EXPIRATION { + // upgrade to write lock + drop(store); + self.token_store.write().await.remove(token); + return Err(TokenError::Expired(expiration)); + } + + Ok(()) + } + + pub async fn authorize_request(&self, request: &ServiceRequest) -> Result<(), TokenError> { + let auth = request + .headers() + .get(header::AUTHORIZATION) + .map(HeaderValue::to_str) + .ok_or(TokenError::Empty)?; + + self.verify_token(auth.unwrap_or_default()).await + } + + pub async fn authenticate_request( + &self, + request: &mut ServiceRequest, + ) -> Result<(String, Instant), AuthenticationError> { + let mut buffer = Vec::new(); + while let Some(Ok(bytes)) = request.parts_mut().1.next().await { + buffer.extend_from_slice(&bytes); + } + + let credentials = serde_json::from_slice::(&buffer)?; + let Some(pass) = self.passwds.get(&credentials.username) else { + return Err(AuthenticationError::IncorrectCredentials); + }; + + P::validate(pass, &credentials.password)?; + + let token = self.token_generator.generate_token(&buffer)?; + let expires = Instant::now() + EXPIRATION; + self.token_store + .write() + .await + .insert(token.clone(), expires); + Ok((token, expires)) + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct Login { + username: String, + password: String, +} + +#[cfg(test)] +mod tests { + use std::ops::Sub; + + use super::*; + use crate::authentication::{ + authentication_context::EXPIRATION, token_generator::MockTokenGenerator, + }; + use actix_web::{ + http::header::AUTHORIZATION, + test::{self}, + }; + + struct DummyValidator {} + impl PasswordValidator for DummyValidator { + fn validate( + _: &str, + _: &str, + ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> { + Ok(()) + } + } + + struct FalseValidator {} + impl PasswordValidator for FalseValidator { + fn validate( + _: &str, + _: &str, + ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> { + Err(AuthenticationError::IncorrectCredentials) + } + } + + fn build_test_context( + token_data: impl Iterator, + user_data: impl Iterator, + ) -> AuthenticationContext { + let mut mock = MockTokenGenerator::default(); + mock.expect_generate_token() + .return_once(move |_: &Vec| Ok("token".to_string())); + AuthenticationContext { + token_store: RwLock::new(HashMap::from_iter(token_data)), + passwds: HashMap::from_iter(user_data), + token_generator: mock, + password_validator: PhantomData::, + } + } + + fn build_false_context( + token_data: impl Iterator, + user_data: impl Iterator, + ) -> AuthenticationContext { + let mock = MockTokenGenerator::default(); + AuthenticationContext { + token_store: RwLock::new(HashMap::from_iter(token_data)), + passwds: HashMap::from_iter(user_data), + token_generator: mock, + password_validator: PhantomData::, + } + } + + #[actix_web::test] + async fn test_wrongly_encoded_token() { + let context = build_test_context(Vec::new().into_iter(), Vec::new().into_iter()); + assert!(matches!( + context.verify_token("").await.unwrap_err(), + TokenError::HttpParseError(_), + )); + assert!(matches!( + context.verify_token("Bearernospace").await.unwrap_err(), + TokenError::HttpParseError(_), + )); + assert_eq!( + TokenError::Empty, + context.verify_token("Bearer ").await.unwrap_err() + ); + + let req = test::TestRequest::default().to_srv_request(); + assert_eq!( + TokenError::Empty, + context.authorize_request(&req).await.unwrap_err() + ); + } + + #[actix_web::test] + async fn test_token_failures() { + let context = build_test_context( + [ + ("123".to_string(), Instant::now()), + ("2".to_string(), Instant::now().sub(EXPIRATION)), + ] + .into_iter(), + Vec::new().into_iter(), + ); + + let req = test::TestRequest::default() + .insert_header((AUTHORIZATION, "Bearer 1234")) + .to_srv_request(); + + assert_eq!( + context.authorize_request(&req).await.unwrap_err(), + TokenError::NoMatch("1234".to_string()) + ); + + let req = test::TestRequest::default() + .insert_header((AUTHORIZATION, "Bearer 2")) + .to_srv_request(); + assert!(matches!( + context.authorize_request(&req).await.unwrap_err(), + TokenError::Expired(x) if Instant::now().saturating_duration_since(x) > EXPIRATION + )); + } + + #[actix_web::test] + async fn test_happy_flow() { + let context = build_test_context( + [ + ("123".to_string(), Instant::now()), + ("2".to_string(), Instant::now().sub(EXPIRATION)), + ] + .into_iter(), + Vec::new().into_iter(), + ); + let req = test::TestRequest::default() + .insert_header((AUTHORIZATION, "Bearer 123")) + .to_srv_request(); + assert_eq!(Ok(()), context.authorize_request(&req).await); + } + + #[actix_web::test] + async fn authentication_errors() { + let context = build_test_context( + Vec::new().into_iter(), + [("test_user".to_string(), "password".to_string())].into_iter(), + ); + + let mut req = test::TestRequest::default() + .set_payload("{not a valid json") + .to_srv_request(); + assert!(matches!( + context.authenticate_request(&mut req).await.unwrap_err(), + AuthenticationError::ParseError(_) + )); + + let mut req = test::TestRequest::default() + .set_json(Login { + username: "John".to_string(), + password: "1234".to_string(), + }) + .to_srv_request(); + assert!(matches!( + context.authenticate_request(&mut req).await.unwrap_err(), + AuthenticationError::IncorrectCredentials + )); + } + + #[actix_web::test] + async fn invalid_password() { + let context = build_false_context( + Vec::new().into_iter(), + [("test_user".to_string(), "password".to_string())].into_iter(), + ); + let mut req = test::TestRequest::default() + .set_json(Login { + username: "test_user".to_string(), + password: "1234".to_string(), + }) + .to_srv_request(); + assert!(matches!( + context.authenticate_request(&mut req).await.unwrap_err(), + AuthenticationError::IncorrectCredentials + )); + } + + #[actix_web::test] + async fn pass_authentication() { + let context = build_test_context( + Vec::new().into_iter(), + [("test_user".to_string(), "password".to_string())].into_iter(), + ); + let mut req = test::TestRequest::default() + .set_json(Login { + username: "test_user".to_string(), + password: "password".to_string(), + }) + .to_srv_request(); + + assert!(matches!( + context.authenticate_request(&mut req).await.unwrap(), + (token, _) if token == "token" + )); + } +} diff --git a/bmcd/src/authentication/authentication_errors.rs b/bmcd/src/authentication/authentication_errors.rs new file mode 100644 index 0000000..78ca922 --- /dev/null +++ b/bmcd/src/authentication/authentication_errors.rs @@ -0,0 +1,70 @@ +use std::fmt::Display; +use tokio::time::Instant; + +#[derive(Debug)] +pub enum AuthenticationError { + ParseError(serde_json::Error), + IncorrectCredentials, + InvalidToken(TokenError), +} + +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 credetials: {}", e) + } + AuthenticationError::IncorrectCredentials => write!(f, "credetials incorrect"), + AuthenticationError::InvalidToken(token_error) => token_error.fmt(f), + } + } +} + +impl std::error::Error for AuthenticationError {} + +impl From for AuthenticationError { + fn from(value: serde_json::Error) -> Self { + Self::ParseError(value) + } +} + +impl From for AuthenticationError { + fn from(value: TokenError) -> Self { + Self::InvalidToken(value) + } +} + +#[derive(Debug, PartialEq)] +pub enum TokenError { + Expired(Instant), + NoMatch(String), + HttpParseError(String), + TokenGenerationError(jsonwebtoken::errors::Error), + Empty, +} + +impl Display for TokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TokenError::Expired(instant) => write!( + f, + "token expired {}s ago", + Instant::now().duration_since(*instant).as_secs() + ), + TokenError::NoMatch(token) => write!(f, "token {} is not registerd", token), + TokenError::Empty => write!(f, "empty token provided"), + TokenError::HttpParseError(token) => write!(f, "cannot parse token {}", token), + TokenError::TokenGenerationError(e) => { + write!(f, "could not generate access-token: {}", e) + } + } + } +} + +impl std::error::Error for TokenError {} + +impl From for TokenError { + fn from(value: jsonwebtoken::errors::Error) -> Self { + Self::TokenGenerationError(value) + } +} diff --git a/bmcd/src/authentication/authentication_service.rs b/bmcd/src/authentication/authentication_service.rs new file mode 100644 index 0000000..e4435a3 --- /dev/null +++ b/bmcd/src/authentication/authentication_service.rs @@ -0,0 +1,127 @@ +use super::{ + authentication_context::AuthenticationContext, passwd_validator::PasswordValidator, + token_generator::TokenGenerator, +}; +use actix_web::{ + body::{EitherBody, MessageBody}, + dev::{Service, ServiceRequest, ServiceResponse}, + http::header, + Error, HttpRequest, HttpResponse, +}; +use futures::future::LocalBoxFuture; +use serde_json::json; +use std::{rc::Rc, sync::Arc, time::Duration}; +use tokio::time::Instant; + +#[derive(Clone)] +pub struct AuthenticationService +where + G: TokenGenerator + 'static, + P: PasswordValidator + 'static, +{ + service: Rc, + context: Arc>, +} + +impl AuthenticationService +where + G: TokenGenerator, + P: PasswordValidator, +{ + pub fn new(service: Rc, context: Arc>) -> Self { + AuthenticationService { service, context } + } +} + +impl Service for AuthenticationService +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: MessageBody + 'static, + G: TokenGenerator, + P: PasswordValidator, +{ + 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(); + + Box::pin(async move { + // authentication request + let request_clone = request.request().clone(); + if request.request().uri().path() == "authenticate" { + match context.authenticate_request(&mut request).await { + Ok((token, expires)) => { + return authenticated_response( + request_clone, + token, + expires.saturating_duration_since(Instant::now()), + ); + } + Err(error) => return forbidden_response(request_clone, error), + } + } + + // authorize request + if let Err(e) = context.authorize_request(&request).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, + duration: Duration, +) -> Result>, Error> { + let body = json! {{ + "access_token" : token, + "token_type" : "Bearer", + "expires_in": duration.as_secs(), + }}; + + Ok(ServiceResponse::new( + request, + HttpResponse::Ok() + .insert_header(("X-Auth-Token", token)) + .body(body.to_string()) + .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..1f7e9a1 --- /dev/null +++ b/bmcd/src/authentication/linux_authenticator.rs @@ -0,0 +1,84 @@ +use super::{ + authentication_context::AuthenticationContext, authentication_service::AuthenticationService, + passwd_validator::BcryptValidator, token_generator::Hs512Generator, +}; +use actix_web::{ + body::{EitherBody, MessageBody}, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + Error, +}; +use jsonwebtoken::EncodingKey; +use std::{ + future::{ready, Ready}, + io, +}; +use std::{rc::Rc, sync::Arc}; +use tokio::{ + fs::OpenOptions, + io::{AsyncBufReadExt, BufReader}, +}; + +type LinuxContext = AuthenticationContext; + +pub struct LinuxAuthenticator { + context: Arc, +} + +impl LinuxAuthenticator { + pub async fn new(private_key: &[u8]) -> io::Result { + let password_entries = Self::parse_shadow_file().await?; + let encoding_key = EncodingKey::from_rsa_pem(private_key).unwrap(); + Ok(Self { + context: Arc::new(LinuxContext::with_hs_512_generator( + password_entries, + encoding_key, + )), + }) + } +} + +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; + } + + // Passwords are salted, therefore their hashes are unique. Simplify + // the credential lookup by omitting the username. Smoking foot-gun? + if let Some((user, password_hashed)) = line.split_once(':') { + if password_hashed != "*" { + password_hashes.push((user.to_string(), password_hashed.to_string())); + } + } + } + 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(), + ))) + } +} diff --git a/bmcd/src/authentication/mod.rs b/bmcd/src/authentication/mod.rs new file mode 100644 index 0000000..91b486d --- /dev/null +++ b/bmcd/src/authentication/mod.rs @@ -0,0 +1,6 @@ +pub mod authentication_context; +pub mod authentication_errors; +pub mod authentication_service; +pub mod linux_authenticator; +pub mod passwd_validator; +pub mod token_generator; diff --git a/bmcd/src/authentication/passwd_validator.rs b/bmcd/src/authentication/passwd_validator.rs new file mode 100644 index 0000000..43c70fd --- /dev/null +++ b/bmcd/src/authentication/passwd_validator.rs @@ -0,0 +1,19 @@ +use pwhash::bcrypt; + +use super::authentication_errors::AuthenticationError; + +pub trait PasswordValidator { + fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError>; +} + +pub struct BcryptValidator {} + +impl PasswordValidator for BcryptValidator { + fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError> { + if !bcrypt::verify(hash, password) { + Err(AuthenticationError::IncorrectCredentials) + } else { + Ok(()) + } + } +} diff --git a/bmcd/src/authentication/token_generator.rs b/bmcd/src/authentication/token_generator.rs new file mode 100644 index 0000000..5ff3558 --- /dev/null +++ b/bmcd/src/authentication/token_generator.rs @@ -0,0 +1,34 @@ +use super::authentication_errors::TokenError; +use jsonwebtoken::{encode, Algorithm}; +use jsonwebtoken::{EncodingKey, Header}; +#[cfg(test)] +use mockall::automock; +use serde::Serialize; + +#[cfg_attr(test, automock)] +pub trait TokenGenerator { + fn generate_token( + &self, + data: &T, + ) -> Result; +} + +pub struct Hs512Generator { + encoding_key: EncodingKey, +} + +impl Hs512Generator { + pub fn new(encoding_key: EncodingKey) -> Self { + Self { encoding_key } + } +} + +impl TokenGenerator for Hs512Generator { + fn generate_token( + &self, + data: &T, + ) -> Result { + let header = Header::new(Algorithm::HS512); + Ok(encode(&header, &data, &self.encoding_key)?) + } +} diff --git a/bmcd/src/main.rs b/bmcd/src/main.rs index 1a073a2..63d67fe 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, 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; @@ -27,12 +37,18 @@ 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 (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)?; 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(&private_key.raw_private_key()?).await?); let run_server = HttpServer::new(move || { App::new() @@ -41,6 +57,7 @@ async fn main() -> anyhow::Result<()> { .app_data(flash_service.clone()) // Legacy API .configure(legacy::config) + .wrap(authentication.clone()) // Enable logger .wrap(middleware::Logger::default()) // Serve a static tree of files of the web UI. Must be the last item. @@ -111,12 +128,22 @@ 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)) }