Implement tpm binding of cached password hashes (#1754)

This commit is contained in:
Firstyear 2023-06-21 20:33:01 +10:00 committed by GitHub
parent 56a2257360
commit f3080df628
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 907 additions and 70 deletions

193
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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<OperationError> for CryptoError {
fn into(self) -> OperationError {
OperationError::CryptographyError
}
}
#[cfg(feature = "tpm")]
impl From<TpmError> 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<u8>,
key: Vec<u8>,
},
//
ARGON2ID {
m_cost: u32,
@ -272,6 +330,16 @@ impl TryFrom<DbPasswordV1> for Password {
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, OperationError> {
pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, CryptoError> {
let pbkdf2_cost = policy.pbkdf2_cost;
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (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<Self, OperationError> {
pub fn new_argon2id(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, CryptoError> {
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<Self, CryptoError> {
let version = Version::V0x13;
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
let mut check_key: Vec<u8> = (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<Self, OperationError> {
pub fn new(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, CryptoError> {
Self::new_pbkdf2(policy, cleartext)
}
pub fn verify(&self, cleartext: &str) -> Result<bool, OperationError> {
match &self.material {
Kdf::ARGON2ID {
m_cost,
t_cost,
p_cost,
version,
salt,
key,
} => {
pub fn verify(&self, cleartext: &str) -> Result<bool, CryptoError> {
self.verify_ctx(cleartext, None)
}
pub fn verify_ctx<'a>(
&self,
cleartext: &str,
tpm: Option<(&'a mut TpmContext, TpmHandle)>,
) -> Result<bool, CryptoError> {
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<u8> = (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<u8> = (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<u8> = (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<u8> = 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<TpmHandle, CryptoError> {
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<u8>,
ctx: &mut TpmContext,
key_handle: TpmHandle,
) -> Result<Vec<u8>, 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<u8>,
_ctx: &mut TpmContext,
_key_handle: TpmHandle,
) -> Result<Vec<u8>, 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();
}
}

View file

@ -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

View file

@ -460,7 +460,12 @@ impl Credential {
policy: &CryptoPolicy,
cleartext: &str,
) -> Result<Self, OperationError> {
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<Self, OperationError> {
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<Self, OperationError> {
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<bool, OperationError> {
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.

View file

@ -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(

View file

@ -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<bool, OperationError> {
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),
}
}

View file

@ -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

View file

@ -77,8 +77,9 @@ impl CacheLayer {
uid_attr_map: UidAttr,
gid_attr_map: UidAttr,
allow_id_overrides: Vec<String>,
require_tpm: Option<&str>,
) -> Result<Self, ()> {
let db = Db::new(path)?;
let db = Db::new(path, require_tpm)?;
// setup and do a migrate.
{

View file

@ -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;

View file

@ -17,6 +17,7 @@ pub struct Db {
pool: Pool<SqliteConnectionManager>,
lock: Mutex<()>,
crypto_policy: CryptoPolicy,
require_tpm: Option<tpm::TpmConfig>,
}
pub struct DbTxn<'a> {
@ -24,10 +25,11 @@ pub struct DbTxn<'a> {
committed: bool,
conn: r2d2::PooledConnection<SqliteConnectionManager>,
crypto_policy: &'a CryptoPolicy,
require_tpm: Option<&'a tpm::TpmConfig>,
}
impl Db {
pub fn new(path: &str) -> Result<Self, ()> {
pub fn new(path: &str, require_tpm: Option<&str>) -> Result<Self, ()> {
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<SqliteConnectionManager>,
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<bool, ()> {
#[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<bool, ()> = 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<Vec<(Vec<u8>, 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<SqliteConnectionManager>,
) -> Result<TpmConfig, ()> {
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<Vec<u8>> = 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<Password, ()> {
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");
<TpmError as Into<CryptoError>>::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<bool, ()> {
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");
<TpmError as Into<CryptoError>>::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());

View file

@ -33,6 +33,7 @@ struct ConfigInt {
selinux: Option<bool>,
#[serde(default)]
allow_local_account_override: Vec<String>,
require_tpm: Option<String>,
}
#[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<String>,
pub allow_local_account_override: Vec<String>,
}
@ -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,
})
}

View file

@ -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.");