diff --git a/Cargo.lock b/Cargo.lock index c12447e6e..1cd26dc88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1412,18 +1412,18 @@ dependencies = [ [[package]] name = "enum-map" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53337c2dbf26a3c31eccc73a37b10c1614e8d4ae99b6a50d553e8936423c1f16" +checksum = "ed40247825a1a0393b91b51d475ea1063a6cbbf0847592e7f13fb427aca6a716" dependencies = [ "enum-map-derive", ] [[package]] name = "enum-map-derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25" +checksum = "7933cd46e720348d29ed1493f89df9792563f272f96d8f13d18afe03b32f8cb8" dependencies = [ "proc-macro2", "quote", @@ -1479,9 +1479,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ "libc", "windows-sys 0.48.0", @@ -1803,9 +1803,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -2949,6 +2949,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "kanidm-hsm-crypto" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2710fd18cfe2f774e6d1e743dae86ae9a7332cad1bb85cb66777f7ef63578410" +dependencies = [ + "argon2", + "openssl", + "serde", + "tracing", + "tss-esapi", + "zeroize", +] + [[package]] name = "kanidm-ipa-sync" version = "1.1.0-rc.15-dev" @@ -3037,6 +3051,7 @@ dependencies = [ "base64 0.21.5", "base64urlsafedata", "hex", + "kanidm-hsm-crypto", "kanidm_proto", "openssl", "openssl-sys", @@ -3044,7 +3059,6 @@ dependencies = [ "serde", "sketching", "tracing", - "tss-esapi", "uuid", ] @@ -3121,6 +3135,7 @@ dependencies = [ "csv", "futures", "hashbrown 0.14.2", + "kanidm-hsm-crypto", "kanidm_build_profiles", "kanidm_client", "kanidm_lib_crypto", @@ -3143,7 +3158,6 @@ dependencies = [ "tokio-util", "toml", "tracing", - "tss-esapi", "uuid", "walkdir", ] @@ -3556,9 +3570,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -5040,9 +5054,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] @@ -5100,9 +5114,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a83fce2c5..27cc8ec5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,8 @@ sshkey-attest = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218 # webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" } # sshkey-attest = { path = "../webauthn-rs/sshkey-attest" } +# kanidm-hsm-crypto = { path = "../hsm-crypto" } + [workspace.dependencies] kanidmd_core = { path = "./server/core", version = "1.1.0-rc.15-dev" } kanidmd_lib = { path = "./server/lib", version = "1.1.0-rc.15-dev" } @@ -82,6 +84,7 @@ kanidmd_lib_macros = { path = "./server/lib-macros", version = "1.1.0-rc.15-dev" kanidmd_testkit = { path = "./server/testkit", version = "1.1.0-rc.15-dev" } kanidm_build_profiles = { path = "./libs/profiles", version = "1.1.0-rc.15-dev" } kanidm_client = { path = "./libs/client", version = "1.1.0-rc.15-dev" } +kanidm-hsm-crypto = "^0.1.1" kanidm_lib_crypto = { path = "./libs/crypto", version = "1.1.0-rc.15-dev" } kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "1.1.0-rc.15-dev" } kanidm_proto = { path = "./proto", version = "1.1.0-rc.15-dev" } diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml index 6604a9302..d9b08a092 100644 --- a/libs/crypto/Cargo.toml +++ b/libs/crypto/Cargo.toml @@ -7,9 +7,8 @@ license = { workspace = true } homepage = { workspace = true } repository = { workspace = true } - [features] -tpm = ["dep:tss-esapi"] +tpm = ["kanidm-hsm-crypto/tpm"] [lib] test = true @@ -21,6 +20,7 @@ base64 = { workspace = true } base64urlsafedata = { workspace = true } hex = { workspace = true } kanidm_proto = { workspace = true } +kanidm-hsm-crypto = { workspace = true } # We need to explicitly ask for openssl-sys so that we get the version propagated # into the build.rs for legacy feature checks. @@ -29,7 +29,6 @@ openssl = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } -tss-esapi = { workspace = true, optional = true } uuid = { workspace = true } [dev-dependencies] diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index e4ec436d0..cbdb77cac 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -29,17 +29,12 @@ use openssl::nid::Nid; use openssl::pkcs5::pbkdf2_hmac; use openssl::sha::Sha512; +use kanidm_hsm_crypto::{HmacKey, Tpm}; + pub mod mtls; pub mod prelude; pub mod serialise; -#[cfg(feature = "tpm")] -pub use tss_esapi::{handles::ObjectHandle as TpmHandle, Context as TpmContext, Error as TpmError}; -#[cfg(not(feature = "tpm"))] -pub struct TpmContext {} -#[cfg(not(feature = "tpm"))] -pub struct TpmHandle {} - // NIST 800-63.b salt should be 112 bits -> 14 8u8. const PBKDF2_SALT_LEN: usize = 24; @@ -68,11 +63,8 @@ const ARGON2_MAX_P_COST: u32 = 1; #[derive(Clone, Debug)] pub enum CryptoError { - Tpm2, - Tpm2PublicBuilder, - Tpm2FeatureMissing, - Tpm2InputExceeded, - Tpm2ContextMissing, + Hsm, + HsmContextMissing, OpenSSL(u64), Md4Disabled, Argon2, @@ -102,13 +94,6 @@ impl Into for CryptoError { } } -#[cfg(feature = "tpm")] -impl From for CryptoError { - fn from(_e: TpmError) -> Self { - CryptoError::Tpm2 - } -} - #[derive(Serialize, Deserialize)] #[allow(non_camel_case_types)] pub enum DbPasswordV1 { @@ -832,11 +817,11 @@ impl Password { .map(|material| Password { material }) } - pub fn new_argon2id_tpm( + pub fn new_argon2id_hsm( policy: &CryptoPolicy, cleartext: &str, - tpm_ctx: &mut TpmContext, - tpm_key_handle: TpmHandle, + hsm: &mut dyn Tpm, + hmac_key: &HmacKey, ) -> Result { let version = Version::V0x13; @@ -853,7 +838,12 @@ impl Password { check_key.as_mut_slice(), ) .map_err(|_| CryptoError::Argon2) - .and_then(|()| do_tpm_hmac(check_key, tpm_ctx, tpm_key_handle)) + .and_then(|()| { + hsm.hmac(hmac_key, &check_key).map_err(|err| { + error!(?err, "hsm error"); + CryptoError::Hsm + }) + }) .map(|key| Kdf::TPM_ARGON2ID { m_cost: policy.argon2id_params.m_cost(), t_cost: policy.argon2id_params.t_cost(), @@ -877,9 +867,9 @@ impl Password { pub fn verify_ctx( &self, cleartext: &str, - tpm: Option<(&mut TpmContext, TpmHandle)>, + hsm: Option<(&mut dyn Tpm, &HmacKey)>, ) -> Result { - match (&self.material, tpm) { + match (&self.material, hsm) { ( Kdf::TPM_ARGON2ID { m_cost, @@ -889,7 +879,7 @@ impl Password { salt, key, }, - Some((tpm_ctx, tpm_key_handle)), + Some((hsm, hmac_key)), ) => { let version: Version = (*version).try_into().map_err(|_| { error!("Failed to convert {} to valid argon2id version", version); @@ -917,15 +907,20 @@ impl Password { error!(err = ?e, "unable to perform argon2id hash"); CryptoError::Argon2 }) - .and_then(|()| do_tpm_hmac(check_key, tpm_ctx, tpm_key_handle)) + .and_then(|()| { + hsm.hmac(hmac_key, &check_key).map_err(|err| { + error!(?err, "hsm error"); + CryptoError::Hsm + }) + }) .map(|hmac_key| { // Actually compare the outputs. &hmac_key == key }) } (Kdf::TPM_ARGON2ID { .. }, None) => { - error!("Unable to validate password - not tpm context available"); - Err(CryptoError::Tpm2ContextMissing) + error!("Unable to validate password - not hsm context available"); + Err(CryptoError::HsmContextMissing) } ( Kdf::ARGON2ID { @@ -1190,41 +1185,10 @@ impl Password { } } -#[cfg(feature = "tpm")] -fn do_tpm_hmac( - data: Vec, - ctx: &mut TpmContext, - key_handle: TpmHandle, -) -> Result, CryptoError> { - use tss_esapi::interface_types::algorithm::HashingAlgorithm; - use tss_esapi::structures::MaxBuffer; - - let data: MaxBuffer = data.try_into().map_err(|_| { - error!("input data exceeds maximum tpm input buffer"); - CryptoError::Tpm2InputExceeded - })?; - - ctx.hmac(key_handle, data.into(), HashingAlgorithm::Sha256) - .map(|dgst| dgst.to_vec()) - .map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - }) -} - -#[cfg(not(feature = "tpm"))] -#[allow(clippy::needless_pass_by_value)] -fn do_tpm_hmac( - _data: Vec, - _ctx: &mut TpmContext, - _key_handle: TpmHandle, -) -> Result, CryptoError> { - error!("Unable to perform hmac - tpm feature not compiled"); - Err(CryptoError::Tpm2FeatureMissing) -} - #[cfg(test)] mod tests { + use kanidm_hsm_crypto::soft::SoftTpm; + use kanidm_hsm_crypto::AuthValue; use std::convert::TryFrom; use crate::*; @@ -1393,33 +1357,30 @@ mod tests { } } - #[cfg(feature = "tpm")] #[test] - fn test_password_argon2id_tpm_bind() { - use std::str::FromStr; - + fn test_password_argon2id_hsm_bind() { sketching::test_init(); - use tss_esapi::{Context, TctiNameConf}; + let mut hsm: Box = Box::new(SoftTpm::new()); - let mut context = - Context::new(TctiNameConf::from_str("device:/dev/tpmrm0").expect("Failed to get TCTI")) - .expect("Failed to create Context"); + let auth_value = AuthValue::new_random().unwrap(); - let key = context - .execute_with_nullauth_session(|ctx| prepare_tpm_key(ctx)) + let loadable_machine_key = hsm.machine_key_create(&auth_value).unwrap(); + let machine_key = hsm + .machine_key_load(&auth_value, &loadable_machine_key) .unwrap(); + let loadable_hmac_key = hsm.hmac_key_create(&machine_key).unwrap(); + let key = hsm.hmac_key_load(&machine_key, &loadable_hmac_key).unwrap(); + + let ctx: &mut dyn Tpm = &mut *hsm; + let p = CryptoPolicy::minimum(); - let c = context - .execute_with_nullauth_session(|ctx| { - Password::new_argon2id_tpm(&p, "password", ctx, key) - }) - .unwrap(); + let c = Password::new_argon2id_hsm(&p, "password", ctx, &key).unwrap(); assert!(matches!( c.verify("password"), - Err(CryptoError::Tpm2ContextMissing) + Err(CryptoError::HsmContextMissing) )); // Assert it fails without the hmac @@ -1446,83 +1407,8 @@ mod tests { assert!(!dup.verify("password").unwrap()); - context - .execute_with_nullauth_session(|ctx| { - assert!(c.verify_ctx("password", Some((ctx, key))).unwrap()); - assert!(!c.verify_ctx("password1", Some((ctx, key))).unwrap()); - assert!(!c.verify_ctx("Password1", Some((ctx, key))).unwrap()); - - ctx.flush_context(key).expect("Failed to unload hmac key"); - - // Should fail, no key! - assert!(matches!( - c.verify_ctx("password", Some((ctx, key))), - Err(CryptoError::Tpm2) - )); - - Ok::<(), CryptoError>(()) - }) - .unwrap(); - } - - #[cfg(feature = "tpm")] - pub fn prepare_tpm_key(tpm_ctx: &mut TpmContext) -> Result { - use tss_esapi::{ - attributes::ObjectAttributesBuilder, - interface_types::{ - algorithm::{HashingAlgorithm, PublicAlgorithm}, - resource_handles::Hierarchy, - }, - structures::{Digest, KeyedHashScheme, PublicBuilder, PublicKeyedHashParameters}, - }; - - // We generate a digest, which is really some unique small amount of data that - // we save into the key context that we are going to save/load. This allows us - // to have unique hmac keys compared to other users of the same tpm. - - let digest = tpm_ctx - .get_random(16) - .map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - }) - .and_then(|rand| { - Digest::try_from(rand).map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - }) - })?; - - let object_attributes = ObjectAttributesBuilder::new() - .with_sign_encrypt(true) - .with_sensitive_data_origin(true) - .with_user_with_auth(true) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - })?; - - let key_pub = PublicBuilder::new() - .with_public_algorithm(PublicAlgorithm::KeyedHash) - .with_name_hashing_algorithm(HashingAlgorithm::Sha256) - .with_object_attributes(object_attributes) - .with_keyed_hash_parameters(PublicKeyedHashParameters::new( - KeyedHashScheme::HMAC_SHA_256, - )) - .with_keyed_hash_unique_identifier(digest) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - })?; - - tpm_ctx - .create_primary(Hierarchy::Owner, key_pub, None, None, None, None) - .map(|key| key.key_handle.into()) - .map_err(|e| { - error!(tpm_err = ?e, "unable to proceed, tpm error"); - CryptoError::Tpm2 - }) + assert!(c.verify_ctx("password", Some((ctx, &key))).unwrap()); + assert!(!c.verify_ctx("password1", Some((ctx, &key))).unwrap()); + assert!(!c.verify_ctx("Password1", Some((ctx, &key))).unwrap()); } } diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index 6eb4d6756..319a439a2 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -15,7 +15,7 @@ repository = { workspace = true } default = ["unix"] unix = [] selinux = ["dep:selinux"] -tpm = ["dep:tss-esapi", "kanidm_lib_crypto/tpm"] +tpm = ["kanidm-hsm-crypto/tpm"] [[bin]] name = "kanidm_unixd" @@ -64,6 +64,7 @@ libsqlite3-sys = { workspace = true } lru = { workspace = true } kanidm_client = { workspace = true } kanidm_proto = { workspace = true } +kanidm-hsm-crypto = { workspace = true } kanidm_lib_crypto = { workspace = true } kanidm_lib_file_permissions = { workspace = true } notify-debouncer-full = { workspace = true } @@ -78,7 +79,6 @@ toml = { workspace = true } tokio = { workspace = true, features = ["rt", "fs", "macros", "sync", "time", "net", "io-util"] } tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } -tss-esapi = { workspace = true, optional = true } uuid = { workspace = true } walkdir = { workspace = true } diff --git a/unix_integration/src/constants.rs b/unix_integration/src/constants.rs index 368ed9fa3..ec69453ce 100644 --- a/unix_integration/src/constants.rs +++ b/unix_integration/src/constants.rs @@ -15,3 +15,4 @@ pub const DEFAULT_UID_ATTR_MAP: UidAttr = UidAttr::Spn; pub const DEFAULT_GID_ATTR_MAP: UidAttr = UidAttr::Spn; pub const DEFAULT_SELINUX: bool = true; pub const DEFAULT_TPM_TCTI_NAME: &str = "device:/dev/tpmrm0"; +pub const DEFAULT_HSM_PIN_PATH: &str = "/etc/kanidm/unixd-hsm-pin"; diff --git a/unix_integration/src/daemon.rs b/unix_integration/src/daemon.rs index c1ab64a39..a5eb1fdb8 100644 --- a/unix_integration/src/daemon.rs +++ b/unix_integration/src/daemon.rs @@ -26,11 +26,11 @@ use futures::{SinkExt, StreamExt}; use kanidm_client::KanidmClientBuilder; use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; -use kanidm_unix_common::db::Db; +use kanidm_unix_common::db::{Cache, CacheTxn, Db}; use kanidm_unix_common::idprovider::kanidm::KanidmProvider; // use kanidm_unix_common::idprovider::interface::AuthSession; use kanidm_unix_common::resolver::Resolver; -use kanidm_unix_common::unix_config::KanidmUnixdConfig; +use kanidm_unix_common::unix_config::{HsmType, KanidmUnixdConfig}; use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd}; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, TaskRequest, TaskResponse}; @@ -48,6 +48,8 @@ use tokio::sync::oneshot; use tokio::time; use tokio_util::codec::{Decoder, Encoder, Framed}; +use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, Tpm}; + use notify_debouncer_full::{new_debouncer, notify::RecursiveMode, notify::Watcher}; //=== the codec @@ -434,6 +436,13 @@ async fn process_etc_passwd_group( Ok(()) } +async fn read_hsm_pin(hsm_pin_path: &str) -> Result, Box> { + let mut file = File::open(hsm_pin_path).await?; + let mut contents = vec![]; + file.read_to_end(&mut contents).await?; + Ok(contents) +} + #[tokio::main(flavor = "current_thread")] async fn main() -> ExitCode { let cuid = get_current_uid(); @@ -619,7 +628,6 @@ async fn main() -> ExitCode { rm_if_exist(cfg.sock_path.as_str()); rm_if_exist(cfg.task_sock_path.as_str()); - // Check the db path will be okay. if !cfg.db_path.is_empty() { let db_path = PathBuf::from(cfg.db_path.as_str()); @@ -716,7 +724,7 @@ async fn main() -> ExitCode { let idprovider = KanidmProvider::new(rsclient); - let db = match Db::new(cfg.db_path.as_str(), &cfg.tpm_policy) { + let db = match Db::new(cfg.db_path.as_str()) { Ok(db) => db, Err(_e) => { error!("Failed to create database"); @@ -724,9 +732,81 @@ async fn main() -> ExitCode { } }; + // read the hsm pin + let hsm_pin = match read_hsm_pin(cfg.hsm_pin_path.as_str()).await { + Ok(hp) => hp, + Err(err) => { + error!(?err, "Failed to read hsm pin"); + return ExitCode::FAILURE + } + }; + + let auth_value = match AuthValue::try_from(hsm_pin.as_slice()) { + Ok(av) => av, + Err(err) => { + error!(?err, "invalid hsm pin"); + return ExitCode::FAILURE + } + }; + + let mut hsm: Box = match cfg.hsm_type { + HsmType::Soft => { + Box::new(SoftTpm::new()) + } + HsmType::Tpm => { + error!("TPM not supported ... yet"); + return ExitCode::FAILURE + } + }; + + // With the assistance of the db, setup the hsm and it's machine key. + let db_txn = db.write().await; + + let loadable_machine_key = match db_txn.get_hsm_machine_key() { + Ok(Some(lmk)) => lmk, + Ok(None) => { + // No machine key found - create one, and store it. + let loadable_machine_key = match hsm.machine_key_create(&auth_value) { + Ok(lmk) => lmk, + Err(err) => { + error!(?err, "Unable to create hsm loadable machine key"); + return ExitCode::FAILURE + } + }; + + if let Err(err) = db_txn.insert_hsm_machine_key(&loadable_machine_key) { + error!(?err, "Unable to persist hsm loadable machine key"); + return ExitCode::FAILURE + } + + loadable_machine_key + } + Err(err) => { + error!(?err, "Unable to access hsm loadable machine key"); + return ExitCode::FAILURE + } + }; + + let machine_key = match hsm.machine_key_load(&auth_value, &loadable_machine_key) { + Ok(mk) => mk, + Err(err) => { + error!(?err, "Unable to load machine key"); + return ExitCode::FAILURE + } + }; + + if let Err(err) = db_txn.commit() { + error!(?err, "Failed to commit database transaction, unable to proceed"); + return ExitCode::FAILURE + } + + // Okay, the hsm is now loaded and ready to go. + let cl_inner = match Resolver::new( db, idprovider, + hsm, + machine_key, cfg.cache_timeout, cfg.pam_allowed_login_groups.clone(), cfg.default_shell.clone(), diff --git a/unix_integration/src/db.rs b/unix_integration/src/db.rs index 89e5b65c1..19426a750 100644 --- a/unix_integration/src/db.rs +++ b/unix_integration/src/db.rs @@ -3,16 +3,19 @@ use std::fmt; use std::time::Duration; use crate::idprovider::interface::{GroupToken, Id, UserToken}; -use crate::unix_config::TpmPolicy; use async_trait::async_trait; use kanidm_lib_crypto::CryptoPolicy; use kanidm_lib_crypto::DbPasswordV1; use kanidm_lib_crypto::Password; use libc::umask; -use rusqlite::Connection; +use rusqlite::{Connection, OptionalExtension}; use tokio::sync::{Mutex, MutexGuard}; use uuid::Uuid; +use serde::{de::DeserializeOwned, Serialize}; + +use kanidm_hsm_crypto::{HmacKey, LoadableHmacKey, LoadableMachineKey, Tpm}; + #[async_trait] pub trait Cache { type Txn<'db> @@ -42,6 +45,14 @@ pub trait CacheTxn { fn clear(&self) -> Result<(), CacheError>; + fn get_hsm_machine_key(&self) -> Result, CacheError>; + + fn insert_hsm_machine_key(&self, machine_key: &LoadableMachineKey) -> Result<(), CacheError>; + + fn get_hsm_hmac_key(&self) -> Result, CacheError>; + + fn insert_hsm_hmac_key(&self, hmac_key: &LoadableHmacKey) -> Result<(), CacheError>; + fn get_account(&self, account_id: &Id) -> Result, CacheError>; fn get_accounts(&self) -> Result, CacheError>; @@ -50,9 +61,21 @@ pub trait CacheTxn { fn delete_account(&self, a_uuid: Uuid) -> Result<(), CacheError>; - fn update_account_password(&self, a_uuid: Uuid, cred: &str) -> Result<(), CacheError>; + fn update_account_password( + &self, + a_uuid: Uuid, + cred: &str, + hsm: &mut dyn Tpm, + hmac_key: &HmacKey, + ) -> Result<(), CacheError>; - fn check_account_password(&self, a_uuid: Uuid, cred: &str) -> Result; + fn check_account_password( + &self, + a_uuid: Uuid, + cred: &str, + hsm: &mut dyn Tpm, + hmac_key: &HmacKey, + ) -> Result; fn get_group(&self, grp_id: &Id) -> Result, CacheError>; @@ -68,14 +91,12 @@ pub trait CacheTxn { pub struct Db { conn: Mutex, crypto_policy: CryptoPolicy, - require_tpm: Option, } pub struct DbTxn<'a> { conn: MutexGuard<'a, Connection>, committed: bool, crypto_policy: &'a CryptoPolicy, - require_tpm: Option<&'a tpm::TpmConfig>, } #[derive(Debug)] @@ -86,7 +107,7 @@ pub enum DbError { } impl Db { - pub fn new(path: &str, tpm_policy: &TpmPolicy) -> Result { + pub fn new(path: &str) -> Result { let before = unsafe { umask(0o0027) }; let conn = Connection::open(path).map_err(|e| { error!(err = ?e, "rusqulite error"); @@ -99,20 +120,9 @@ impl Db { debug!("Configured {:?}", crypto_policy); - // Test if we have a tpm context. - - let require_tpm = match tpm_policy { - TpmPolicy::Ignore => None, - TpmPolicy::IfPossible(tcti_str) => Db::tpm_setup_context(tcti_str, &conn).ok(), - TpmPolicy::Required(tcti_str) => { - Some(Db::tpm_setup_context(tcti_str, &conn).map_err(|_| DbError::Tpm)?) - } - }; - Ok(Db { conn: Mutex::new(conn), crypto_policy, - require_tpm, }) } } @@ -124,7 +134,7 @@ impl Cache for Db { #[allow(clippy::expect_used)] async fn write<'db>(&'db self) -> Self::Txn<'db> { let conn = self.conn.lock().await; - DbTxn::new(conn, &self.crypto_policy, self.require_tpm.as_ref()) + DbTxn::new(conn, &self.crypto_policy) } } @@ -135,11 +145,7 @@ impl fmt::Debug for Db { } impl<'a> DbTxn<'a> { - fn new( - conn: MutexGuard<'a, Connection>, - crypto_policy: &'a CryptoPolicy, - require_tpm: Option<&'a tpm::TpmConfig>, - ) -> Self { + fn new(conn: MutexGuard<'a, Connection>, crypto_policy: &'a CryptoPolicy) -> Self { // Start the transaction // debug!("Starting db WR txn ..."); #[allow(clippy::expect_used)] @@ -149,7 +155,6 @@ impl<'a> DbTxn<'a> { committed: false, conn, crypto_policy, - require_tpm, } } @@ -248,6 +253,72 @@ impl<'a> DbTxn<'a> { .collect(); data } + + pub fn get_tagged_hsm_key( + &self, + tag: &str, + ) -> Result, CacheError> { + let mut stmt = self + .conn + .prepare("SELECT value FROM hsm_data_t WHERE key = :key") + .map_err(|e| self.sqlite_error("select prepare", &e))?; + + let data: Option> = stmt + .query_row( + named_params! { + ":key": tag + }, + |row| row.get(0), + ) + .optional() + .map_err(|e| self.sqlite_error("query_row", &e))?; + + match data { + Some(d) => Ok(serde_json::from_slice(d.as_slice()) + .map_err(|e| { + error!("json error -> {:?}", e); + }) + .ok()), + None => Ok(None), + } + } + + pub fn insert_tagged_hsm_key( + &self, + tag: &str, + key: &K, + ) -> Result<(), CacheError> { + let data = serde_json::to_vec(key).map_err(|e| { + error!("insert_hsm_machine_key json error -> {:?}", e); + CacheError::SerdeJson + })?; + + let mut stmt = self + .conn + .prepare("INSERT OR REPLACE INTO hsm_int_t (key, value) VALUES (:key, :value)") + .map_err(|e| self.sqlite_error("prepare", &e))?; + + stmt.execute(named_params! { + ":key": tag, + ":value": &data, + }) + .map(|r| { + debug!("insert -> {:?}", r); + }) + .map_err(|e| self.sqlite_error("execute", &e)) + } + + pub fn delete_tagged_hsm_key(&self, tag: &str) -> Result<(), CacheError> { + self.conn + .execute( + "DELETE FROM hsm_data_t where key = :key", + named_params! { + ":key": tag, + }, + ) + .map(|_| ()) + .map_err(|e| self.sqlite_error("delete hsm_data_t", &e)) + } } impl<'a> CacheTxn for DbTxn<'a> { @@ -311,6 +382,30 @@ impl<'a> CacheTxn for DbTxn<'a> { ) .map_err(|e| self.sqlite_error("memberof_t create error", &e))?; + // Create the hsm_data store. These are all generally encrypted private + // keys, and the hsm structures will decrypt these as required. + self.conn + .execute( + "CREATE TABLE IF NOT EXISTS hsm_int_t ( + key TEXT PRIMARY KEY, + value BLOB NOT NULL + ) + ", + [], + ) + .map_err(|e| self.sqlite_error("hsm_int_t create error", &e))?; + + self.conn + .execute( + "CREATE TABLE IF NOT EXISTS hsm_data_t ( + key TEXT PRIMARY KEY, + value BLOB NOT NULL + ) + ", + [], + ) + .map_err(|e| self.sqlite_error("hsm_data_t create error", &e))?; + Ok(()) } @@ -356,6 +451,90 @@ impl<'a> CacheTxn for DbTxn<'a> { Ok(()) } + fn get_hsm_machine_key(&self) -> Result, CacheError> { + let mut stmt = self + .conn + .prepare("SELECT value FROM hsm_int_t WHERE key = 'mk'") + .map_err(|e| self.sqlite_error("select prepare", &e))?; + + let data: Option> = stmt + .query_row([], |row| row.get(0)) + .optional() + .map_err(|e| self.sqlite_error("query_row", &e))?; + + match data { + Some(d) => Ok(serde_json::from_slice(d.as_slice()) + .map_err(|e| { + error!("json error -> {:?}", e); + }) + .ok()), + None => Ok(None), + } + } + + fn insert_hsm_machine_key(&self, machine_key: &LoadableMachineKey) -> Result<(), CacheError> { + let data = serde_json::to_vec(machine_key).map_err(|e| { + error!("insert_hsm_machine_key json error -> {:?}", e); + CacheError::SerdeJson + })?; + + let mut stmt = self + .conn + .prepare("INSERT OR REPLACE INTO hsm_int_t (key, value) VALUES (:key, :value)") + .map_err(|e| self.sqlite_error("prepare", &e))?; + + stmt.execute(named_params! { + ":key": "mk", + ":value": &data, + }) + .map(|r| { + debug!("insert -> {:?}", r); + }) + .map_err(|e| self.sqlite_error("execute", &e)) + } + + fn get_hsm_hmac_key(&self) -> Result, CacheError> { + let mut stmt = self + .conn + .prepare("SELECT value FROM hsm_int_t WHERE key = 'hmac'") + .map_err(|e| self.sqlite_error("select prepare", &e))?; + + let data: Option> = stmt + .query_row([], |row| row.get(0)) + .optional() + .map_err(|e| self.sqlite_error("query_row", &e))?; + + match data { + Some(d) => Ok(serde_json::from_slice(d.as_slice()) + .map_err(|e| { + error!("json error -> {:?}", e); + }) + .ok()), + None => Ok(None), + } + } + + fn insert_hsm_hmac_key(&self, hmac_key: &LoadableHmacKey) -> Result<(), CacheError> { + let data = serde_json::to_vec(hmac_key).map_err(|e| { + error!("insert_hsm_hmac_key json error -> {:?}", e); + CacheError::SerdeJson + })?; + + let mut stmt = self + .conn + .prepare("INSERT OR REPLACE INTO hsm_int_t (key, value) VALUES (:key, :value)") + .map_err(|e| self.sqlite_error("prepare", &e))?; + + stmt.execute(named_params! { + ":key": "hmac", + ":value": &data, + }) + .map(|r| { + debug!("insert -> {:?}", r); + }) + .map_err(|e| self.sqlite_error("execute", &e)) + } + fn get_account(&self, account_id: &Id) -> Result, CacheError> { let data = match account_id { Id::Name(n) => self.get_account_data_name(n.as_str()), @@ -535,24 +714,18 @@ impl<'a> CacheTxn for DbTxn<'a> { .map_err(|e| self.sqlite_error("account_t delete", &e)) } - fn update_account_password(&self, a_uuid: Uuid, cred: &str) -> Result<(), CacheError> { - #[allow(unused_variables)] - let pw = if let Some(tcti_str) = self.require_tpm { - // Do nothing. - #[cfg(not(feature = "tpm"))] - return Ok(()); - - #[cfg(feature = "tpm")] - let pw = - Db::tpm_new(self.crypto_policy, cred, tcti_str).map_err(|()| CacheError::Tpm)?; - #[cfg(feature = "tpm")] - pw - } else { - Password::new(self.crypto_policy, cred).map_err(|e| { + fn update_account_password( + &self, + a_uuid: Uuid, + cred: &str, + hsm: &mut dyn Tpm, + hmac_key: &HmacKey, + ) -> Result<(), CacheError> { + let pw = + Password::new_argon2id_hsm(self.crypto_policy, cred, hsm, hmac_key).map_err(|e| { error!("password error -> {:?}", e); CacheError::Cryptography - })? - }; + })?; let dbpw = pw.to_dbpasswordv1(); let data = serde_json::to_vec(&dbpw).map_err(|e| { @@ -572,12 +745,13 @@ impl<'a> CacheTxn for DbTxn<'a> { .map(|_| ()) } - fn check_account_password(&self, a_uuid: Uuid, cred: &str) -> Result { - #[cfg(not(feature = "tpm"))] - if self.require_tpm.is_some() { - return Ok(false); - } - + fn check_account_password( + &self, + a_uuid: Uuid, + cred: &str, + hsm: &mut dyn Tpm, + hmac_key: &HmacKey, + ) -> Result { let mut stmt = self .conn .prepare("SELECT password FROM account_t WHERE uuid = :a_uuid AND password IS NOT NULL") @@ -616,22 +790,10 @@ impl<'a> CacheTxn for DbTxn<'a> { _ => return Ok(false), }; - #[allow(unused_variables)] - if let Some(tcti_str) = self.require_tpm { - #[cfg(feature = "tpm")] - let r = Db::tpm_verify(pw, cred, tcti_str).map_err(|()| CacheError::Tpm); - - // Do nothing. - #[cfg(not(feature = "tpm"))] - let r = Ok(false); - - r - } else { - pw.verify(cred).map_err(|e| { - error!("password error -> {:?}", e); - CacheError::Cryptography - }) - } + pw.verify_ctx(cred, Some((hsm, hmac_key))).map_err(|e| { + error!("password error -> {:?}", e); + CacheError::Cryptography + }) } fn get_group(&self, grp_id: &Id) -> Result, CacheError> { @@ -792,367 +954,12 @@ impl<'a> Drop for DbTxn<'a> { } } -#[cfg(not(feature = "tpm"))] -pub(crate) mod tpm { - use super::{Db, DbError}; - - use rusqlite::Connection; - - pub struct TpmConfig {} - - impl Db { - pub fn tpm_setup_context( - _tcti_str: &str, - _conn: &Connection, - ) -> Result { - warn!("tpm feature is not available in this build"); - Err(DbError::Tpm) - } - } -} - -#[cfg(feature = "tpm")] -pub(crate) mod tpm { - use super::Db; - - use rusqlite::{Connection, OptionalExtension}; - - use kanidm_lib_crypto::{CryptoError, CryptoPolicy, Password, TpmError}; - use tss_esapi::{Context, TctiNameConf}; - - use tss_esapi::{ - attributes::ObjectAttributesBuilder, - handles::KeyHandle, - interface_types::{ - algorithm::{HashingAlgorithm, PublicAlgorithm}, - resource_handles::Hierarchy, - }, - structures::{ - Digest, KeyedHashScheme, Private, Public, PublicBuilder, PublicKeyedHashParameters, - SymmetricCipherParameters, SymmetricDefinitionObject, - }, - traits::{Marshall, UnMarshall}, - }; - - use std::str::FromStr; - - use base64urlsafedata::Base64UrlSafeData; - use serde::{Deserialize, Serialize}; - - #[derive(Serialize, Deserialize, Debug)] - struct BinaryLoadableKey { - private: Base64UrlSafeData, - public: Base64UrlSafeData, - } - - impl Into for LoadableKey { - fn into(self) -> BinaryLoadableKey { - BinaryLoadableKey { - private: self.private.as_slice().to_owned().into(), - public: self.public.marshall().unwrap().into(), - } - } - } - - impl TryFrom for LoadableKey { - type Error = &'static str; - - fn try_from(value: BinaryLoadableKey) -> Result { - let private = Private::try_from(value.private.0).map_err(|e| { - error!(tpm_err = ?e, "Failed to restore tpm hmac key"); - "private try from" - })?; - - let public = Public::unmarshall(value.public.0.as_slice()).map_err(|e| { - error!(tpm_err = ?e, "Failed to restore tpm hmac key"); - "public unmarshall" - })?; - - Ok(LoadableKey { public, private }) - } - } - - #[derive(Serialize, Deserialize, Debug, Clone)] - #[serde(try_from = "BinaryLoadableKey", into = "BinaryLoadableKey")] - struct LoadableKey { - private: Private, - public: Public, - } - - pub struct TpmConfig { - tcti: TctiNameConf, - private: Private, - public: Public, - } - - // First, setup the primary key we will be using. Remember tpm Primary keys - // are the same provided the *same* public parameters are used and the tpm - // seed hasn't been reset. - fn get_primary_key_public() -> Result { - let object_attributes = ObjectAttributesBuilder::new() - .with_fixed_tpm(true) - .with_fixed_parent(true) - .with_st_clear(false) - .with_sensitive_data_origin(true) - .with_user_with_auth(true) - .with_decrypt(true) - // .with_sign_encrypt(true) - .with_restricted(true) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm primary key attributes"); - })?; - - PublicBuilder::new() - .with_public_algorithm(PublicAlgorithm::SymCipher) - .with_name_hashing_algorithm(HashingAlgorithm::Sha256) - .with_object_attributes(object_attributes) - .with_symmetric_cipher_parameters(SymmetricCipherParameters::new( - SymmetricDefinitionObject::AES_128_CFB, - )) - .with_symmetric_cipher_unique_identifier(Digest::default()) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm primary key public"); - }) - } - - fn new_hmac_public() -> Result { - let object_attributes = ObjectAttributesBuilder::new() - .with_fixed_tpm(true) - .with_fixed_parent(true) - .with_st_clear(false) - .with_sensitive_data_origin(true) - .with_user_with_auth(true) - .with_sign_encrypt(true) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm hmac key attributes"); - })?; - - PublicBuilder::new() - .with_public_algorithm(PublicAlgorithm::KeyedHash) - .with_name_hashing_algorithm(HashingAlgorithm::Sha256) - .with_object_attributes(object_attributes) - .with_keyed_hash_parameters(PublicKeyedHashParameters::new( - KeyedHashScheme::HMAC_SHA_256, - )) - .with_keyed_hash_unique_identifier(Digest::default()) - .build() - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm hmac key public"); - }) - } - - fn setup_keys(ctx: &mut Context, tpm_conf: &TpmConfig) -> Result { - // Given our public and private parts, setup our keys for usage. - let primary_pub = get_primary_key_public().map_err(|_| CryptoError::Tpm2PublicBuilder)?; - trace!(?primary_pub); - let primary_key = ctx - .create_primary(Hierarchy::Owner, primary_pub, None, None, None, None) - .map_err(|e| { - error!(tpm_err = ?e, "Failed to load tpm context"); - >::into(e) - })?; - - /* - let hmac_pub = new_hmac_public() - .map_err(|_| CryptoError::Tpm2PublicBuilder ) - ?; - trace!(?hmac_pub); - */ - - ctx.load( - primary_key.key_handle.clone(), - tpm_conf.private.clone(), - tpm_conf.public.clone(), - ) - .map_err(|e| { - error!(tpm_err = ?e, "Failed to load tpm context"); - >::into(e) - }) - } - - impl Db { - pub fn tpm_setup_context(tcti_str: &str, conn: &Connection) -> Result { - let tcti = TctiNameConf::from_str(tcti_str).map_err(|e| { - error!(tpm_err = ?e, "Failed to parse tcti name"); - })?; - - let mut context = Context::new(tcti.clone()).map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm context"); - })?; - - // Create the primary object that will contain our key. - let primary_pub = get_primary_key_public()?; - let primary_key = context - .execute_with_nullauth_session(|ctx| { - ctx.create_primary(Hierarchy::Owner, primary_pub, None, None, None, None) - }) - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm primary key"); - })?; - - // Now we know we can establish a correct primary key, lets get any previously saved - // hmac keys. - - conn.execute( - "CREATE TABLE IF NOT EXISTS config_t ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) - ", - [], - ) - .map_err(|e| { - error!(sqlite_err = ?e, "update config_t tpm_ctx"); - })?; - - // Try and get the db context. - let ctx_data: Option> = conn - .query_row( - "SELECT value FROM config_t WHERE key='tpm2_ctx'", - [], - |row| row.get(0), - ) - .optional() - .map_err(|e| { - error!(sqlite_err = ?e, "Failed to load tpm2_ctx"); - }) - .unwrap_or(None); - - trace!(ctx_data_present = %ctx_data.is_some()); - - let ex_private = if let Some(ctx_data) = ctx_data { - // Test loading, blank it out if it fails. - serde_json::from_slice(ctx_data.as_slice()) - .map_err(|e| { - warn!("json error -> {:?}", e); - }) - // On error, becomes none and we do nothing else. - .ok() - .and_then(|loadable: LoadableKey| { - // test if it can load? - context - .execute_with_nullauth_session(|ctx| { - ctx.load( - primary_key.key_handle.clone(), - loadable.private.clone(), - loadable.public.clone(), - ) - // We have to flush the handle here to not overfill tpm context memory. - // We'll be reloading the key later anyway. - .and_then(|kh| ctx.flush_context(kh.into())) - }) - .map_err(|e| { - error!(tpm_err = ?e, "Failed to load tpm hmac key"); - }) - .ok() - // It loaded, so sub in our Private data. - .map(|_hmac_handle| loadable) - }) - } else { - None - }; - - let loadable = if let Some(existing) = ex_private { - existing - } else { - // Need to regenerate the private key for some reason - info!("Creating new hmac key"); - let hmac_pub = new_hmac_public()?; - context - .execute_with_nullauth_session(|ctx| { - ctx.create( - primary_key.key_handle.clone(), - hmac_pub, - None, - None, - None, - None, - ) - .map(|key| LoadableKey { - public: key.out_public, - private: key.out_private, - }) - }) - .map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm hmac key"); - })? - }; - - // Serialise it out. - let data = serde_json::to_vec(&loadable).map_err(|e| { - error!("json error -> {:?}", e); - })?; - - // Update the tpm ctx str - conn.execute( - "INSERT OR REPLACE INTO config_t (key, value) VALUES ('tpm2_ctx', :data)", - named_params! { - ":data": &data, - }, - ) - .map_err(|e| { - error!(sqlite_err = ?e, "update config_t tpm_ctx"); - }) - .map(|_| ())?; - - // - - info!("tpm binding configured"); - - let LoadableKey { private, public } = loadable; - - Ok(TpmConfig { - tcti, - private, - public, - }) - } - - pub fn tpm_new( - policy: &CryptoPolicy, - cred: &str, - tpm_conf: &TpmConfig, - ) -> Result { - let mut context = Context::new(tpm_conf.tcti.clone()).map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm context"); - })?; - - context - .execute_with_nullauth_session(|ctx| { - let key = setup_keys(ctx, tpm_conf)?; - Password::new_argon2id_tpm(policy, cred, ctx, key.into()) - }) - .map_err(|e: CryptoError| { - error!(tpm_err = ?e, "Failed to create tpm bound password"); - }) - } - - pub fn tpm_verify(pw: Password, cred: &str, tpm_conf: &TpmConfig) -> Result { - let mut context = Context::new(tpm_conf.tcti.clone()).map_err(|e| { - error!(tpm_err = ?e, "Failed to create tpm context"); - })?; - - context - .execute_with_nullauth_session(|ctx| { - let key = setup_keys(ctx, tpm_conf)?; - pw.verify_ctx(cred, Some((ctx, key.into()))) - }) - .map_err(|e: CryptoError| { - error!(tpm_err = ?e, "Failed to create tpm bound password"); - }) - } - } -} - #[cfg(test)] mod tests { // use std::assert_matches::assert_matches; use super::{Cache, CacheTxn, Db}; use crate::idprovider::interface::{GroupToken, Id, UserToken}; - use crate::unix_config::TpmPolicy; + use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, Tpm}; const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test"; const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test"; @@ -1160,7 +967,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_basic() { sketching::test_init(); - let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1244,7 +1051,7 @@ mod tests { #[tokio::test] async fn test_cache_db_group_basic() { sketching::test_init(); - let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1319,7 +1126,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_group_update() { sketching::test_init(); - let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1388,17 +1195,27 @@ mod tests { async fn test_cache_db_account_password() { sketching::test_init(); - #[cfg(feature = "tpm")] - let tpm_policy = TpmPolicy::Required("device:/dev/tpmrm0".to_string()); - - #[cfg(not(feature = "tpm"))] - let tpm_policy = TpmPolicy::default(); - - let db = Db::new("", &tpm_policy).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); + // Setup the hsm + // #[cfg(feature = "tpm")] + + #[cfg(not(feature = "tpm"))] + let mut hsm: Box = Box::new(SoftTpm::new()); + + let auth_value = AuthValue::new_random().unwrap(); + + let loadable_machine_key = hsm.machine_key_create(&auth_value).unwrap(); + let machine_key = hsm + .machine_key_load(&auth_value, &loadable_machine_key) + .unwrap(); + + let loadable_hmac_key = hsm.hmac_key_create(&machine_key).unwrap(); + let hmac_key = hsm.hmac_key_load(&machine_key, &loadable_hmac_key).unwrap(); + let uuid1 = uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"); let mut ut1 = UserToken { name: "testuser".to_string(), @@ -1414,40 +1231,40 @@ mod tests { // Test that with no account, is false assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), Ok(false) )); // test adding an account dbtxn.update_account(&ut1, 0).unwrap(); // check with no password is false. assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), Ok(false) )); // update the pw assert!(dbtxn - .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) + .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key) .is_ok()); // Check it now works. assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), Ok(true) )); assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), Ok(false) )); // Update the pw assert!(dbtxn - .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) + .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key) .is_ok()); // Check it matches. assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), Ok(false) )); assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), Ok(true) )); @@ -1455,7 +1272,7 @@ mod tests { ut1.displayname = "Test User Update".to_string(); dbtxn.update_account(&ut1, 0).unwrap(); assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B), + dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), Ok(true) )); @@ -1465,7 +1282,7 @@ mod tests { #[tokio::test] async fn test_cache_db_group_rename_duplicate() { sketching::test_init(); - let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1520,7 +1337,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_rename_duplicate() { sketching::test_init(); - let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); + let db = Db::new("").expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); diff --git a/unix_integration/src/idprovider/interface.rs b/unix_integration/src/idprovider/interface.rs index eac32b069..d8b643e0f 100644 --- a/unix_integration/src/idprovider/interface.rs +++ b/unix_integration/src/idprovider/interface.rs @@ -1,8 +1,11 @@ +use crate::db::DbTxn; use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse}; use async_trait::async_trait; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use uuid::Uuid; +pub use kanidm_hsm_crypto::{KeyAlgorithm, MachineKey, Tpm}; + /// Errors that the IdProvider may return. These drive the resolver state machine /// and should be carefully selected to match your expected errors. #[derive(Debug)] @@ -21,6 +24,8 @@ pub enum IdpError { /// The idp has indicated that the requested resource does not exist and should /// be considered deleted, removed, or not present. NotFound, + /// The idp was unable to perform an operation on the underlying hsm keystorage + KeyStore, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -86,8 +91,52 @@ pub enum AuthCacheAction { PasswordHashUpdate { cred: String }, } +pub struct KeyStore<'a> { + dbtxn: &'a DbTxn<'a>, +} + +impl<'a> KeyStore<'a> { + pub(crate) fn new(dbtxn: &'a DbTxn<'a>) -> Self { + KeyStore { dbtxn } + } + + pub fn get_tagged_hsm_key( + &mut self, + tag: &str, + ) -> Result, IdpError> { + self.dbtxn + .get_tagged_hsm_key(tag) + .map_err(|_err| IdpError::KeyStore) + } + + pub fn insert_tagged_hsm_key( + &mut self, + tag: &str, + key: &K, + ) -> Result<(), IdpError> { + self.dbtxn + .insert_tagged_hsm_key(tag, key) + .map_err(|_err| IdpError::KeyStore) + } + + pub fn delete_tagged_hsm_key(&mut self, tag: &str) -> Result<(), IdpError> { + self.dbtxn + .delete_tagged_hsm_key(tag) + .map_err(|_err| IdpError::KeyStore) + } +} + #[async_trait] pub trait IdProvider { + async fn configure_hsm_keys( + &self, + _keystore: &mut KeyStore, + _tpm: &mut (dyn Tpm + Send), + _machine_key: &MachineKey, + ) -> Result<(), IdpError> { + Ok(()) + } + async fn provider_authenticate(&self) -> Result<(), IdpError>; async fn unix_user_get( diff --git a/unix_integration/src/resolver.rs b/unix_integration/src/resolver.rs index bdc572cf1..3a60f91c1 100644 --- a/unix_integration/src/resolver.rs +++ b/unix_integration/src/resolver.rs @@ -2,7 +2,7 @@ use hashbrown::HashSet; use std::collections::BTreeSet; use std::num::NonZeroUsize; -use std::ops::{Add, Sub}; +use std::ops::{Add, DerefMut, Sub}; use std::path::Path; use std::string::ToString; use std::time::{Duration, SystemTime}; @@ -13,12 +13,13 @@ use uuid::Uuid; use crate::db::{Cache, CacheTxn, Db}; use crate::idprovider::interface::{ - AuthCacheAction, AuthCredHandler, AuthResult, GroupToken, Id, IdProvider, IdpError, UserToken, + AuthCacheAction, AuthCredHandler, AuthResult, GroupToken, Id, IdProvider, IdpError, KeyStore, + UserToken, }; use crate::unix_config::{HomeAttr, UidAttr}; use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse}; -// use crate::unix_passwd::{EtcUser, EtcGroup}; +use kanidm_hsm_crypto::{HmacKey, MachineKey, Tpm}; const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(128) }; @@ -44,13 +45,15 @@ pub enum AuthSession { Denied, } -#[derive(Debug)] pub struct Resolver where I: IdProvider + Sync, { // Generic / modular types. db: Db, + hsm: Mutex>, + // machine_key: MachineKey, + hmac_key: HmacKey, client: I, // Types to update still. state: Mutex, @@ -84,6 +87,8 @@ where pub async fn new( db: Db, client: I, + hsm: Box, + machine_key: MachineKey, // cache timeout timeout_seconds: u64, pam_allow_groups: Vec, @@ -95,21 +100,71 @@ where gid_attr_map: UidAttr, allow_id_overrides: Vec, ) -> Result { + let hsm = Mutex::new(hsm); + let mut hsm_lock = hsm.lock().await; + // setup and do a migrate. - { - let dbtxn = db.write().await; - dbtxn.migrate().map_err(|_| ())?; - dbtxn.commit().map_err(|_| ())?; - } + let dbtxn = db.write().await; + dbtxn.migrate().map_err(|_| ())?; + dbtxn.commit().map_err(|_| ())?; + + // Setup our internal keys + let dbtxn = db.write().await; + + let loadable_hmac_key = match dbtxn.get_hsm_hmac_key() { + Ok(Some(hmk)) => hmk, + Ok(None) => { + // generate a new key. + let loadable_hmac_key = hsm_lock.hmac_key_create(&machine_key).map_err(|err| { + error!(?err, "Unable to create hmac key"); + })?; + + dbtxn + .insert_hsm_hmac_key(&loadable_hmac_key) + .map_err(|err| { + error!(?err, "Unable to persist hmac key"); + })?; + + loadable_hmac_key + } + Err(err) => { + error!(?err, "Unable to retrieve loadable hmac key from db"); + return Err(()); + } + }; + + let hmac_key = hsm_lock + .hmac_key_load(&machine_key, &loadable_hmac_key) + .map_err(|err| { + error!(?err, "Unable to load hmac key"); + })?; + + // Ask the client what keys it wants the HSM to configure. + // make a key store + let mut ks = KeyStore::new(&dbtxn); + + client + .configure_hsm_keys(&mut ks, &mut **hsm_lock.deref_mut(), &machine_key) + .await + .map_err(|err| { + error!(?err, "Client was unable to configure hsm keys"); + })?; + + drop(hsm_lock); + + dbtxn.commit().map_err(|_| ())?; if pam_allow_groups.is_empty() { - eprintln!("Will not be able to authorise user logins, pam_allow_groups config is not configured."); + warn!("Will not be able to authorise user logins, pam_allow_groups config is not configured."); } // We assume we are offline at start up, and we mark the next "online check" as // being valid from "now". Ok(Resolver { db, + hsm, + // machine_key, + hmac_key, client, state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())), timeout_seconds, @@ -391,16 +446,18 @@ where async fn set_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result<(), ()> { let dbtxn = self.db.write().await; + let mut hsm_txn = self.hsm.lock().await; dbtxn - .update_account_password(a_uuid, cred) + .update_account_password(a_uuid, cred, &mut **hsm_txn, &self.hmac_key) .and_then(|x| dbtxn.commit().map(|_| x)) .map_err(|_| ()) } async fn check_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result { let dbtxn = self.db.write().await; + let mut hsm_txn = self.hsm.lock().await; dbtxn - .check_account_password(a_uuid, cred) + .check_account_password(a_uuid, cred, &mut **hsm_txn, &self.hmac_key) .and_then(|x| dbtxn.commit().map(|_| x)) .map_err(|_| ()) } @@ -448,7 +505,7 @@ where Ok(None) } - Err(IdpError::BadRequest) => { + Err(IdpError::KeyStore) | Err(IdpError::BadRequest) => { // Some other transient error, continue with the token. Ok(token) } @@ -495,7 +552,7 @@ where self.set_nxcache(grp_id).await; Ok(None) } - Err(IdpError::BadRequest) => { + Err(IdpError::KeyStore) | Err(IdpError::BadRequest) => { // Some other transient error, continue with the token. Ok(token) } @@ -746,132 +803,6 @@ where self.get_nssgroup(Id::Gid(gid)).await } - /* - async fn online_account_authenticate( - &self, - token: &Option, - account_id: &Id, - cred: Option, - data: Option, - ) -> Result { - debug!("Attempt online password check"); - // Unwrap the cred for passing to the step function - let ucred = match &cred { - Some(PamCred::Password(v)) => Some(v.clone()), - Some(PamCred::MFACode(v)) => Some(v.clone()), - None => None, - }; - // We are online, attempt the pw to the server. - match self - .client - .unix_user_authenticate_step(account_id, ucred.as_deref(), data) - .await - { - Ok(ProviderResult::UserToken(Some(mut n_tok))) => { - if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { - // Refuse to release the token, it's in the denied set. - self.delete_cache_usertoken(n_tok.uuid).await?; - Ok(ClientResponse::PamStatus(None)) - } else { - debug!("online password check success."); - self.set_cache_usertoken(&mut n_tok).await?; - match cred { - Some(PamCred::Password(cred)) => { - // Only cache an actual password (not an MFA token) - self.set_cache_userpassword(n_tok.uuid, &cred).await?; - } - Some(_) => {} - None => {} - } - Ok(ClientResponse::PamStatus(Some(true))) - } - } - Ok(ProviderResult::UserToken(None)) => { - error!("incorrect password"); - // PW failed the check. - Ok(ClientResponse::PamStatus(Some(false))) - } - Ok(ProviderResult::PamPrompt(prompt)) => { - debug!("Requesting pam prompt {:?}", prompt); - Ok(ClientResponse::PamPrompt(prompt)) - } - Err(IdpError::Transport) => { - error!("transport error, moving to offline"); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - match token.as_ref() { - Some(t) => match ucred { - Some(cred) => match self.check_cache_userpassword(t.uuid, &cred).await { - Ok(res) => Ok(ClientResponse::PamStatus(Some(res))), - Err(e) => Err(e), - }, - None => Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())), - }, - None => Ok(ClientResponse::PamStatus(None)), - } - } - Err(IdpError::ProviderUnauthorised) => { - // Something went wrong, mark offline to force a re-auth ASAP. - let time = SystemTime::now().sub(Duration::from_secs(1)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - match token.as_ref() { - Some(t) => match ucred { - Some(cred) => match self.check_cache_userpassword(t.uuid, &cred).await { - Ok(res) => Ok(ClientResponse::PamStatus(Some(res))), - Err(e) => Err(e), - }, - None => Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())), - }, - None => Ok(ClientResponse::PamStatus(None)), - } - } - Err(IdpError::NotFound) => Ok(ClientResponse::PamStatus(None)), - Err(IdpError::BadRequest) => { - // Some other unknown processing error? - Err(()) - } - } - } - - async fn offline_account_authenticate( - &self, - token: &Option, - cred: Option, - ) -> Result { - let cred = match cred { - Some(cred) => match cred { - PamCred::Password(v) => v.clone(), - // We can only authenticate using a password - _ => return Ok(ClientResponse::PamStatus(Some(false))), - }, - None => return Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())), - }; - debug!("Attempt offline password check"); - match token.as_ref() { - Some(t) => { - if t.valid { - match self.check_cache_userpassword(t.uuid, &cred).await { - Ok(res) => Ok(ClientResponse::PamStatus(Some(res))), - Err(e) => Err(e), - } - } else { - Ok(ClientResponse::PamStatus(Some(false))) - } - } - None => Ok(ClientResponse::PamStatus(None)), - } - /* - token - .as_ref() - .map(async |t| self.check_cache_userpassword(&t.uuid, cred).await) - .transpose() - */ - } - */ - pub async fn pam_account_allowed(&self, account_id: &str) -> Result, ()> { let token = self.get_usertoken(Id::Name(account_id.to_string())).await?; @@ -956,7 +887,7 @@ where .await; Err(()) } - Err(IdpError::BadRequest) => Err(()), + Err(IdpError::BadRequest) | Err(IdpError::KeyStore) => Err(()), } } @@ -1109,7 +1040,7 @@ where .await; Err(()) } - Err(IdpError::BadRequest) => Err(()), + Err(IdpError::KeyStore) | Err(IdpError::BadRequest) => Err(()), } } diff --git a/unix_integration/src/unix_config.rs b/unix_integration/src/unix_config.rs index a1530ed8e..f3dbb1499 100644 --- a/unix_integration/src/unix_config.rs +++ b/unix_integration/src/unix_config.rs @@ -10,12 +10,7 @@ use crate::unix_passwd::UnixIntegrationError; use serde::Deserialize; -use crate::constants::{ - DEFAULT_CACHE_TIMEOUT, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_GID_ATTR_MAP, - DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SELINUX, DEFAULT_SHELL, - DEFAULT_SOCK_PATH, DEFAULT_TASK_SOCK_PATH, DEFAULT_TPM_TCTI_NAME, DEFAULT_UID_ATTR_MAP, - DEFAULT_USE_ETC_SKEL, -}; +use crate::constants::*; #[derive(Debug, Deserialize)] struct ConfigInt { @@ -35,8 +30,10 @@ struct ConfigInt { selinux: Option, #[serde(default)] allow_local_account_override: Vec, + + hsm_pin_path: Option, + hsm_type: Option, tpm_tcti_name: Option, - tpm_policy: Option, } #[derive(Debug, Copy, Clone)] @@ -80,23 +77,18 @@ impl Display for UidAttr { } #[derive(Debug, Clone, Default)] -pub enum TpmPolicy { - #[default] - Ignore, - IfPossible(String), - Required(String), +pub enum HsmType { + #[cfg_attr(not(feature = "tpm"), default)] + Soft, + #[cfg_attr(feature = "tpm", default)] + Tpm, } -impl Display for TpmPolicy { +impl Display for HsmType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - TpmPolicy::Ignore => write!(f, "Ignore"), - TpmPolicy::IfPossible(p) => { - write!(f, "IfPossible ({})", p) - } - TpmPolicy::Required(p) => { - write!(f, "Required ({})", p) - } + HsmType::Soft => write!(f, "Soft"), + HsmType::Tpm => write!(f, "Tpm"), } } } @@ -118,7 +110,9 @@ pub struct KanidmUnixdConfig { pub uid_attr_map: UidAttr, pub gid_attr_map: UidAttr, pub selinux: bool, - pub tpm_policy: TpmPolicy, + pub hsm_type: HsmType, + pub hsm_pin_path: String, + pub tpm_tcti_name: String, pub allow_local_account_override: Vec, } @@ -152,8 +146,10 @@ impl Display for KanidmUnixdConfig { writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?; writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?; + writeln!(f, "hsm_type: {}", self.hsm_type)?; + writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?; + writeln!(f, "selinux: {}", self.selinux)?; - writeln!(f, "tpm_policy: {}", self.tpm_policy)?; writeln!( f, "allow_local_account_override: {:#?}", @@ -168,6 +164,11 @@ impl KanidmUnixdConfig { Ok(val) => val, Err(_) => DEFAULT_DB_PATH.into(), }; + let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") { + Ok(val) => val, + Err(_) => DEFAULT_HSM_PIN_PATH.into(), + }; + KanidmUnixdConfig { db_path, sock_path: DEFAULT_SOCK_PATH.to_string(), @@ -184,7 +185,9 @@ impl KanidmUnixdConfig { uid_attr_map: DEFAULT_UID_ATTR_MAP, gid_attr_map: DEFAULT_GID_ATTR_MAP, selinux: DEFAULT_SELINUX, - tpm_policy: TpmPolicy::default(), + hsm_pin_path, + hsm_type: HsmType::default(), + tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(), allow_local_account_override: Vec::default(), } } @@ -301,23 +304,21 @@ impl KanidmUnixdConfig { true => selinux_util::supported(), _ => false, }, - tpm_policy: config - .tpm_policy - .and_then(|v| { - let tpm_tcti_name = config - .tpm_tcti_name - .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()); - match v.as_str() { - "ignore" => Some(TpmPolicy::Ignore), - "if_possible" => Some(TpmPolicy::IfPossible(tpm_tcti_name)), - "required" => Some(TpmPolicy::Required(tpm_tcti_name)), - _ => { - warn!("Invalid tpm_policy configured, using default ..."); - None - } + hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path), + hsm_type: config + .hsm_type + .and_then(|v| match v.as_str() { + "soft" => Some(HsmType::Soft), + "tpm" => Some(HsmType::Tpm), + _ => { + warn!("Invalid hsm_type configured, using default ..."); + None } }) - .unwrap_or(self.tpm_policy), + .unwrap_or(self.hsm_type), + tpm_tcti_name: config + .tpm_tcti_name + .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), allow_local_account_override: config.allow_local_account_override, }) } diff --git a/unix_integration/tests/cache_layer_test.rs b/unix_integration/tests/cache_layer_test.rs index a79b1105b..68debf075 100644 --- a/unix_integration/tests/cache_layer_test.rs +++ b/unix_integration/tests/cache_layer_test.rs @@ -14,13 +14,14 @@ use kanidm_unix_common::db::Db; use kanidm_unix_common::idprovider::interface::Id; use kanidm_unix_common::idprovider::kanidm::KanidmProvider; use kanidm_unix_common::resolver::Resolver; -use kanidm_unix_common::unix_config::TpmPolicy; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::create_server_core; use kanidmd_testkit::{is_free_port, PORT_ALLOC}; use tokio::task; use tracing::log::{debug, trace}; +use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, Tpm}; + const ADMIN_TEST_USER: &str = "admin"; const ADMIN_TEST_PASSWORD: &str = "integration test admin password"; const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test"; @@ -99,13 +100,23 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) let db = Db::new( "", // The sqlite db path, this is in memory. - &TpmPolicy::default(), ) .expect("Failed to setup DB"); + let mut hsm: Box = Box::new(SoftTpm::new()); + + let auth_value = AuthValue::new_random().unwrap(); + + let loadable_machine_key = hsm.machine_key_create(&auth_value).unwrap(); + let machine_key = hsm + .machine_key_load(&auth_value, &loadable_machine_key) + .unwrap(); + let cachelayer = Resolver::new( db, idprovider, + hsm, + machine_key, 300, vec!["allowed_group".to_string()], DEFAULT_SHELL.to_string(),