From 0425122ba3de78d8bae4bb7089c8c7994b7e925b Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 30 Jun 2023 12:41:32 +1000 Subject: [PATCH] 20230629 tpm keygen ... again (#1793) --- Cargo.lock | 1 + libs/crypto/src/lib.rs | 125 +++++++++--------- unix_integration/Cargo.toml | 1 + unix_integration/src/db.rs | 245 +++++++++++++++++++++++++++++++----- 4 files changed, 277 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6248ebcb..4e8f1b870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2571,6 +2571,7 @@ dependencies = [ name = "kanidm_unix_int" version = "1.1.0-beta.13-dev" dependencies = [ + "base64urlsafedata", "bytes", "clap", "clap_complete", diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index 0cbf5e71c..bbeddacda 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -64,6 +64,7 @@ const ARGON2_MAX_P_COST: u32 = 1; #[derive(Clone, Debug)] pub enum CryptoError { Tpm2, + Tpm2PublicBuilder, Tpm2FeatureMissing, Tpm2InputExceeded, Tpm2ContextMissing, @@ -1167,67 +1168,6 @@ impl Password { | Kdf::NT_MD4(_) => true, } } - - #[cfg(feature = "tpm")] - pub fn prepare_tpm_key(tpm_ctx: &mut TpmContext) -> Result { - use tss_esapi::{ - attributes::ObjectAttributesBuilder, - interface_types::{ - algorithm::{HashingAlgorithm, PublicAlgorithm}, - resource_handles::Hierarchy, - }, - structures::{Digest, KeyedHashScheme, PublicBuilder, PublicKeyedHashParameters}, - }; - - // 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 - }) - } } #[cfg(feature = "tpm")] @@ -1447,7 +1387,7 @@ mod tests { .expect("Failed to create Context"); let key = context - .execute_with_nullauth_session(|ctx| Password::prepare_tpm_key(ctx)) + .execute_with_nullauth_session(|ctx| prepare_tpm_key(ctx)) .unwrap(); let p = CryptoPolicy::minimum(); @@ -1504,4 +1444,65 @@ mod tests { }) .unwrap(); } + + #[cfg(feature = "tpm")] + pub fn prepare_tpm_key(tpm_ctx: &mut TpmContext) -> Result { + use tss_esapi::{ + attributes::ObjectAttributesBuilder, + interface_types::{ + algorithm::{HashingAlgorithm, PublicAlgorithm}, + resource_handles::Hierarchy, + }, + structures::{Digest, KeyedHashScheme, PublicBuilder, PublicKeyedHashParameters}, + }; + + // 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 + }) + } } diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index adb692c4e..2f1258dd2 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -42,6 +42,7 @@ name = "kanidm_unix_common" path = "src/lib.rs" [dependencies] +base64urlsafedata = { workspace = true } bytes = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } csv = { workspace = true } diff --git a/unix_integration/src/db.rs b/unix_integration/src/db.rs index 8d50e1716..49a491bca 100644 --- a/unix_integration/src/db.rs +++ b/unix_integration/src/db.rs @@ -782,13 +782,159 @@ pub(crate) mod tpm { use rusqlite::{Connection, OptionalExtension}; use kanidm_lib_crypto::{CryptoError, CryptoPolicy, Password, TpmError}; - use tss_esapi::{utils::TpmsContext, Context, TctiNameConf}; + 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 for LoadableKey { + fn into(self) -> BinaryLoadableKey { + BinaryLoadableKey { + private: self.private.as_slice().to_owned().into(), + public: self.public.marshall().unwrap().into(), + } + } + } + + impl TryFrom for LoadableKey { + type Error = &'static str; + + fn try_from(value: BinaryLoadableKey) -> Result { + 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, - ctx: TpmsContext, + 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 { + 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 { + 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 { + // 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"); + >::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"); + >::into(e) + }) } impl Db { @@ -801,6 +947,19 @@ pub(crate) mod tpm { 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, @@ -828,46 +987,66 @@ pub(crate) mod tpm { trace!(ctx_data_present = %ctx_data.is_some()); - let ex_ctx = if let Some(ctx_data) = ctx_data { + 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| { + 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(|maybe_ctx: TpmsContext| { - // can it load? + .and_then(|loadable: LoadableKey| { + // test if it can load? context - .execute_with_nullauth_session(|ctx| ctx.context_load(maybe_ctx.clone())) + .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 context"); + error!(tpm_err = ?e, "Failed to load tpm hmac key"); }) .ok() - // It loaded, so sub in our context. - .map(|_handle| maybe_ctx) + // It loaded, so sub in our Private data. + .map(|_hmac_handle| loadable) }) } else { None }; - let ctx = if let Some(existing_ctx) = ex_ctx { - existing_ctx + let loadable = if let Some(existing) = ex_private { + existing } else { - // Need to regenerate for some reason - info!("Creating new tpm ctx key"); + // 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| { - let key = Password::prepare_tpm_key(ctx)?; - - ctx.context_save(key.into()).map_err(|e| e.into()) + 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: CryptoError| { - error!(tpm_err = ?e, "Failed to create tpm key"); + .map_err(|e| { + error!(tpm_err = ?e, "Failed to create tpm hmac key"); })? }; // Serialise it out. - let data = serde_json::to_vec(&ctx).map_err(|e| { + let data = serde_json::to_vec(&loadable).map_err(|e| { error!("json error -> {:?}", e); })?; @@ -883,9 +1062,17 @@ pub(crate) mod tpm { }) .map(|_| ())?; + // + info!("tpm binding configured"); - Ok(TpmConfig { tcti, ctx }) + let LoadableKey { private, public } = loadable; + + Ok(TpmConfig { + tcti, + private, + public, + }) } pub fn tpm_new( @@ -899,12 +1086,8 @@ pub(crate) mod tpm { context .execute_with_nullauth_session(|ctx| { - let key = ctx.context_load(tpm_conf.ctx.clone()).map_err(|e| { - error!(tpm_err = ?e, "Failed to load tpm context"); - >::into(e) - })?; - - Password::new_argon2id_tpm(policy, cred, ctx, key) + 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"); @@ -918,12 +1101,8 @@ pub(crate) mod tpm { context .execute_with_nullauth_session(|ctx| { - let key = ctx.context_load(tpm_conf.ctx.clone()).map_err(|e| { - error!(tpm_err = ?e, "Failed to load tpm context"); - >::into(e) - })?; - - pw.verify_ctx(cred, Some((ctx, key))) + 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");