mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Implement tpm binding of cached password hashes (#1754)
This commit is contained in:
parent
56a2257360
commit
f3080df628
193
Cargo.lock
generated
193
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
{
|
||||
|
|
|
@ -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,11 +741,16 @@ 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
|
||||
{
|
||||
|
||||
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.
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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| {
|
||||
#[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| {
|
||||
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);
|
||||
})?;
|
||||
let pw = Password::try_from(dbpw)?;
|
||||
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());
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
|
|
Loading…
Reference in a new issue