#![allow(
dead_code,
unused_imports,
unused_qualifications,
unreachable_patterns,
unsafe_code,
let_underscore_drop
)]
use super::convert::p1363_to_der;
use super::export::export_public_key;
use super::key;
use super::provider;
use super::state;
use crate::internal::core::metadata;
use crate::internal::core::traits::{EnclaveKeyManager, EnclaveSigner};
use crate::internal::core::types::validate_label;
use crate::internal::core::{AccessPolicy, Error, KeyMeta, KeyType, Result};
use sha2::{Digest, Sha256};
use windows::Win32::Security::Cryptography::*;
const ECDSA_P256_ALGORITHM: &str = "ECDSA_P256";
#[derive(Debug)]
pub struct TpmSigner {
app_name: String,
keys_dir_override: Option<std::path::PathBuf>,
}
impl TpmSigner {
pub fn new(app_name: &str) -> Self {
TpmSigner {
app_name: app_name.to_string(),
keys_dir_override: None,
}
}
pub fn with_keys_dir(app_name: &str, keys_dir: std::path::PathBuf) -> Self {
TpmSigner {
app_name: app_name.to_string(),
keys_dir_override: Some(keys_dir),
}
}
fn keys_dir(&self) -> std::path::PathBuf {
self.keys_dir_override
.clone()
.unwrap_or_else(|| metadata::keys_dir(&self.app_name))
}
}
impl EnclaveKeyManager for TpmSigner {
fn generate(&self, label: &str, key_type: KeyType, policy: AccessPolicy) -> Result<Vec<u8>> {
validate_label(label)?;
if key_type != KeyType::Signing {
return Err(Error::KeyOperation {
operation: "generate".into(),
detail: "TpmSigner only supports signing keys".into(),
});
}
let dir = self.keys_dir();
let provider = provider::open_provider()?;
let state = state::KeyMaterialState::acquire(&dir)?;
state.ensure_label_available(label, || {
match key::open_key(&provider, &self.app_name, label) {
Ok(_key) => Ok(state::AuthoritativeKeyState::Present),
Err(Error::KeyNotFound { .. }) => Ok(state::AuthoritativeKeyState::Missing),
Err(error) => Err(error),
}
})?;
let (_key_handle, pub_key) = key::create_key(
&provider,
&self.app_name,
label,
ECDSA_P256_ALGORITHM,
policy,
)?;
state.persist_generated_key(label, key_type, policy, &pub_key, || {
key::delete_key(&self.app_name, label)
})?;
let hmac_key_opt = crate::internal::windows::meta_hmac::load_or_create(&self.app_name)
.ok()
.flatten();
if let Some(hk) = hmac_key_opt {
let meta = KeyMeta::new(label, key_type, policy);
if let Err(e) = metadata::save_meta_with_hmac(state.dir(), label, &meta, hk.as_slice())
{
rollback_after_persist(state.dir(), &self.app_name, label);
return Err(e);
}
let meta_path = state.dir().join(format!("{label}.meta"));
let meta_bytes = match std::fs::read(&meta_path) {
Ok(b) => b,
Err(e) => {
rollback_after_persist(state.dir(), &self.app_name, label);
return Err(Error::KeyOperation {
operation: "post_persist_meta_read".into(),
detail: format!("read {}: {e}", meta_path.display()),
});
}
};
let tag = metadata::compute_meta_hmac_bytes(hk.as_slice(), &meta_bytes);
if let Err(e) = crate::internal::windows::meta_tag::store(&self.app_name, label, &tag) {
rollback_after_persist(state.dir(), &self.app_name, label);
return Err(e);
}
} else {
tracing::warn!(
label = label,
"meta-HMAC key unavailable at keygen; key persisted without integrity tag. \
Run `<app> migrate-meta` once DPAPI is reachable."
);
}
Ok(pub_key)
}
fn public_key(&self, label: &str) -> Result<Vec<u8>> {
validate_label(label)?;
let dir = self.keys_dir();
let state = state::KeyMaterialState::acquire(&dir)?;
let provider = provider::open_provider()?;
let key_handle = key::open_key(&provider, &self.app_name, label)?;
let pub_key = export_public_key(&key_handle)?;
metadata::sync_pub_key(state.dir(), label, &pub_key)
}
fn list_keys(&self) -> Result<Vec<String>> {
let provider = provider::open_provider()?;
key::enumerate_keys(&provider, &self.app_name)
}
fn delete_key(&self, label: &str) -> Result<()> {
validate_label(label)?;
let dir = self.keys_dir();
let state = state::KeyMaterialState::acquire(&dir)?;
state.reconcile_deleted_key(label, || match key::delete_key(&self.app_name, label) {
Ok(()) => Ok(state::AuthoritativeKeyState::Present),
Err(Error::KeyNotFound { .. }) => Ok(state::AuthoritativeKeyState::Missing),
Err(error) => Err(error),
})
}
fn rename_key(&self, old_label: &str, new_label: &str) -> Result<()> {
let _ = (old_label, new_label);
Err(Error::KeyOperation {
operation: "rename_key".into(),
detail: "renaming is not supported on the Windows TPM backend: \
CNG keys are immutable by name. Create a new key with \
the desired label and migrate authorized_keys instead."
.into(),
})
}
fn is_available(&self) -> bool {
provider::is_available()
}
}
fn rollback_after_persist(dir: &std::path::Path, app_name: &str, label: &str) {
if let Err(e) = crate::internal::windows::meta_tag::delete(app_name, label) {
tracing::warn!(label = label, error = %e, "rollback: meta_tag::delete failed");
}
match metadata::delete_key_files(dir, label) {
Ok(()) | Err(Error::KeyNotFound { .. }) => {}
Err(e) => tracing::warn!(label = label, error = %e, "rollback: file cleanup failed"),
}
if let Err(e) = key::delete_key(app_name, label) {
tracing::warn!(label = label, error = %e, "rollback: CNG key delete failed");
}
}
fn ensure_meta_integrity(app_name: &str, label: &str, dir: &std::path::Path) -> Result<()> {
let meta_path = dir.join(format!("{label}.meta"));
if !meta_path.exists() {
return Ok(());
}
let hmac_key = match crate::internal::windows::meta_hmac::load_existing(app_name) {
Ok(Some(k)) => k,
Ok(None) | Err(_) => return Ok(()),
};
match crate::internal::windows::meta_tag::verify(app_name, label, dir, hmac_key.as_slice())? {
crate::internal::windows::meta_tag::VerifyOutcome::Match
| crate::internal::windows::meta_tag::VerifyOutcome::NoMeta
| crate::internal::windows::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
crate::internal::windows::meta_tag::VerifyOutcome::Tamper => Err(Error::KeyOperation {
operation: "meta_tag_verify".into(),
detail: format!(
"key '{label}': metadata integrity check failed. The on-disk meta \
does not match the keychain-stored tag — meta may have been \
tampered with. Refusing to proceed. Regenerate the key to restore \
a known-good state."
),
}),
crate::internal::windows::meta_tag::VerifyOutcome::Legacy => {
let marker_set =
crate::internal::windows::meta_migration_marker::is_set(app_name).unwrap_or(false);
if marker_set {
Err(Error::KeyOperation {
operation: "meta_tag_legacy_post_migration".into(),
detail: format!(
"key '{label}' has no integrity tag, but `{app_name} migrate-meta` \
has already completed on this install. This is a strong tamper \
signal — legitimate operation should not produce a missing tag \
after the marker is set. Recommended: regenerate the affected \
key with `{app_name} keygen`. Do NOT run migrate-meta again \
unless you can independently explain why this key's tag is \
missing (e.g., manual restore from a backup of an unrelated \
machine), in which case pass \
`--force-rerun-i-understand` to override."
),
})
} else {
Err(Error::KeyOperation {
operation: "meta_tag_legacy".into(),
detail: format!(
"key '{label}' has no integrity tag. This is the one-time \
migration required by upgrading to a build that introduces meta \
integrity tags, and is not something future upgrades will repeat. \
Before migrating, verify the key's current policy looks correct: \
`{app_name} inspect {label}`. To migrate: `{app_name} \
migrate-meta`."
),
})
}
}
}
}
impl EnclaveSigner for TpmSigner {
fn sign(&self, label: &str, data: &[u8]) -> Result<Vec<u8>> {
validate_label(label)?;
let provider = provider::open_provider()?;
let key_handle = key::open_key(&provider, &self.app_name, label)?;
ensure_meta_integrity(&self.app_name, label, &self.keys_dir())?;
let dir = self.keys_dir();
let expected_policy = match metadata::load_meta(&dir, label) {
Ok(meta) => meta.access_policy,
Err(Error::KeyNotFound { .. }) => AccessPolicy::None,
Err(err) => return Err(err),
};
crate::internal::windows::ui_policy::verify_ui_policy_matches(
&key_handle,
expected_policy,
)?;
let digest = Sha256::digest(data);
let mut sig_size: u32 = 0;
unsafe {
NCryptSignHash(
key_handle.as_key(),
None,
&digest,
None,
&mut sig_size,
NCRYPT_FLAGS::default(),
)
.map_err(|e| Error::SignFailed {
detail: format!("NCryptSignHash size query: {e}"),
})?;
}
let mut sig = vec![0_u8; sig_size as usize];
unsafe {
NCryptSignHash(
key_handle.as_key(),
None,
&digest,
Some(&mut sig),
&mut sig_size,
NCRYPT_FLAGS::default(),
)
.map_err(|e| Error::SignFailed {
detail: format!("NCryptSignHash: {e}"),
})?;
}
sig.truncate(sig_size as usize);
Ok(p1363_to_der(&sig))
}
}