20231101 add id cert to unixint (#2284)

This commit is contained in:
Firstyear 2023-11-09 13:11:23 +10:00 committed by GitHub
parent 0174283115
commit 3bd2cc8a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 596 additions and 804 deletions

46
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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());
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(()),
}
}

View file

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

View file

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