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