diff --git a/Cargo.lock b/Cargo.lock index bd91a801f..83623ce6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + [[package]] name = "bitflags" version = "1.3.2" @@ -1430,6 +1436,26 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "erased-serde" version = "0.3.25" @@ -2110,6 +2136,12 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + [[package]] name = "http" version = "0.2.9" @@ -2484,6 +2516,7 @@ dependencies = [ "serde", "sketching", "tracing", + "tss-esapi", ] [[package]] @@ -2573,6 +2606,7 @@ dependencies = [ "tokio-util", "toml", "tracing", + "tss-esapi", "users", "walkdir", ] @@ -2988,6 +3022,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "mbox" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f88d5c34d63aad11aa4321ef55ccb064af58b3ad8091079ae22bf83e5eb75d6" +dependencies = [ + "libc", + "rustc_version 0.3.3", + "stable_deref_trait", +] + [[package]] name = "memchr" version = "2.5.0" @@ -3145,6 +3190,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -3245,6 +3301,15 @@ dependencies = [ "url", ] +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + [[package]] name = "oid-registry" version = "0.4.0" @@ -3475,6 +3540,51 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pest" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16833386b02953ca926d19f64af613b9bf742c48dcd5e09b32fbfc9740bf84e2" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "picky-asn1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889bbb26c80acf919e89980dfc8e04eb19df272d8a9893ec9b748d3a1675abde" +dependencies = [ + "oid", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-der" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbbd5390ab967396cc7473e6e0848684aec7166e657c6088604e07b54a73dbe" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3033675030de806aba1d5470949701b7c9f1dbf77e3bb17bd12e5f945e560ba" +dependencies = [ + "base64 0.13.1", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", +] + [[package]] name = "pin-project" version = "1.1.0" @@ -4024,7 +4134,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", ] [[package]] @@ -4173,7 +4292,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser", + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", ] [[package]] @@ -4182,6 +4310,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.164" @@ -4481,6 +4618,12 @@ dependencies = [ "sha2 0.8.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "standback" version = "0.2.17" @@ -4503,7 +4646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" dependencies = [ "discard", - "rustc_version", + "rustc_version 0.2.3", "stdweb-derive", "stdweb-internal-macros", "stdweb-internal-runtime", @@ -4659,6 +4802,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + [[package]] name = "tempfile" version = "3.6.0" @@ -5100,12 +5249,50 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tss-esapi" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891582e26e83f2cbd608b18cbd7ffb921482740524187a2bca20cf44a286547b" +dependencies = [ + "bitfield", + "enumflags2", + "hostname-validator", + "log", + "mbox", + "num-derive", + "num-traits", + "oid", + "picky-asn1", + "picky-asn1-x509", + "regex", + "serde", + "tss-esapi-sys", + "zeroize", +] + +[[package]] +name = "tss-esapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b8be553262e0924410fe96404830252477f175f228081f21cb0bd87f2ccebe" +dependencies = [ + "pkg-config", + "target-lexicon", +] + [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index 4bdf36034..53086f478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,8 @@ tracing-subscriber = { version = "^0.3.17", features = ["env-filter"] } # tracing-forest = { path = "/Users/william/development/tracing-forest/tracing-forest" } tracing-forest = { git = "https://github.com/QnnOkabayashi/tracing-forest.git", rev = "77daf8c8abf010b87d45ece2bf656983c6f8cecb" } +tss-esapi = "^7.2.0" + url = "^2.4.0" urlencoding = "2.1.2" users = "^0.11.0" diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml index 112ea38ed..e88191046 100644 --- a/libs/crypto/Cargo.toml +++ b/libs/crypto/Cargo.toml @@ -3,6 +3,9 @@ name = "kanidm_lib_crypto" version = "0.1.0" edition = "2021" +[features] +tpm = ["dep:tss-esapi"] + [dependencies] argon2.workspace = true base64.workspace = true @@ -17,6 +20,7 @@ openssl.workspace = true rand.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true +tss-esapi = { workspace = true, optional = true } [dev-dependencies] sketching.workspace = true diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index e3a46072e..842489e57 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -28,6 +28,13 @@ use openssl::nid::Nid; use openssl::pkcs5::pbkdf2_hmac; use openssl::sha::Sha512; +#[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; @@ -48,9 +55,43 @@ const ARGON2_SALT_LEN: usize = 24; const ARGON2_KEY_LEN: usize = 32; const ARGON2_MAX_RAM_KIB: u32 = 32 * 1024; +#[derive(Clone, Debug)] +pub enum CryptoError { + Tpm2, + Tpm2FeatureMissing, + Tpm2InputExceeded, + Tpm2ContextMissing, + OpenSSL, + Md4Disabled, + Argon2, + Argon2Version, + Argon2Parameters, +} + +impl Into for CryptoError { + fn into(self) -> OperationError { + OperationError::CryptographyError + } +} + +#[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 { + TPM_ARGON2ID { + m: u32, + t: u32, + p: u32, + v: u32, + s: Base64UrlSafeData, + k: Base64UrlSafeData, + }, ARGON2ID { m: u32, t: u32, @@ -69,6 +110,14 @@ pub enum DbPasswordV1 { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[allow(non_camel_case_types)] pub enum ReplPasswordV1 { + TPM_ARGON2ID { + m_cost: u32, + t_cost: u32, + p_cost: u32, + version: u32, + salt: Base64UrlSafeData, + key: Base64UrlSafeData, + }, ARGON2ID { m_cost: u32, t_cost: u32, @@ -104,6 +153,7 @@ pub enum ReplPasswordV1 { impl fmt::Debug for DbPasswordV1 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + DbPasswordV1::TPM_ARGON2ID { .. } => write!(f, "TPM_ARGON2ID"), DbPasswordV1::ARGON2ID { .. } => write!(f, "ARGON2ID"), DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"), DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"), @@ -239,6 +289,14 @@ impl CryptoPolicy { #[derive(Clone, Debug, PartialEq)] #[allow(non_camel_case_types)] enum Kdf { + TPM_ARGON2ID { + m_cost: u32, + t_cost: u32, + p_cost: u32, + version: u32, + salt: Vec, + key: Vec, + }, // ARGON2ID { m_cost: u32, @@ -272,6 +330,16 @@ impl TryFrom for Password { fn try_from(value: DbPasswordV1) -> Result { match value { + DbPasswordV1::TPM_ARGON2ID { m, t, p, v, s, k } => Ok(Password { + material: Kdf::TPM_ARGON2ID { + m_cost: m, + t_cost: t, + p_cost: p, + version: v, + salt: s.into(), + key: k.into(), + }, + }), DbPasswordV1::ARGON2ID { m, t, p, v, s, k } => Ok(Password { material: Kdf::ARGON2ID { m_cost: m, @@ -306,6 +374,23 @@ impl TryFrom<&ReplPasswordV1> for Password { fn try_from(value: &ReplPasswordV1) -> Result { match value { + ReplPasswordV1::TPM_ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + } => Ok(Password { + material: Kdf::TPM_ARGON2ID { + m_cost: *m_cost, + t_cost: *t_cost, + p_cost: *p_cost, + version: *version, + salt: salt.0.clone(), + key: key.0.clone(), + }, + }), ReplPasswordV1::ARGON2ID { m_cost, t_cost, @@ -624,7 +709,7 @@ impl Password { end.checked_duration_since(start) } - pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result { + pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result { let pbkdf2_cost = policy.pbkdf2_cost; let mut rng = rand::thread_rng(); let salt: Vec = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect(); @@ -642,11 +727,11 @@ impl Password { // Turn key to a vec. Kdf::PBKDF2(pbkdf2_cost, salt, key) }) - .map_err(|_| OperationError::CryptographyError) + .map_err(|_| CryptoError::OpenSSL) .map(|material| Password { material }) } - pub fn new_argon2id(policy: &CryptoPolicy, cleartext: &str) -> Result { + pub fn new_argon2id(policy: &CryptoPolicy, cleartext: &str) -> Result { let version = Version::V0x13; let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone()); @@ -665,28 +750,72 @@ impl Password { salt, key, }) - .map_err(|_| OperationError::CryptographyError) + .map_err(|_| CryptoError::Argon2) + .map(|material| Password { material }) + } + + pub fn new_argon2id_tpm( + policy: &CryptoPolicy, + cleartext: &str, + tpm_ctx: &mut TpmContext, + tpm_key_handle: TpmHandle, + ) -> Result { + let version = Version::V0x13; + + let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone()); + + let mut rng = rand::thread_rng(); + let salt: Vec = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect(); + let mut check_key: Vec = (0..ARGON2_KEY_LEN).map(|_| 0).collect(); + + argon + .hash_password_into( + cleartext.as_bytes(), + salt.as_slice(), + check_key.as_mut_slice(), + ) + .map_err(|_| CryptoError::Argon2) + .and_then(|()| do_tpm_hmac(check_key, tpm_ctx, tpm_key_handle)) + .map(|key| Kdf::TPM_ARGON2ID { + m_cost: policy.argon2id_params.m_cost(), + t_cost: policy.argon2id_params.t_cost(), + p_cost: policy.argon2id_params.p_cost(), + version: version as u32, + salt, + key, + }) .map(|material| Password { material }) } #[inline] - pub fn new(policy: &CryptoPolicy, cleartext: &str) -> Result { + pub fn new(policy: &CryptoPolicy, cleartext: &str) -> Result { Self::new_pbkdf2(policy, cleartext) } - pub fn verify(&self, cleartext: &str) -> Result { - match &self.material { - Kdf::ARGON2ID { - m_cost, - t_cost, - p_cost, - version, - salt, - key, - } => { + pub fn verify(&self, cleartext: &str) -> Result { + self.verify_ctx(cleartext, None) + } + + pub fn verify_ctx<'a>( + &self, + cleartext: &str, + tpm: Option<(&'a mut TpmContext, TpmHandle)>, + ) -> Result { + match (&self.material, tpm) { + ( + Kdf::TPM_ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + }, + Some((tpm_ctx, tpm_key_handle)), + ) => { let version: Version = (*version).try_into().map_err(|_| { error!("Failed to convert {} to valid argon2id version", version); - OperationError::CryptographyError + CryptoError::Argon2Version })?; let key_len = key.len(); @@ -694,7 +823,7 @@ impl Password { let params = Params::new(*m_cost, *t_cost, *p_cost, Some(key_len)).map_err(|e| { error!(err = ?e, "invalid argon2id parameters"); - OperationError::CryptographyError + CryptoError::Argon2Parameters })?; let argon = Argon2::new(Algorithm::Argon2id, version, params); @@ -708,14 +837,61 @@ impl Password { ) .map_err(|e| { error!(err = ?e, "unable to perform argon2id hash"); - OperationError::CryptographyError + CryptoError::Argon2 + }) + .and_then(|()| do_tpm_hmac(check_key, tpm_ctx, tpm_key_handle)) + .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) + } + ( + Kdf::ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + }, + _, + ) => { + let version: Version = (*version).try_into().map_err(|_| { + error!("Failed to convert {} to valid argon2id version", version); + CryptoError::Argon2Version + })?; + + let key_len = key.len(); + + let params = + Params::new(*m_cost, *t_cost, *p_cost, Some(key_len)).map_err(|e| { + error!(err = ?e, "invalid argon2id parameters"); + CryptoError::Argon2Parameters + })?; + + let argon = Argon2::new(Algorithm::Argon2id, version, params); + let mut check_key: Vec = (0..key_len).map(|_| 0).collect(); + + argon + .hash_password_into( + cleartext.as_bytes(), + salt.as_slice(), + check_key.as_mut_slice(), + ) + .map_err(|e| { + error!(err = ?e, "unable to perform argon2id hash"); + CryptoError::Argon2 }) .map(|()| { // Actually compare the outputs. &check_key == key }) } - Kdf::PBKDF2(cost, salt, key) => { + (Kdf::PBKDF2(cost, salt, key), _) => { // We have to get the number of bits to derive from our stored hash // as some imported hash types may have variable lengths let key_len = key.len(); @@ -728,13 +904,13 @@ impl Password { MessageDigest::sha256(), chal_key.as_mut_slice(), ) - .map_err(|_| OperationError::CryptographyError) + .map_err(|_| CryptoError::OpenSSL) .map(|()| { // Actually compare the outputs. &chal_key == key }) } - Kdf::PBKDF2_SHA1(cost, salt, key) => { + (Kdf::PBKDF2_SHA1(cost, salt, key), _) => { let key_len = key.len(); debug_assert!(key_len >= PBKDF2_SHA1_MIN_KEY_LEN); let mut chal_key: Vec = (0..key_len).map(|_| 0).collect(); @@ -745,13 +921,13 @@ impl Password { MessageDigest::sha1(), chal_key.as_mut_slice(), ) - .map_err(|_| OperationError::CryptographyError) + .map_err(|_| CryptoError::OpenSSL) .map(|()| { // Actually compare the outputs. &chal_key == key }) } - Kdf::PBKDF2_SHA512(cost, salt, key) => { + (Kdf::PBKDF2_SHA512(cost, salt, key), _) => { let key_len = key.len(); debug_assert!(key_len >= PBKDF2_MIN_NIST_KEY_LEN); let mut chal_key: Vec = (0..key_len).map(|_| 0).collect(); @@ -762,20 +938,20 @@ impl Password { MessageDigest::sha512(), chal_key.as_mut_slice(), ) - .map_err(|_| OperationError::CryptographyError) + .map_err(|_| CryptoError::OpenSSL) .map(|()| { // Actually compare the outputs. &chal_key == key }) } - Kdf::SSHA512(salt, key) => { + (Kdf::SSHA512(salt, key), _) => { let mut hasher = Sha512::new(); hasher.update(cleartext.as_bytes()); hasher.update(salt); let r = hasher.finish(); Ok(key == &(r.to_vec())) } - Kdf::NT_MD4(key) => { + (Kdf::NT_MD4(key), _) => { // We need to get the cleartext to utf16le for reasons. let clear_utf16le: Vec = cleartext .encode_utf16() @@ -786,7 +962,7 @@ impl Password { let dgst = MessageDigest::from_nid(Nid::MD4).ok_or_else(|| { error!("Unable to access MD4 - fips mode may be enabled, or you may need to activate the legacy provider."); error!("For more details, see https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers"); - OperationError::CryptographyError + CryptoError::Md4Disabled })?; hash::hash(dgst, &clear_utf16le) @@ -794,7 +970,7 @@ impl Password { debug!(?e); error!("Unable to digest MD4 - fips mode may be enabled, or you may need to activate the legacy provider."); error!("For more details, see https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers"); - OperationError::CryptographyError + CryptoError::Md4Disabled }) .map(|chal_key| chal_key.as_ref() == key) } @@ -803,6 +979,21 @@ impl Password { pub fn to_dbpasswordv1(&self) -> DbPasswordV1 { match &self.material { + Kdf::TPM_ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + } => DbPasswordV1::TPM_ARGON2ID { + m: *m_cost, + t: *t_cost, + p: *p_cost, + v: *version, + s: salt.clone().into(), + k: key.clone().into(), + }, Kdf::ARGON2ID { m_cost, t_cost, @@ -834,6 +1025,21 @@ impl Password { pub fn to_repl_v1(&self) -> ReplPasswordV1 { match &self.material { + Kdf::TPM_ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + } => ReplPasswordV1::TPM_ARGON2ID { + m_cost: *m_cost, + t_cost: *t_cost, + p_cost: *p_cost, + version: *version, + salt: salt.clone().into(), + key: key.clone().into(), + }, Kdf::ARGON2ID { m_cost, t_cost, @@ -876,6 +1082,7 @@ impl Password { pub fn requires_upgrade(&self) -> bool { match &self.material { + Kdf::TPM_ARGON2ID { .. } => false, Kdf::ARGON2ID { .. } => false, Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => { *cost < PBKDF2_MIN_NIST_COST @@ -885,6 +1092,81 @@ impl Password { Kdf::PBKDF2_SHA1(_, _, _) | Kdf::SSHA512(_, _) | Kdf::NT_MD4(_) => true, } } + + #[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}, + }; + + 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::default()) + .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 + }) + } +} + +#[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"))] +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)] @@ -1056,4 +1338,76 @@ mod tests { } } } + + #[cfg(feature = "tpm")] + #[test] + fn test_password_argon2id_tpm_bind() { + use std::str::FromStr; + + sketching::test_init(); + + use tss_esapi::{Context, TctiNameConf}; + + let mut context = + Context::new(TctiNameConf::from_str("device:/dev/tpmrm0").expect("Failed to get TCTI")) + .expect("Failed to create Context"); + + let key = context + .execute_with_nullauth_session(|ctx| Password::prepare_tpm_key(ctx)) + .unwrap(); + + let p = CryptoPolicy::minimum(); + let c = context + .execute_with_nullauth_session(|ctx| { + Password::new_argon2id_tpm(&p, "password", ctx, key) + }) + .unwrap(); + + assert!(matches!( + c.verify("password"), + Err(CryptoError::Tpm2ContextMissing) + )); + + // Assert it fails without the hmac + let dup = match &c.material { + Kdf::TPM_ARGON2ID { + m_cost, + t_cost, + p_cost, + version, + salt, + key, + } => Password { + material: Kdf::ARGON2ID { + m_cost: *m_cost, + t_cost: *t_cost, + p_cost: *p_cost, + version: *version, + salt: salt.clone(), + key: key.clone(), + }, + }, + _ => unreachable!(), + }; + + 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(); + } } diff --git a/platform/opensuse/kanidm-unixd.service b/platform/opensuse/kanidm-unixd.service index 33ae50fea..4e591f899 100644 --- a/platform/opensuse/kanidm-unixd.service +++ b/platform/opensuse/kanidm-unixd.service @@ -8,6 +8,7 @@ After=chronyd.service ntpd.service network-online.target [Service] DynamicUser=yes +SupplementaryGroups=tss UMask=0027 CacheDirectory=kanidm-unixd RuntimeDirectory=kanidm-unixd @@ -23,7 +24,8 @@ ExecStart=/usr/sbin/kanidm_unixd # SystemCallFilter=@aio @basic-io @chown @file-system @io-event @network-io @sync NoNewPrivileges=true PrivateTmp=true -PrivateDevices=true +# We have to disable this to allow tpmrm0 access for tpm binding. +PrivateDevices=false ProtectHostname=true ProtectClock=true ProtectKernelTunables=true diff --git a/server/lib/src/credential/mod.rs b/server/lib/src/credential/mod.rs index 4ad09566d..8b57ff5c3 100644 --- a/server/lib/src/credential/mod.rs +++ b/server/lib/src/credential/mod.rs @@ -460,7 +460,12 @@ impl Credential { policy: &CryptoPolicy, cleartext: &str, ) -> Result { - Password::new(policy, cleartext).map(Self::new_from_password) + Password::new(policy, cleartext) + .map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + .map(Self::new_from_password) } /// Create a new credential that contains a CredentialType::GeneratedPassword @@ -468,7 +473,12 @@ impl Credential { policy: &CryptoPolicy, cleartext: &str, ) -> Result { - Password::new(policy, cleartext).map(Self::new_from_generatedpassword) + Password::new(policy, cleartext) + .map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + .map(Self::new_from_generatedpassword) } /// Update the state of the Password on this credential, if a password is present. If possible @@ -478,7 +488,12 @@ impl Credential { policy: &CryptoPolicy, cleartext: &str, ) -> Result { - Password::new(policy, cleartext).map(|pw| self.update_password(pw)) + Password::new(policy, cleartext) + .map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + .map(|pw| self.update_password(pw)) } /// Extend this credential with another alternate webauthn credential. This is especially @@ -634,7 +649,12 @@ impl Credential { #[cfg(test)] pub fn verify_password(&self, cleartext: &str) -> Result { - self.password_ref().and_then(|pw| pw.verify(cleartext)) + self.password_ref().and_then(|pw| { + pw.verify(cleartext).map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + }) } /// Extract this credential into it's Serialisable Database form, ready for persistence. diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index 1724efd55..d85040753 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -458,7 +458,14 @@ impl Account { self.primary .as_ref() .ok_or(OperationError::InvalidState) - .and_then(|cred| cred.password_ref().and_then(|pw| pw.verify(cleartext))) + .and_then(|cred| { + cred.password_ref().and_then(|pw| { + pw.verify(cleartext).map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + }) + }) } pub(crate) fn regenerate_radius_secret_mod( diff --git a/server/lib/src/idm/unix.rs b/server/lib/src/idm/unix.rs index f59af144c..b89be96e4 100644 --- a/server/lib/src/idm/unix.rs +++ b/server/lib/src/idm/unix.rs @@ -234,7 +234,10 @@ impl UnixUserAccount { match &self.cred { Some(cred) => { cred.password_ref().and_then(|pw| { - if pw.verify(cleartext)? { + if pw.verify(cleartext).map_err(|e| { + error!(crypto_err = ?e); + e.into() + })? { security_info!("Successful unix cred handling"); if pw.requires_upgrade() { async_tx @@ -270,7 +273,12 @@ impl UnixUserAccount { pub(crate) fn check_existing_pw(&self, cleartext: &str) -> Result { match &self.cred { - Some(cred) => cred.password_ref().and_then(|pw| pw.verify(cleartext)), + Some(cred) => cred.password_ref().and_then(|pw| { + pw.verify(cleartext).map_err(|e| { + error!(crypto_err = ?e); + e.into() + }) + }), None => Err(OperationError::InvalidState), } } diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index 17b6fe9d8..a271df0b6 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -15,6 +15,7 @@ repository.workspace = true default = ["unix"] unix = [] selinux = ["dep:selinux"] +tpm = ["dep:tss-esapi", "kanidm_lib_crypto/tpm"] [[bin]] name = "kanidm_unixd" @@ -66,6 +67,7 @@ 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 } reqwest = { workspace = true, default-features = false } walkdir.workspace = true diff --git a/unix_integration/src/cache.rs b/unix_integration/src/cache.rs index 265b3e599..b314a6f3c 100644 --- a/unix_integration/src/cache.rs +++ b/unix_integration/src/cache.rs @@ -77,8 +77,9 @@ impl CacheLayer { uid_attr_map: UidAttr, gid_attr_map: UidAttr, allow_id_overrides: Vec, + require_tpm: Option<&str>, ) -> Result { - let db = Db::new(path)?; + let db = Db::new(path, require_tpm)?; // setup and do a migrate. { diff --git a/unix_integration/src/daemon.rs b/unix_integration/src/daemon.rs index 5affd3b93..db538b26e 100644 --- a/unix_integration/src/daemon.rs +++ b/unix_integration/src/daemon.rs @@ -673,6 +673,7 @@ async fn main() -> ExitCode { cfg.uid_attr_map, cfg.gid_attr_map, cfg.allow_local_account_override.clone(), + cfg.require_tpm.as_deref(), ) .await { @@ -712,6 +713,7 @@ async fn main() -> ExitCode { // Start to build the worker tasks let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4); let mut c_broadcast_rx = broadcast_tx.subscribe(); + let mut d_broadcast_rx = broadcast_tx.subscribe(); let task_b = tokio::spawn(async move { loop { @@ -739,10 +741,15 @@ async fn main() -> ExitCode { debug!("A task handler has connected."); // It did? Great, now we can wait and spin on that one // client. - if let Err(e) = - handle_task_client(socket, &task_channel_tx, &mut task_channel_rx).await - { - error!("Task client error occurred; error = {:?}", e); + + tokio::select! { + _ = d_broadcast_rx.recv() => { + break; + } + // We have to check for signals here else this tasks waits forever. + Err(e) = handle_task_client(socket, &task_channel_tx, &mut task_channel_rx) => { + error!("Task client error occurred; error = {:?}", e); + } } // If they DC we go back to accept. } @@ -754,13 +761,14 @@ async fn main() -> ExitCode { } // done } + info!("Stopped task connector"); }); // TODO: Setup a task that handles pre-fetching here. let (inotify_tx, mut inotify_rx) = channel(4); - let _watcher = + let watcher = match new_debouncer(Duration::from_secs(2), None, move |_event| { let _ = inotify_tx.try_send(true); }) @@ -798,6 +806,7 @@ async fn main() -> ExitCode { } } } + info!("Stopped inotify watcher"); }); // Set the umask while we open the path for most clients. @@ -839,6 +848,7 @@ async fn main() -> ExitCode { } } + info!("Stopped resolver"); }); info!("Server started ..."); @@ -880,12 +890,14 @@ async fn main() -> ExitCode { } } } - info!("Signal received, shutting down"); + info!("Signal received, sending down signal to tasks"); // Send a broadcast that we are done. if let Err(e) = broadcast_tx.send(true) { error!("Unable to shutdown workers {:?}", e); } + drop(watcher); + let _ = task_a.await; let _ = task_b.await; let _ = task_c.await; diff --git a/unix_integration/src/db.rs b/unix_integration/src/db.rs index cabd871b7..486ad6268 100644 --- a/unix_integration/src/db.rs +++ b/unix_integration/src/db.rs @@ -17,6 +17,7 @@ pub struct Db { pool: Pool, lock: Mutex<()>, crypto_policy: CryptoPolicy, + require_tpm: Option, } pub struct DbTxn<'a> { @@ -24,10 +25,11 @@ pub struct DbTxn<'a> { committed: bool, conn: r2d2::PooledConnection, crypto_policy: &'a CryptoPolicy, + require_tpm: Option<&'a tpm::TpmConfig>, } impl Db { - pub fn new(path: &str) -> Result { + pub fn new(path: &str, require_tpm: Option<&str>) -> Result { let before = unsafe { umask(0o0027) }; let manager = SqliteConnectionManager::file(path); let _ = unsafe { umask(before) }; @@ -42,10 +44,30 @@ impl Db { debug!("Configured {:?}", crypto_policy); + // Test a tpm context. + #[allow(unused_variables)] + let require_tpm = if let Some(tcti_str) = require_tpm { + #[cfg(feature = "tpm")] + let r = Db::tpm_setup_context( + tcti_str, + pool.get().expect("Unable to get connection from pool!!!"), + )?; + + #[cfg(not(feature = "tpm"))] + warn!("require_tpm is set, but tpm was not built in. This instance will NOT cache passwords!"); + #[cfg(not(feature = "tpm"))] + let r = tpm::TpmConfig {}; + + Some(r) + } else { + None + }; + Ok(Db { pool, lock: Mutex::new(()), crypto_policy, + require_tpm, }) } @@ -56,7 +78,7 @@ impl Db { .pool .get() .expect("Unable to get connection from pool!!!"); - DbTxn::new(conn, guard, &self.crypto_policy) + DbTxn::new(conn, guard, &self.crypto_policy, self.require_tpm.as_ref()) } } @@ -71,6 +93,7 @@ impl<'a> DbTxn<'a> { conn: r2d2::PooledConnection, guard: MutexGuard<'a, ()>, crypto_policy: &'a CryptoPolicy, + require_tpm: Option<&'a tpm::TpmConfig>, ) -> Self { // Start the transaction // debug!("Starting db WR txn ..."); @@ -82,6 +105,7 @@ impl<'a> DbTxn<'a> { conn, _guard: guard, crypto_policy, + require_tpm, } } @@ -448,9 +472,22 @@ impl<'a> DbTxn<'a> { } pub fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), ()> { - let pw = Password::new(self.crypto_policy, cred).map_err(|e| { - error!("password error -> {:?}", e); - })?; + #[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)?; + #[cfg(feature = "tpm")] + pw + } else { + Password::new(self.crypto_policy, cred).map_err(|e| { + error!("password error -> {:?}", e); + })? + }; + let dbpw = pw.to_dbpasswordv1(); let data = serde_json::to_vec(&dbpw).map_err(|e| { error!("json error -> {:?}", e); @@ -471,6 +508,11 @@ impl<'a> DbTxn<'a> { } pub fn check_account_password(&self, a_uuid: &str, cred: &str) -> Result { + #[cfg(not(feature = "tpm"))] + if self.require_tpm.is_some() { + return Ok(false); + } + let mut stmt = self .conn .prepare("SELECT password FROM account_t WHERE uuid = :a_uuid AND password IS NOT NULL") @@ -502,20 +544,34 @@ impl<'a> DbTxn<'a> { return Err(()); } - let r: Result = data - .first() - .map(|raw| { - // Map the option from data.first. - let dbpw: DbPasswordV1 = serde_json::from_slice(raw.as_slice()).map_err(|e| { - error!("json error -> {:?}", e); - })?; - let pw = Password::try_from(dbpw)?; - pw.verify(cred).map_err(|e| { - error!("password error -> {:?}", e); - }) + let pw = data.first().map(|raw| { + // Map the option from data.first. + let dbpw: DbPasswordV1 = serde_json::from_slice(raw.as_slice()).map_err(|e| { + error!("json error -> {:?}", e); + })?; + Password::try_from(dbpw) + }); + + let pw = match pw { + Some(Ok(p)) => p, + _ => 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); + + // Do nothing. + #[cfg(not(feature = "tpm"))] + let r = Ok(false); + + r + } else { + pw.verify(cred).map_err(|e| { + error!("password error -> {:?}", e); }) - .unwrap_or(Ok(false)); - r + } } fn get_group_data_name(&self, grp_id: &str) -> Result, i64)>, ()> { @@ -726,6 +782,170 @@ impl<'a> Drop for DbTxn<'a> { } } +#[cfg(not(feature = "tpm"))] +pub(crate) mod tpm { + pub struct TpmConfig {} +} + +#[cfg(feature = "tpm")] +pub(crate) mod tpm { + use super::Db; + + use r2d2_sqlite::SqliteConnectionManager; + use rusqlite::OptionalExtension; + + use kanidm_lib_crypto::{CryptoError, CryptoPolicy, Password, TpmError}; + use tss_esapi::{utils::TpmsContext, Context, TctiNameConf}; + + use std::str::FromStr; + + pub struct TpmConfig { + tcti: TctiNameConf, + ctx: TpmsContext, + } + + impl Db { + pub fn tpm_setup_context( + tcti_str: &str, + conn: r2d2::PooledConnection, + ) -> 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"); + })?; + + 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_ctx = if let Some(ctx_data) = ctx_data { + // Test loading, blank it out if it fails. + // deserialise + let maybe_ctx: TpmsContext = + serde_json::from_slice(ctx_data.as_slice()).map_err(|e| { + warn!("json error -> {:?}", e); + })?; + + // can it load? + context + .execute_with_nullauth_session(|ctx| ctx.context_load(maybe_ctx.clone())) + .map_err(|e| { + error!(tpm_err = ?e, "Failed to load tpm context"); + })?; + + Some(maybe_ctx) + } else { + None + }; + + let ctx = if let Some(existing_ctx) = ex_ctx { + existing_ctx + } else { + // Need to regenerate for some reason + info!("Creating new tpm ctx key"); + context + .execute_with_nullauth_session(|ctx| { + let key = Password::prepare_tpm_key(ctx)?; + + ctx.context_save(key.into()).map_err(|e| e.into()) + }) + .map_err(|e: CryptoError| { + error!(tpm_err = ?e, "Failed to create tpm key"); + })? + }; + + // Serialise it out. + let data = serde_json::to_vec(&ctx).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"); + + Ok(TpmConfig { tcti, ctx }) + } + + 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 = ctx.context_load(tpm_conf.ctx.clone()).map_err(|e| { + error!(tpm_err = ?e, "Failed to load tpm context"); + >::into(e) + })?; + + Password::new_argon2id_tpm(policy, cred, ctx, key) + }) + .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 = ctx.context_load(tpm_conf.ctx.clone()).map_err(|e| { + error!(tpm_err = ?e, "Failed to load tpm context"); + >::into(e) + })?; + + pw.verify_ctx(cred, Some((ctx, key))) + }) + .map_err(|e: CryptoError| { + error!(tpm_err = ?e, "Failed to create tpm bound password"); + }) + } + } +} + #[cfg(test)] mod tests { use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; @@ -739,7 +959,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_basic() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + let db = Db::new("", None).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -823,7 +1043,7 @@ mod tests { #[tokio::test] async fn test_cache_db_group_basic() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + let db = Db::new("", None).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -898,7 +1118,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_group_update() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + let db = Db::new("", None).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -966,7 +1186,15 @@ mod tests { #[tokio::test] async fn test_cache_db_account_password() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + + #[cfg(feature = "tpm")] + let tcti_str = Some("device:/dev/tpmrm0"); + + #[cfg(not(feature = "tpm"))] + let tcti_str = None; + + let db = Db::new("", tcti_str).expect("failed to create."); + let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1015,7 +1243,7 @@ mod tests { #[tokio::test] async fn test_cache_db_group_rename_duplicate() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + let db = Db::new("", None).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); @@ -1070,7 +1298,7 @@ mod tests { #[tokio::test] async fn test_cache_db_account_rename_duplicate() { sketching::test_init(); - let db = Db::new("").expect("failed to create."); + let db = Db::new("", None).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); diff --git a/unix_integration/src/unix_config.rs b/unix_integration/src/unix_config.rs index 8eba92bd8..f1bf0b1c1 100644 --- a/unix_integration/src/unix_config.rs +++ b/unix_integration/src/unix_config.rs @@ -33,6 +33,7 @@ struct ConfigInt { selinux: Option, #[serde(default)] allow_local_account_override: Vec, + require_tpm: Option, } #[derive(Debug, Copy, Clone)] @@ -92,6 +93,7 @@ pub struct KanidmUnixdConfig { pub uid_attr_map: UidAttr, pub gid_attr_map: UidAttr, pub selinux: bool, + pub require_tpm: Option, pub allow_local_account_override: Vec, } @@ -126,6 +128,11 @@ impl Display for KanidmUnixdConfig { writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?; writeln!(f, "selinux: {}", self.selinux)?; + writeln!( + f, + "require_tpm: {}", + self.require_tpm.as_deref().unwrap_or("-") + )?; writeln!( f, "allow_local_account_override: {:#?}", @@ -156,6 +163,7 @@ impl KanidmUnixdConfig { uid_attr_map: DEFAULT_UID_ATTR_MAP, gid_attr_map: DEFAULT_GID_ATTR_MAP, selinux: DEFAULT_SELINUX, + require_tpm: None, allow_local_account_override: Vec::default(), } } @@ -268,6 +276,7 @@ impl KanidmUnixdConfig { true => selinux_util::supported(), _ => false, }, + require_tpm: config.require_tpm.or(self.require_tpm), 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 236f5edf2..bc5b5217a 100644 --- a/unix_integration/tests/cache_layer_test.rs +++ b/unix_integration/tests/cache_layer_test.rs @@ -111,6 +111,7 @@ async fn setup_test(fix_fn: Fixture) -> (CacheLayer, KanidmClient) { DEFAULT_UID_ATTR_MAP, DEFAULT_GID_ATTR_MAP, vec!["masked_group".to_string()], + None, ) .await .expect("Failed to build cache layer.");