mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
3
Commits
main
...
windows/tpm-key
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd1de7c1fd | ||
|
|
20c33a0c8b | ||
|
|
a00d568212 |
No files matched your search
Generated
+96
@@ -3111,6 +3111,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"which",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3709,12 +3710,89 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -3724,6 +3802,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -3758,6 +3845,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -52,6 +52,10 @@ path = "tests/age.rs"
|
||||
name = "mitm"
|
||||
path = "tests/mitm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "tpm"
|
||||
path = "tests/tpm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "managers"
|
||||
path = "tests/managers.rs"
|
||||
@@ -79,6 +83,9 @@ rama = { version = "0.4.0", features = ["net-apple-networkextension"] }
|
||||
security-framework = "3.7.0"
|
||||
security-framework-sys = "2.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Foundation", "Win32_Security_Cryptography"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ use rcgen::{
|
||||
use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
/// Held in the Secure Enclave on macOS, in memory elsewhere.
|
||||
/// Held in the Secure Enclave on macOS, the TPM on Windows, in memory elsewhere.
|
||||
pub type CaSigner = Arc<dyn SigningKey + Send + Sync>;
|
||||
|
||||
pub struct CertificateAuthority {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::{system_config, uninstall};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod tpm;
|
||||
|
||||
pub use tpm::CaKey;
|
||||
@@ -0,0 +1,153 @@
|
||||
use rama::error::BoxError;
|
||||
use rama::tls::boring::core::bn::BigNum;
|
||||
use rama::tls::boring::core::ecdsa::EcdsaSig;
|
||||
use rama::tls::boring::core::sha::sha256;
|
||||
use rcgen::{Error, PublicKeyData, SignatureAlgorithm, SigningKey, PKCS_ECDSA_P256_SHA256};
|
||||
use windows::core::{Owned, Result as WinResult, HSTRING};
|
||||
use windows::Win32::Foundation::{NTE_BAD_KEY, NTE_BAD_KEYSET};
|
||||
use windows::Win32::Security::Cryptography::*;
|
||||
|
||||
const KEY_NAME: &str = "Strix Firewall CA";
|
||||
|
||||
/// A P-256 key held by the TPM. Signing asks the TPM to sign, so the private key never leaves it.
|
||||
pub struct CaKey {
|
||||
// Fields drop in order, so the key handle closes before its provider's.
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
_provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
/// 0x04 || X || Y, the form rcgen writes into a certificate's SubjectPublicKeyInfo.
|
||||
public_point: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CaKey {
|
||||
/// A machine key, so the service and an elevated installer share it.
|
||||
pub fn load_or_create() -> Result<Self, BoxError> {
|
||||
match open_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)? {
|
||||
Some(key) => Ok(key),
|
||||
None => Ok(create_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// A per-user key under a random name, which needs no elevation; `delete_from_tpm` it after.
|
||||
pub fn create_test_key() -> WinResult<Self> {
|
||||
let name = format!("Strix Firewall test {:016x}", rand::random::<u64>());
|
||||
create_key(&name, NCRYPT_FLAGS(0))
|
||||
}
|
||||
|
||||
/// Deletes the key from the TPM, not just this handle to it.
|
||||
pub fn delete_from_tpm(mut self) -> WinResult<()> {
|
||||
// NCryptDeleteKey also frees the handle, so the Owned mustn't free it again.
|
||||
let key_handle = std::mem::take(&mut *self.key_handle);
|
||||
unsafe { NCryptDeleteKey(key_handle, 0) }
|
||||
}
|
||||
|
||||
/// Takes both handles and reads the key's public point once, for rcgen.
|
||||
fn from_handles(
|
||||
provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
) -> WinResult<Self> {
|
||||
// A BCRYPT_ECCKEY_BLOB header, then X and Y.
|
||||
const HEADER_LEN: usize = size_of::<BCRYPT_ECCKEY_BLOB>();
|
||||
let mut blob = [0u8; HEADER_LEN + 64];
|
||||
let mut blob_len = 0;
|
||||
unsafe {
|
||||
NCryptExportKey(
|
||||
*key_handle,
|
||||
None,
|
||||
BCRYPT_ECCPUBLIC_BLOB,
|
||||
None,
|
||||
Some(&mut blob),
|
||||
&mut blob_len,
|
||||
NCRYPT_FLAGS(0),
|
||||
)?
|
||||
};
|
||||
let (header, x_and_y) = blob.split_at(HEADER_LEN);
|
||||
let is_p256 = header[..4] == BCRYPT_ECDSA_PUBLIC_P256_MAGIC.to_le_bytes();
|
||||
if !is_p256 || blob_len as usize != blob.len() {
|
||||
return Err(windows::core::Error::new(NTE_BAD_KEY, "key is not an ECDSA P-256 key"));
|
||||
}
|
||||
let public_point = [&[0x04], x_and_y].concat();
|
||||
Ok(CaKey { key_handle, _provider_handle: provider_handle, public_point })
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKeyData for CaKey {
|
||||
fn der_bytes(&self) -> &[u8] {
|
||||
&self.public_point
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static SignatureAlgorithm {
|
||||
&PKCS_ECDSA_P256_SHA256
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningKey for CaKey {
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut r_and_s = [0u8; 64];
|
||||
let mut signature_len = 0;
|
||||
let signed = unsafe {
|
||||
NCryptSignHash(
|
||||
*self.key_handle,
|
||||
None,
|
||||
&sha256(msg),
|
||||
Some(&mut r_and_s),
|
||||
&mut signature_len,
|
||||
NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
signed.map_err(|_| Error::RemoteKeyError)?;
|
||||
// NCrypt returns r || s, each half the length; X.509 wants DER SEQUENCE { r, s }.
|
||||
let (r, s) = r_and_s[..signature_len as usize].split_at(signature_len as usize / 2);
|
||||
let der = BigNum::from_slice(r)
|
||||
.and_then(|r| EcdsaSig::from_private_components(r, BigNum::from_slice(s)?))
|
||||
.and_then(|signature| signature.to_der());
|
||||
der.map_err(|_| Error::RemoteKeyError)
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` if the TPM holds no key by that name.
|
||||
/// `scope` is `NCRYPT_MACHINE_KEY_FLAG`, or empty for the current user's keys.
|
||||
fn open_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<Option<CaKey>> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
let opened = unsafe {
|
||||
NCryptOpenKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope | NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
match opened {
|
||||
Err(error) if error.code() == NTE_BAD_KEYSET => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
Ok(()) => CaKey::from_handles(provider_handle, unsafe { Owned::new(key_handle) }).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the key inside the TPM, which never lets it out.
|
||||
fn create_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<CaKey> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
unsafe {
|
||||
NCryptCreatePersistedKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
NCRYPT_ECDSA_P256_ALGORITHM,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope,
|
||||
)?
|
||||
};
|
||||
let key_handle = unsafe { Owned::new(key_handle) };
|
||||
// Until finalized the key is only a handle; this generates and stores it.
|
||||
unsafe { NCryptFinalizeKey(*key_handle, NCRYPT_SILENT_FLAG)? };
|
||||
CaKey::from_handles(provider_handle, key_handle)
|
||||
}
|
||||
|
||||
/// The Microsoft Platform Crypto Provider, which keeps its keys in the TPM.
|
||||
fn open_tpm_provider() -> WinResult<Owned<NCRYPT_PROV_HANDLE>> {
|
||||
let mut provider_handle = NCRYPT_PROV_HANDLE::default();
|
||||
unsafe { NCryptOpenStorageProvider(&mut provider_handle, MS_PLATFORM_CRYPTO_PROVIDER, 0)? };
|
||||
Ok(unsafe { Owned::new(provider_handle) })
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use firewall::mitm::CertificateAuthority;
|
||||
use firewall::os::windows::CaKey;
|
||||
use rcgen::PublicKeyData;
|
||||
|
||||
/// Skipped on machines without a TPM.
|
||||
#[test]
|
||||
fn key_signs_a_verifiable_ca() {
|
||||
let key = match CaKey::create_test_key() {
|
||||
Ok(key) => key,
|
||||
Err(error) => return eprintln!("skipping, no TPM key: {error}"),
|
||||
};
|
||||
let ca = CertificateAuthority::generate(&["registry.npmjs.org"], &key);
|
||||
let spki = key.subject_public_key_info();
|
||||
key.delete_from_tpm().expect("delete the test key");
|
||||
let ca = ca.expect("mint a ca");
|
||||
let public = ca.crt.public_key().expect("ca public key");
|
||||
assert!(ca.crt.verify(&public).expect("verify the ca"));
|
||||
assert_eq!(public.public_key_to_der().unwrap(), spki);
|
||||
}
|
||||
Reference in new issue
Block a user