diff --git a/.github/workflows/rust_ci.yml b/.github/workflows/rust_ci.yml index e79fa3cd..237061e3 100644 --- a/.github/workflows/rust_ci.yml +++ b/.github/workflows/rust_ci.yml @@ -97,4 +97,8 @@ jobs: - name: test SDK working-directory: ./rust/sdk - run: cargo test --workspace + run: cargo test + --workspace + --exclude vbsenclave-sdk-enclave + --exclude windows-enclave + --exclude sdk_enclave_gen diff --git a/rust/scripts/generate_codegen_crates.ps1 b/rust/scripts/generate_codegen_crates.ps1 index 8ebc3682..6aeb05f0 100644 --- a/rust/scripts/generate_codegen_crates.ps1 +++ b/rust/scripts/generate_codegen_crates.ps1 @@ -13,7 +13,9 @@ param( [Parameter(Mandatory = $true)] [string]$Namespace = "", - [string]$Vtl0ClassName = "" + [string]$Vtl0ClassName = "", + + [string]$ImportDirectories = "" ) $ErrorActionPreference = "Stop" @@ -27,7 +29,8 @@ $ErrorActionPreference = "Stop" --language rust ` --EdlPath $EdlPath ` --VirtualTrustLayer enclave ` - --OutputDirectory $EnclaveOutDir + --OutputDirectory $EnclaveOutDir ` + --ImportDirectories $ImportDirectories # Run codegen for the host crate & $edlCodeGenToolsPath ` @@ -36,4 +39,5 @@ $ErrorActionPreference = "Stop" --EdlPath $EdlPath ` --VirtualTrustLayer hostapp ` --Vtl0ClassName $Vtl0ClassName ` - --OutputDirectory $HostAppOutDir + --OutputDirectory $HostAppOutDir ` + --ImportDirectories $ImportDirectories diff --git a/rust/scripts/sign-enclave.ps1 b/rust/scripts/sign-enclave.ps1 index d13f7d4b..d5a9eaa9 100644 --- a/rust/scripts/sign-enclave.ps1 +++ b/rust/scripts/sign-enclave.ps1 @@ -22,23 +22,37 @@ if (-not (Test-Path $sdkRegPath)) { throw "Windows SDK not found. Please install the latest Windows 11 SDK." } -# Read the two important values +# Read the installation folder $sdk = Get-ItemProperty -Path $sdkRegPath - $installationFolder = $sdk.InstallationFolder -$productVersion = $sdk.ProductVersion Write-Host "`nAttempting to run veiid.exe and sign enclave DLL using certificate '$CertName'..." -if (-not $installationFolder -or -not $productVersion) { +if (-not $installationFolder) { throw "Incomplete Windows SDK installation. Please reinstall the latest Windows SDK." } -# Build full SDK bin path -$sdkBin = Join-Path $installationFolder "bin\$productVersion.0\x64" +# Find an SDK version that has veiid.exe in its bin folder +# List all version folders under bin and find one with veiid.exe +$binRoot = Join-Path $installationFolder "bin" +if (-not (Test-Path $binRoot)) { + throw "Windows SDK bin folder not found at: $binRoot. Please install the latest Windows SDK." +} -if (-not (Test-Path $sdkBin)) { - throw "Windows SDK bin folder not found at: $sdkBin. Please install (or repair) the latest Windows SDK." +$sdkBin = $null +$versionFolders = Get-ChildItem -Path $binRoot -Directory | Sort-Object Name -Descending +foreach ($versionFolder in $versionFolders) { + $candidateBin = Join-Path $versionFolder.FullName "x64" + $candidateVeiid = Join-Path $candidateBin "veiid.exe" + if (Test-Path $candidateVeiid) { + $sdkBin = $candidateBin + Write-Host "Found SDK tools in: $sdkBin" + break + } +} + +if (-not $sdkBin) { + throw "Could not find veiid.exe in any SDK version under $binRoot. Please install Windows SDK 10.0.26100.0 or higher." } # Compute tool paths @@ -84,3 +98,6 @@ if ($LASTEXITCODE -ne 0) { } else { Write-Host "Enclave DLL signing completed with no warnings." } + +# Exit with success since signing completed (even with warnings) +exit 0 \ No newline at end of file diff --git a/rust/sdk/Cargo.toml b/rust/sdk/Cargo.toml index 953b7b76..5415ef8b 100644 --- a/rust/sdk/Cargo.toml +++ b/rust/sdk/Cargo.toml @@ -3,8 +3,8 @@ resolver = "3" members = [ "crates/libs/sdk-enclave", "crates/libs/sdk-host", - "crates/libs/sdk-enclave/generated/veil_abi_gen", - "crates/libs/sdk-host/generated/veil_abi_gen", + "crates/libs/sdk-enclave/generated/sdk_gen", + "crates/libs/sdk-host/generated/sdk_gen", "crates/libs/userboundkey-kcm", "crates/tests/sdk", "crates/libs/windows-enclave", @@ -14,11 +14,13 @@ members = [ edition = "2024" [workspace.dependencies] +edlcodegen-tools = { git = "https://github.com/microsoft/VbsEnclaveTooling" } regex = "1.11" spin = "0.10.0" userboundkey-kcm = { version = "0.1.0", path = "crates/libs/userboundkey-kcm" } vbsenclave-sdk-enclave = { version = "0.1.0", path = "crates/libs/sdk-enclave" } vbsenclave-sdk-host = { version = "0.1.0", path = "crates/libs/sdk-host" } +widestring = { version = "1.1", default-features = false, features = ["alloc"] } windows = "0.62.2" windows-bindgen = "0.66.0" windows-enclave = { version = "0.1.0", path = "crates/libs/windows-enclave" } diff --git a/rust/sdk/crates/libs/sdk-enclave/Cargo.toml b/rust/sdk/crates/libs/sdk-enclave/Cargo.toml index d06cb53e..5ff4b67a 100644 --- a/rust/sdk/crates/libs/sdk-enclave/Cargo.toml +++ b/rust/sdk/crates/libs/sdk-enclave/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = { workspace = true } [dependencies] +sdk_enclave_gen = { path = "./generated/sdk_gen" } spin = { workspace = true } -veil_abi_enclave_gen = { path = "./generated/veil_abi_gen" } -windows-enclave = {workspace = true } - +widestring = { workspace = true } +windows-enclave = { workspace = true } diff --git a/rust/sdk/crates/libs/sdk-enclave/src/common/crypto.rs b/rust/sdk/crates/libs/sdk-enclave/src/common/crypto.rs new file mode 100644 index 00000000..b6c486ae --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/common/crypto.rs @@ -0,0 +1,459 @@ +//! Common cryptographic utilities for VBS Enclave SDK +//! +//! This module provides general-purpose cryptographic functionality that can be +//! used across different modules in the SDK, including: +//! - BCrypt API wrappers and RAII types +//! - Enclave sealing and unsealing +//! - Random number generation +//! - AES-GCM encryption and decryption +//! - Error checking helpers + +extern crate alloc; + +use alloc::vec::Vec; +use core::ffi::c_void; +use core::fmt; +use windows_enclave::bcrypt::*; +use windows_enclave::vertdll::*; + +/// Cryptographic operation errors +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CryptoError { + /// HRESULT failure from Windows API + Hresult(i32), + /// NTSTATUS failure from BCrypt/NT API + NtStatus(i32), + /// Authentication tag mismatch during decryption + AuthTagMismatch, + /// Data is too short for the expected format + DataTooShort, +} + +impl fmt::Display for CryptoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CryptoError::Hresult(hr) => write!(f, "HRESULT error: 0x{:08x}", hr), + CryptoError::NtStatus(status) => write!(f, "NTSTATUS error: 0x{:08x}", status), + CryptoError::AuthTagMismatch => write!(f, "Authentication tag mismatch"), + CryptoError::DataTooShort => write!(f, "Data too short"), + } + } +} + +impl core::error::Error for CryptoError {} + +/// Sealing identity policy for enclave data sealing +#[derive(Debug, Clone, Copy)] +pub enum EnclaveSealingIdentityPolicy { + /// Seal to enclave identity (SIGNER + PRODUCT_ID + VERSION) + EnclaveIdentity = 1, + /// Seal to signer only (allows version upgrades) + SignerOnly = 2, +} + +// +// Helper functions (public for use by other modules) +// + +/// Check HRESULT and convert to error +pub fn check_hr(hr: i32) -> Result<(), CryptoError> { + if hr < 0 { + return Err(CryptoError::Hresult(hr)); + } + Ok(()) +} + +/// Check NTSTATUS and convert to error +pub fn check_ntstatus(status: i32) -> Result<(), CryptoError> { + if status < 0 { + return Err(CryptoError::NtStatus(status)); + } + Ok(()) +} + +// +// Random number generation +// + +/// Generate cryptographically secure random bytes +/// +/// Uses BCryptGenRandom with BCRYPT_USE_SYSTEM_PREFERRED_RNG flag. +/// This is safe to call from enclaves as it uses the enclave's entropy source. +pub fn generate_random(buffer: &mut [u8]) -> Result<(), CryptoError> { + unsafe { + let status = BCryptGenRandom( + core::ptr::null_mut(), // Use system RNG + buffer.as_mut_ptr(), + buffer.len() as u32, + BCRYPT_USE_SYSTEM_PREFERRED_RNG, + ); + check_ntstatus(status)?; + } + Ok(()) +} + +/// Generate random bytes of the specified length +pub fn generate_random_bytes(count: usize) -> Result, CryptoError> { + let mut buffer = alloc::vec![0u8; count]; + generate_random(&mut buffer)?; + Ok(buffer) +} + +/// Generate a cryptographically secure random u64 +pub fn generate_random_u64() -> Result { + let mut buffer = [0u8; 8]; + generate_random(&mut buffer)?; + Ok(u64::from_le_bytes(buffer)) +} + +/// Generate random bytes suitable for use as a symmetric key +pub fn generate_symmetric_key_bytes(key_size: usize) -> Result, CryptoError> { + generate_random_bytes(key_size) +} + +// +// Enclave sealing +// + +/// Seal data using enclave sealing +/// +/// Encrypts data such that it can only be decrypted by the same enclave +/// (or enclaves with compatible identity based on the sealing policy). +/// +/// # Arguments +/// * `data` - The data to seal +/// * `sealing_policy` - Identity policy for sealing +/// * `runtime_policy` - Runtime policy flags (pass 0 for default) +/// +/// # Returns +/// The sealed data blob +pub fn seal_data( + data: &[u8], + sealing_policy: EnclaveSealingIdentityPolicy, + runtime_policy: u32, +) -> Result, CryptoError> { + unsafe { + // First call to get required size + let mut sealed_size: u32 = 0; + let hr = EnclaveSealData( + data.as_ptr() as *const c_void, + data.len() as u32, + sealing_policy as i32, + runtime_policy, + core::ptr::null_mut(), + 0, + &mut sealed_size, + ); + check_hr(hr)?; + + // Allocate Vec and seal directly into it + let mut sealed_buffer = alloc::vec![0u8; sealed_size as usize]; + + let hr = EnclaveSealData( + data.as_ptr() as *const c_void, + data.len() as u32, + sealing_policy as i32, + runtime_policy, + sealed_buffer.as_mut_ptr() as *mut c_void, + sealed_size, + &mut sealed_size, + ); + check_hr(hr)?; + + Ok(sealed_buffer) + } +} + +/// Unseal data using enclave sealing +/// +/// Decrypts data that was previously sealed to this enclave. +/// +/// # Arguments +/// * `sealed_data` - The sealed data blob +/// +/// # Returns +/// A tuple of (unsealed_data, unseal_flags). Check unseal_flags for stale key flags +/// to determine if the sealing key has changed and the data should be resealed. +pub fn unseal_data(sealed_data: &[u8]) -> Result<(Vec, u32), CryptoError> { + unsafe { + // First call to get required size + let mut unsealed_size: u32 = 0; + let hr = EnclaveUnsealData( + sealed_data.as_ptr() as *const c_void, + sealed_data.len() as u32, + core::ptr::null_mut(), + 0, + &mut unsealed_size, + core::ptr::null_mut(), + core::ptr::null_mut(), + ); + check_hr(hr)?; + + // Allocate Vec and unseal directly into it + let mut unsealed_buffer = alloc::vec![0u8; unsealed_size as usize]; + let mut unseal_flags: u32 = 0; + + let hr = EnclaveUnsealData( + sealed_data.as_ptr() as *const c_void, + sealed_data.len() as u32, + unsealed_buffer.as_mut_ptr() as *mut c_void, + unsealed_size, + &mut unsealed_size, + core::ptr::null_mut(), + &mut unseal_flags, + ); + check_hr(hr)?; + + Ok((unsealed_buffer, unseal_flags)) + } +} + +// +// Nonce utilities +// + +/// Create a nonce buffer from a numeric value +/// +/// Creates a 12-byte nonce with the value placed at the end (little-endian). +pub fn make_nonce_from_number(nonce: u64, nonce_size: usize) -> Vec { + let mut buffer = alloc::vec![0u8; nonce_size]; + if nonce_size >= 8 { + // Place nonce value at the end of the buffer + let nonce_bytes = nonce.to_le_bytes(); + buffer[nonce_size - 8..].copy_from_slice(&nonce_bytes); + } + buffer +} + +// +// Symmetric Key Handle (RAII wrapper) +// + +/// RAII wrapper for BCrypt symmetric key handle +pub struct SymmetricKeyHandle { + handle: BCRYPT_KEY_HANDLE, +} + +impl SymmetricKeyHandle { + /// Create a symmetric key from raw key bytes using AES-GCM + pub fn from_bytes(key_bytes: &[u8]) -> Result { + let mut handle: BCRYPT_KEY_HANDLE = core::ptr::null_mut(); + + unsafe { + let status = BCryptGenerateSymmetricKey( + BCRYPT_AES_GCM_ALG_HANDLE, + &mut handle, + core::ptr::null_mut(), // No key object buffer needed + 0, + key_bytes.as_ptr(), + key_bytes.len() as u32, + 0, + ); + check_ntstatus(status)?; + } + + Ok(Self { handle }) + } + + /// Get the raw handle for use with BCrypt functions + pub fn handle(&self) -> BCRYPT_KEY_HANDLE { + self.handle + } +} + +impl Drop for SymmetricKeyHandle { + fn drop(&mut self) { + if !self.handle.is_null() { + unsafe { + let _ = BCryptDestroyKey(self.handle); + } + } + } +} + +// +// AES-GCM Encryption / Decryption +// + +/// Encrypt data using AES-GCM +/// +/// # Arguments +/// * `key` - The symmetric key handle +/// * `plaintext` - Data to encrypt +/// * `nonce` - Nonce (IV) bytes +/// * `tag_size` - Size of authentication tag in bytes +/// +/// # Returns +/// A tuple of (ciphertext, tag) +pub fn encrypt( + key: &SymmetricKeyHandle, + plaintext: &[u8], + nonce: &[u8], + tag_size: usize, +) -> Result<(Vec, Vec), CryptoError> { + let mut tag = alloc::vec![0u8; tag_size]; + let mut nonce_copy = nonce.to_vec(); + + // AES-GCM: ciphertext length equals plaintext length + let mut ciphertext = alloc::vec![0u8; plaintext.len()]; + let mut result_size: u32 = 0; + + let cipher_info = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO::for_encrypt(&mut nonce_copy, &mut tag); + + unsafe { + let status = BCryptEncrypt( + key.handle(), + plaintext.as_ptr(), + plaintext.len() as u32, + &cipher_info as *const _ as *const c_void, + core::ptr::null_mut(), // No IV buffer (using nonce in cipher_info) + 0, + ciphertext.as_mut_ptr(), + ciphertext.len() as u32, + &mut result_size, + 0, + ); + check_ntstatus(status)?; + } + + Ok((ciphertext, tag)) +} + +/// Decrypt data using AES-GCM +/// +/// # Arguments +/// * `key` - The symmetric key handle +/// * `ciphertext` - Data to decrypt +/// * `nonce` - Nonce (IV) bytes used during encryption +/// * `tag` - Authentication tag from encryption +/// +/// # Returns +/// The decrypted plaintext +pub fn decrypt( + key: &SymmetricKeyHandle, + ciphertext: &[u8], + nonce: &[u8], + tag: &[u8], +) -> Result, CryptoError> { + // AES-GCM: plaintext length equals ciphertext length + let mut plaintext = alloc::vec![0u8; ciphertext.len()]; + let mut result_size: u32 = 0; + + let cipher_info = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO::for_decrypt(nonce, tag); + + unsafe { + let status = BCryptDecrypt( + key.handle(), + ciphertext.as_ptr(), + ciphertext.len() as u32, + &cipher_info as *const _ as *const c_void, + core::ptr::null_mut(), // No IV buffer (using nonce in cipher_info) + 0, + plaintext.as_mut_ptr(), + plaintext.len() as u32, + &mut result_size, + 0, + ); + + // STATUS_AUTH_TAG_MISMATCH = 0xC000A002 + if status == -1073700862i32 { + return Err(CryptoError::AuthTagMismatch); + } + check_ntstatus(status)?; + } + + Ok(plaintext) +} + +/// Encrypt data and append the tag to the output +/// +/// # Arguments +/// * `key` - The symmetric key handle +/// * `plaintext` - Data to encrypt +/// * `nonce` - Nonce (IV) bytes +/// * `tag_size` - Size of authentication tag in bytes +/// +/// # Returns +/// Combined output: [ciphertext][tag] +pub fn encrypt_and_tag( + key: &SymmetricKeyHandle, + plaintext: &[u8], + nonce: &[u8], + tag_size: usize, +) -> Result, CryptoError> { + let (ciphertext, tag) = encrypt(key, plaintext, nonce, tag_size)?; + + let mut combined = Vec::with_capacity(ciphertext.len() + tag_size); + combined.extend_from_slice(&ciphertext); + combined.extend_from_slice(&tag); + + Ok(combined) +} + +/// Decrypt data that has the tag appended +/// +/// # Arguments +/// * `key` - The symmetric key handle +/// * `combined` - Combined input: [ciphertext][tag] +/// * `nonce` - Nonce (IV) bytes used during encryption +/// * `tag_size` - Size of authentication tag in bytes +/// +/// # Returns +/// The decrypted plaintext +pub fn decrypt_and_untag( + key: &SymmetricKeyHandle, + combined: &[u8], + nonce: &[u8], + tag_size: usize, +) -> Result, CryptoError> { + if combined.len() < tag_size { + return Err(CryptoError::DataTooShort); + } + + let ciphertext_len = combined.len() - tag_size; + let ciphertext = &combined[..ciphertext_len]; + let tag = &combined[ciphertext_len..]; + + decrypt(key, ciphertext, nonce, tag) +} + +/// Encrypt data with a zero nonce and append tag +/// +/// Uses a zero nonce (all zeros). Only safe when each key is used once. +pub fn encrypt_and_tag_zero_nonce( + key: &SymmetricKeyHandle, + plaintext: &[u8], + nonce_size: usize, + tag_size: usize, +) -> Result, CryptoError> { + let zero_nonce = alloc::vec![0u8; nonce_size]; + encrypt_and_tag(key, plaintext, &zero_nonce, tag_size) +} + +/// Decrypt data with a zero nonce that has tag appended +/// +/// Uses a zero nonce (all zeros). +pub fn decrypt_and_untag_zero_nonce( + key: &SymmetricKeyHandle, + combined: &[u8], + nonce_size: usize, + tag_size: usize, +) -> Result, CryptoError> { + let zero_nonce = alloc::vec![0u8; nonce_size]; + decrypt_and_untag(key, combined, &zero_nonce, tag_size) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_make_nonce_from_number() { + let nonce = make_nonce_from_number(0x0102030405060708, 12); + assert_eq!(nonce[0..4], [0, 0, 0, 0]); + assert_eq!( + nonce[4..12], + [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01] + ); + } +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/common/mod.rs b/rust/sdk/crates/libs/sdk-enclave/src/common/mod.rs new file mode 100644 index 00000000..804d25e1 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/common/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Common utilities for VTL1 enclave operations +//! +//! This module provides general-purpose utilities that can be used across +//! different SDK modules for enclave operations. + +pub mod crypto; +pub mod sdk_impls; +mod utils; + +pub use crypto::CryptoError; +pub use sdk_impls::EnclaveImpl; +pub use utils::*; diff --git a/rust/sdk/crates/libs/sdk-enclave/src/common/sdk_impls.rs b/rust/sdk/crates/libs/sdk-enclave/src/common/sdk_impls.rs new file mode 100644 index 00000000..0ea9319b --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/common/sdk_impls.rs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! SDK implementation of the EDL Trusted trait. +//! +//! This module provides the `EnclaveImpl` struct that implements the generated +//! `Trusted` trait by delegating to free functions in feature modules. +//! This pattern allows feature implementations to live in their own modules +//! while the trait impl acts as a thin wrapper. + +use alloc::vec::Vec; +use sdk_enclave_gen::AbiError; +use sdk_enclave_gen::implementation::trusted::Trusted; +use sdk_enclave_gen::implementation::types::attestationReportAndSessionInfo; + +use crate::userboundkey; + +/// SDK enclave implementation of the Trusted trait. +/// +/// This struct implements all EDL trusted functions by delegating to +/// free functions in the appropriate feature modules. +pub struct EnclaveImpl; + +#[allow(non_snake_case)] +impl Trusted for EnclaveImpl { + fn userboundkey_get_attestation_report( + challenge: &Vec, + ) -> Result { + userboundkey::userboundkey_get_attestation_report(challenge) + } + + fn userboundkey_close_session(sessionInfo: u64) -> Result<(), AbiError> { + userboundkey::userboundkey_close_session(sessionInfo) + } +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/common/utils.rs b/rust/sdk/crates/libs/sdk-enclave/src/common/utils.rs new file mode 100644 index 00000000..9f73b455 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/common/utils.rs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Utility functions for VTL1 enclave operations +//! +//! This module provides general-purpose utilities for enclave operations +//! that can be used across different SDK modules. + +use core::mem; +use spin::Once; + +use windows_enclave::vertdll::{ + ENCLAVE_FLAG_DYNAMIC_DEBUG_ACTIVE, ENCLAVE_FLAG_DYNAMIC_DEBUG_ENABLED, + ENCLAVE_FLAG_FULL_DEBUG_ENABLED, ENCLAVE_INFORMATION, EnclaveGetEnclaveInformation, +}; + +/// Common error type for enclave utility operations +#[derive(Debug, Clone, Copy)] +pub enum EnclaveUtilsError { + /// Windows HRESULT error + Hresult(i32), +} + +impl EnclaveUtilsError { + /// Convert to HRESULT for compatibility + pub fn to_hresult(self) -> i32 { + match self { + EnclaveUtilsError::Hresult(hr) => hr, + } + } +} + +/// Wrapper for ENCLAVE_INFORMATION that implements Send + Sync. +/// SAFETY: The BaseAddress pointer is read-only after initialization +/// and points to enclave memory which is valid for the enclave's lifetime. +#[derive(Clone, Copy)] +struct SyncEnclaveInfo(ENCLAVE_INFORMATION); + +// SAFETY: ENCLAVE_INFORMATION contains a *mut c_void (BaseAddress) but: +// 1. It's effectively read-only after initialization +// 2. It points to enclave memory mapped for the process lifetime +// 3. We only ever read from it, never write through it +unsafe impl Send for SyncEnclaveInfo {} +unsafe impl Sync for SyncEnclaveInfo {} + +/// Cached enclave information (initialized once) +static ENCLAVE_INFO: Once> = Once::new(); + +/// Get enclave information, initializing and caching it on first access. +/// +/// - Thread-safe +/// - No heap allocation +/// - No atomic pointer management +/// - Error is cached to avoid repeated syscalls +pub fn get_enclave_information() -> Result { + ENCLAVE_INFO + .call_once(|| { + let mut info: ENCLAVE_INFORMATION = unsafe { mem::zeroed() }; + + let hr = unsafe { + EnclaveGetEnclaveInformation( + mem::size_of::() as u32, + &mut info, + ) + }; + + if hr < 0 { + return Err(EnclaveUtilsError::Hresult(hr)); + } + + Ok(SyncEnclaveInfo(info)) + }) + .as_ref() + .map(|s| s.0) + .map_err(|e| *e) +} + +/// Get the base address of the enclave +/// +/// Returns the enclave's base address as a pointer, suitable for passing +/// to APIs that require an enclave identifier. +pub fn get_enclave_base_address() -> Result<*mut core::ffi::c_void, EnclaveUtilsError> { + let info = get_enclave_information()?; + Ok(info.BaseAddress) +} + +/// Get the base address of the enclave as a u64 +/// +/// Convenience function that returns the base address as a u64, +/// suitable for EDL interface calls. +pub fn get_enclave_base_address_u64() -> Result { + Ok(get_enclave_base_address()? as u64) +} + +/// Check if full debug is enabled for the enclave +#[allow(dead_code)] +pub fn is_enclave_full_debug_enabled() -> Result { + let info = get_enclave_information()?; + Ok((info.Identity.Flags & ENCLAVE_FLAG_FULL_DEBUG_ENABLED) != 0) +} + +/// Check if dynamic debug is enabled for the enclave +#[allow(dead_code)] +pub fn is_enclave_dynamic_debug_enabled() -> Result { + let info = get_enclave_information()?; + Ok((info.Identity.Flags & ENCLAVE_FLAG_DYNAMIC_DEBUG_ENABLED) != 0) +} + +/// Check if dynamic debug is currently active for the enclave +#[allow(dead_code)] +pub fn is_enclave_dynamic_debug_active() -> Result { + let info = get_enclave_information()?; + Ok((info.Identity.Flags & ENCLAVE_FLAG_DYNAMIC_DEBUG_ACTIVE) != 0) +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/lib.rs b/rust/sdk/crates/libs/sdk-enclave/src/lib.rs index 5aa5786a..a0a35327 100644 --- a/rust/sdk/crates/libs/sdk-enclave/src/lib.rs +++ b/rust/sdk/crates/libs/sdk-enclave/src/lib.rs @@ -5,3 +5,38 @@ extern crate alloc; mod enclave_ffi; + +pub mod common; +pub mod userboundkey; + +// Re-export the SDK generated enclave crate. +// This is used internally by the export_sdk_enclave_functions! macro. +pub use sdk_enclave_gen; + +/// Export all SDK enclave functions. +/// +/// Call this macro once in your enclave's lib.rs to enable SDK features. +/// This exports the necessary callback registration and trusted functions +/// that the SDK needs to communicate with VTL0. +/// +/// # Example +/// +/// ```rust,ignore +/// // In your enclave's lib.rs: +/// vbsenclave_sdk_enclave::export_sdk_enclave_functions!(); +/// ``` +/// +/// This is equivalent to manually exporting each SDK module's functions, +/// but hides the internal implementation details. +#[macro_export] +macro_rules! export_sdk_enclave_functions { + () => { + // Export SDK enclave functions in a private module + // to avoid naming conflicts with the application's own exports + mod __sdk_exports { + $crate::sdk_enclave_gen::export_enclave_functions!($crate::common::EnclaveImpl); + } + + // Future SDK features will be added here as additional modules + }; +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/crypto.rs b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/crypto.rs new file mode 100644 index 00000000..ac88ae1f --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/crypto.rs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! User-bound key specific cryptographic constants and utilities. +//! +//! This module provides UBK-specific constants and convenience wrappers over +//! `common::crypto` that use UBK-specific sizes and error types. + +use crate::common::crypto; +use crate::userboundkey::types::UserBoundKeyError; +use alloc::{string::ToString, vec::Vec}; + +// Re-export from common +pub use crypto::{ + EnclaveSealingIdentityPolicy, SymmetricKeyHandle, check_hr, generate_symmetric_key_bytes, + seal_data, unseal_data, +}; + +// +// UBK-specific constants +// + +/// Size of symmetric keys in bytes (AES-256) +pub const SYMMETRIC_KEY_SIZE_BYTES: usize = 32; + +/// Size of Diffie-Hellman keys in bits +#[allow(dead_code)] +pub const DH_KEY_SIZE_BITS: u32 = 384; + +/// Size of nonce for AES-GCM in bytes +pub const NONCE_SIZE: usize = 12; + +/// Size of authentication tag for AES-GCM in bytes +pub const TAG_SIZE: usize = 16; + +/// Flag indicating that the unsealing key is stale and data should be resealed +pub const ENCLAVE_UNSEAL_FLAG_STALE_KEY: u32 = 0x00000001; + +/// Zero nonce for AES-GCM (all zeros) +pub const ZERO_NONCE: [u8; NONCE_SIZE] = [0u8; NONCE_SIZE]; + +// +// UBK-specific functions +// + +/// Check if unseal flags indicate a stale key +#[inline] +pub fn is_stale_key(unseal_flags: u32) -> bool { + (unseal_flags & ENCLAVE_UNSEAL_FLAG_STALE_KEY) != 0 +} + +/// Encrypt data using AES-GCM with UBK constants. +/// +/// This is a convenience wrapper over `common::crypto::encrypt` that uses +/// UBK-specific tag size and returns `UserBoundKeyError`. +pub fn encrypt( + key: &SymmetricKeyHandle, + plaintext: &[u8], + nonce: &[u8], +) -> Result<(Vec, Vec), UserBoundKeyError> { + crypto::encrypt(key, plaintext, nonce, TAG_SIZE) + .map_err(|e| UserBoundKeyError::CryptoError(e.to_string())) +} + +/// Decrypt data using AES-GCM. +/// +/// This is a convenience wrapper over `common::crypto::decrypt` that returns +/// `UserBoundKeyError`. +pub fn decrypt( + key: &SymmetricKeyHandle, + ciphertext: &[u8], + nonce: &[u8], + tag: &[u8], +) -> Result, UserBoundKeyError> { + crypto::decrypt(key, ciphertext, nonce, tag).map_err(|e| { + let error_str = e.to_string(); + if error_str.contains("Authentication tag mismatch") { + UserBoundKeyError::InvalidData("Authentication tag mismatch") + } else { + UserBoundKeyError::CryptoError(error_str) + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_stale_key() { + assert!(!is_stale_key(0)); + assert!(is_stale_key(ENCLAVE_UNSEAL_FLAG_STALE_KEY)); + assert!(is_stale_key(ENCLAVE_UNSEAL_FLAG_STALE_KEY | 0x10)); + } +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/mod.rs b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/mod.rs new file mode 100644 index 00000000..7410d020 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/mod.rs @@ -0,0 +1,714 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! VTL1 User-Bound Key Implementation +//! +//! Implements enclave-side user-bound key operations using VBS enclave sealing +//! and the veinterop.dll APIs for KCM (Key Credential Manager) interactions. + +mod crypto; +mod types; + +pub use crypto::{ + EnclaveSealingIdentityPolicy, NONCE_SIZE, SYMMETRIC_KEY_SIZE_BYTES, SymmetricKeyHandle, + TAG_SIZE, ZERO_NONCE, decrypt, encrypt, +}; +pub use types::*; + +// Re-export the cache config type for use by samples +pub use sdk_enclave_gen::implementation::types::keyCredentialCacheConfig; + +// Re-export widestring types for public API consumers +pub use widestring::U16Str; + +use alloc::vec::Vec; + +use sdk_enclave_gen::AbiError; +use sdk_enclave_gen::implementation::types::attestationReportAndSessionInfo; +use sdk_enclave_gen::stubs::untrusted; +use widestring::U16String; + +use windows_enclave::veinterop::{ + CloseUserBoundKeyAuthContext, CloseUserBoundKeySession, + CreateUserBoundKeyRequestForDeriveSharedSecret, + CreateUserBoundKeyRequestForRetrieveAuthorizationContext, GetUserBoundKeyAuthContext, + InitializeUserBoundKeySession, ProtectUserBoundKey, USER_BOUND_KEY_AUTH_CONTEXT_HANDLE, + USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY, USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY_NAME, + USER_BOUND_KEY_SESSION_HANDLE, UnprotectUserBoundKey, ValidateUserBoundKeyAuthContext, +}; +use windows_enclave::vertdll::{GetProcessHeap, HeapFree}; + +use crate::common::get_enclave_base_address_u64; +use crypto::{check_hr, generate_symmetric_key_bytes, is_stale_key, seal_data, unseal_data}; + +// BCrypt algorithm pseudo-handle value from Windows SDK bcrypt.h. +// BCRYPT_ECDH_P384_ALG_HANDLE = ((BCRYPT_ALG_HANDLE) 0x000002b1) +// Stored as u64 for EDL interface compatibility. +const BCRYPT_ECDH_P384_ALG_HANDLE_U64: u64 = 0x000002b1; + +/// RAII wrapper for heap-allocated buffers returned by veinterop APIs. +/// Automatically frees the buffer via HeapFree when dropped. +struct HeapBuffer { + ptr: *mut core::ffi::c_void, + size: u32, +} + +impl HeapBuffer { + /// Takes ownership of a heap-allocated buffer. + /// + /// # Safety + /// The pointer must have been allocated via GetProcessHeap/HeapAlloc + /// and the size must be accurate. + unsafe fn from_raw(ptr: *mut core::ffi::c_void, size: u32) -> Self { + Self { ptr, size } + } + + /// Returns true if the buffer is null or empty. + fn is_empty(&self) -> bool { + self.ptr.is_null() || self.size == 0 + } + + /// Copies the buffer contents to a Vec. + fn to_vec(&self) -> Vec { + if self.is_empty() { + return Vec::new(); + } + unsafe { core::slice::from_raw_parts(self.ptr as *const u8, self.size as usize).to_vec() } + } +} + +impl Drop for HeapBuffer { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + let heap = GetProcessHeap(); + HeapFree(heap, 0, self.ptr); + } + } + } +} + +/// RAII wrapper for session handle +struct UniqueSessionHandle(USER_BOUND_KEY_SESSION_HANDLE); + +impl UniqueSessionHandle { + fn new(handle: USER_BOUND_KEY_SESSION_HANDLE) -> Self { + Self(handle) + } + + fn get(&self) -> USER_BOUND_KEY_SESSION_HANDLE { + self.0 + } + + #[allow(dead_code)] + fn release(&mut self) -> USER_BOUND_KEY_SESSION_HANDLE { + let handle = self.0; + self.0 = core::ptr::null_mut(); + handle + } +} + +impl Drop for UniqueSessionHandle { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { + let _ = CloseUserBoundKeySession(self.0); + } + } + } +} + +/// RAII wrapper for auth context handle +struct UniqueAuthContextHandle(USER_BOUND_KEY_AUTH_CONTEXT_HANDLE); + +impl UniqueAuthContextHandle { + fn get(&self) -> USER_BOUND_KEY_AUTH_CONTEXT_HANDLE { + self.0 + } +} + +impl Drop for UniqueAuthContextHandle { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { + let _ = CloseUserBoundKeyAuthContext(self.0); + } + } + } +} + +/// RAII wrapper for credential (calls VTL0 to delete) +struct UniqueCredential(u64); + +impl UniqueCredential { + fn new(credential: u64) -> Self { + Self(credential) + } + + fn get(&self) -> u64 { + self.0 + } + + #[allow(dead_code)] + fn release(&mut self) -> u64 { + let credential = self.0; + self.0 = 0; + credential + } +} + +impl Drop for UniqueCredential { + fn drop(&mut self) { + if self.0 != 0 { + let _ = untrusted::userboundkey_delete_credential(self.0); + } + } +} + +/// Get ephemeral public key bytes from bound key structure +/// +/// The bound key structure format: +/// - [4 bytes] enclave public key blob size +/// - [N bytes] enclave public key blob (ephemeral public key) +fn get_ephemeral_public_key_bytes_from_bound_key( + bound_key_bytes: &[u8], +) -> Result, UserBoundKeyError> { + if bound_key_bytes.len() < 4 { + return Err(UserBoundKeyError::InvalidData("Bound key too small")); + } + + let public_key_size = u32::from_le_bytes([ + bound_key_bytes[0], + bound_key_bytes[1], + bound_key_bytes[2], + bound_key_bytes[3], + ]) as usize; + + let remaining = bound_key_bytes.len() - 4; + if remaining < public_key_size || public_key_size == 0 { + return Err(UserBoundKeyError::InvalidData("Invalid public key size")); + } + + Ok(bound_key_bytes[4..4 + public_key_size].to_vec()) +} + +/// Validate that formatted key name ends with expected suffix (wide string version) +fn validate_formatted_key_name_wide( + formatted_key_name: &U16Str, + original_key_name: &U16Str, +) -> Result<(), UserBoundKeyError> { + // Build expected suffix: "//" + original_key_name + let slash_slash = U16String::from_str("//"); + let expected_suffix_len = 2 + original_key_name.len(); + + if formatted_key_name.len() < expected_suffix_len { + return Err(UserBoundKeyError::SecurityViolation( + "Formatted key name does not match expected pattern", + )); + } + + // Check if formatted_key_name ends with "//{original_key_name}" + let suffix_start = formatted_key_name.len() - expected_suffix_len; + let formatted_suffix = &formatted_key_name.as_slice()[suffix_start..]; + + if formatted_suffix[0..2] != slash_slash.as_slice()[..] + || formatted_suffix[2..] != *original_key_name.as_slice() + { + return Err(UserBoundKeyError::SecurityViolation( + "Formatted key name does not match expected pattern", + )); + } + Ok(()) +} + +/// Create a new user-bound key +/// +/// This function: +/// 1. Generates a symmetric key +/// 2. Creates a Windows Hello credential with VBS attestation +/// 3. Protects the key using the KCM auth context +/// 4. Seals the protected key to the enclave +/// +/// # Arguments +/// * `key_name` - Name for the Windows Hello credential (UTF-16 wide string without null terminator) +/// * `cache_config` - Cache configuration for the credential +/// * `message` - Message to display during Windows Hello prompt (UTF-16 wide string without null terminator) +/// * `window_id` - Window ID for the prompt +/// * `sealing_policy` - Enclave sealing identity policy +/// * `runtime_policy` - Runtime policy flags +/// * `key_credential_creation_option` - Credential creation options +/// +/// # Returns +/// Sealed key material that can be stored and later loaded +pub fn create_user_bound_key( + key_name: &U16Str, + cache_config: &keyCredentialCacheConfig, + message: &U16Str, + window_id: u64, + sealing_policy: EnclaveSealingIdentityPolicy, + runtime_policy: u32, + key_credential_creation_option: u32, +) -> Result, UserBoundKeyError> { + // Generate symmetric key + let user_key_bytes = generate_symmetric_key_bytes(SYMMETRIC_KEY_SIZE_BYTES)?; + + create_user_bound_key_with_custom_key( + key_name, + cache_config, + message, + window_id, + sealing_policy, + runtime_policy, + key_credential_creation_option, + &user_key_bytes, + ) +} + +/// Create a user-bound key with custom key bytes +#[allow(clippy::too_many_arguments)] +pub fn create_user_bound_key_with_custom_key( + key_name: &U16Str, + cache_config: &keyCredentialCacheConfig, + message: &U16Str, + window_id: u64, + sealing_policy: EnclaveSealingIdentityPolicy, + runtime_policy: u32, + key_credential_creation_option: u32, + custom_key_bytes: &[u8], +) -> Result, UserBoundKeyError> { + // Format the key name (calls VTL0) - this is the FIRST VTL0 call + // Create WString from key_name slice + let key_name_wstring = sdk_enclave_gen::implementation::types::edl::WString { + wchars: key_name.as_slice().to_vec(), + }; + let formatted_key_name_wstring = untrusted::userboundkey_format_key_name(&key_name_wstring) + .map_err(UserBoundKeyError::AbiError)?; + + // Security validation (wide string version) + let formatted_key_name_u16str = U16Str::from_slice(&formatted_key_name_wstring.wchars); + validate_formatted_key_name_wide(formatted_key_name_u16str, key_name)?; + + // Get enclave base address + let enclave_ptr = get_enclave_base_address_u64()?; + + // Create WString for message + let message_wstring = sdk_enclave_gen::implementation::types::edl::WString { + wchars: message.as_slice().to_vec(), + }; + + // Establish session (calls VTL0 which triggers Windows Hello) + // Requires at least 2 enclave threads - one for this call and one for the callback + let credential_and_session = untrusted::userboundkey_establish_session_for_create( + enclave_ptr, + &key_name_wstring, + BCRYPT_ECDH_P384_ALG_HANDLE_U64, + &message_wstring, + window_id, + cache_config, + key_credential_creation_option, + ) + .map_err(UserBoundKeyError::AbiError)?; + + // RAII wrappers for cleanup + let credential = UniqueCredential::new(credential_and_session.credential); + let session_handle = UniqueSessionHandle::new( + credential_and_session.sessionInfo as USER_BOUND_KEY_SESSION_HANDLE, + ); + + // Create encrypted KCM request for RetrieveAuthorizationContext + let mut encrypted_request_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut encrypted_request_size: u32 = 0; + let mut local_nonce: u64 = 0; + + // Create null-terminated wide string for Windows API + let formatted_key_name_with_null = U16String::from(formatted_key_name_u16str); + + unsafe { + let hr = CreateUserBoundKeyRequestForRetrieveAuthorizationContext( + session_handle.get(), + formatted_key_name_with_null.as_ptr(), + &mut local_nonce, + &mut encrypted_request_ptr, + &mut encrypted_request_size, + ); + check_hr(hr)?; + } + + // Take ownership of the heap-allocated buffer + let encrypted_request_buf = + unsafe { HeapBuffer::from_raw(encrypted_request_ptr, encrypted_request_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if encrypted_request_buf.is_empty() { + return Err(UserBoundKeyError::InvalidData("API returned empty request")); + } + + // Convert to Vec (HeapBuffer will free on drop) + let encrypted_request = encrypted_request_buf.to_vec(); + + // Get authorization context from credential (calls VTL0) + let auth_context_blob = untrusted::userboundkey_get_authorization_context_from_credential( + credential.get(), + &encrypted_request, + &message_wstring, + window_id, + ) + .map_err(UserBoundKeyError::AbiError)?; + + // Get auth context handle + let mut auth_context_handle: USER_BOUND_KEY_AUTH_CONTEXT_HANDLE = core::ptr::null_mut(); + unsafe { + let hr = GetUserBoundKeyAuthContext( + session_handle.get(), + auth_context_blob.as_ptr() as *const core::ffi::c_void, + auth_context_blob.len() as u32, + local_nonce, + &mut auth_context_handle, + ); + check_hr(hr)?; + } + let auth_context = UniqueAuthContextHandle(auth_context_handle); + + // Validate auth context + let prop = USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY { + name: USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY_NAME::UserBoundKeyAuthContextPropertyCacheConfig, + size: core::mem::size_of::() as u32, + value: cache_config as *const _ as *mut core::ffi::c_void, + }; + + unsafe { + let hr = ValidateUserBoundKeyAuthContext( + formatted_key_name_with_null.as_ptr(), + auth_context.get(), + 1, + &prop, + ); + check_hr(hr)?; + } + + // Protect the user key + let mut bound_key_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut bound_key_size: u32 = 0; + + unsafe { + let hr = ProtectUserBoundKey( + auth_context.get(), + custom_key_bytes.as_ptr() as *const core::ffi::c_void, + custom_key_bytes.len() as u32, + &mut bound_key_ptr, + &mut bound_key_size, + ); + check_hr(hr)?; + } + + // Take ownership of the heap-allocated buffer + let bound_key_buf = unsafe { HeapBuffer::from_raw(bound_key_ptr, bound_key_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if bound_key_buf.is_empty() { + return Err(UserBoundKeyError::InvalidData( + "API returned empty bound key", + )); + } + + // Convert bound key to Vec (HeapBuffer will free on drop) + let bound_key_bytes = bound_key_buf.to_vec(); + + // Seal the bound key + let sealed_key = seal_data(&bound_key_bytes, sealing_policy, runtime_policy)?; + + Ok(sealed_key) +} + +/// Load a previously created user-bound key +/// +/// # Arguments +/// * `key_name` - Name of the Windows Hello credential (UTF-16 wide string without null terminator) +/// * `cache_config` - Expected cache configuration +/// * `message` - Message to display during Windows Hello prompt (UTF-16 wide string without null terminator) +/// * `window_id` - Window ID for the prompt +/// * `sealed_bound_key_bytes` - Previously sealed key material +/// +/// # Returns +/// A tuple of (decrypted user key bytes, needs_reseal flag). +/// If `needs_reseal` is true, the caller should reseal the key with [`reseal_user_bound_key`]. +pub fn load_user_bound_key( + key_name: &U16Str, + cache_config: &keyCredentialCacheConfig, + message: &U16Str, + window_id: u64, + sealed_bound_key_bytes: &[u8], +) -> Result<(Vec, bool), UserBoundKeyError> { + // Unseal the bound key + let (bound_key_bytes, unseal_flags) = unseal_data(sealed_bound_key_bytes)?; + + // Check if key is stale + if is_stale_key(unseal_flags) { + return Err(UserBoundKeyError::StaleKey); + } + + // Extract ephemeral public key from bound key structure + let ephemeral_public_key = get_ephemeral_public_key_bytes_from_bound_key(&bound_key_bytes)?; + + // Format the key name (calls VTL0) + let key_name_wstring = sdk_enclave_gen::implementation::types::edl::WString { + wchars: key_name.as_slice().to_vec(), + }; + let formatted_key_name_wstring = untrusted::userboundkey_format_key_name(&key_name_wstring) + .map_err(UserBoundKeyError::AbiError)?; + + // Security validation (wide string version) + let formatted_key_name_u16str = U16Str::from_slice(&formatted_key_name_wstring.wchars); + validate_formatted_key_name_wide(formatted_key_name_u16str, key_name)?; + + // Get enclave base address + let enclave_ptr = get_enclave_base_address_u64()?; + + // Create WString for message + let message_wstring = sdk_enclave_gen::implementation::types::edl::WString { + wchars: message.as_slice().to_vec(), + }; + + // Establish session for load (calls VTL0) + let credential_and_session = untrusted::userboundkey_establish_session_for_load( + enclave_ptr, + &key_name_wstring, + &message_wstring, + window_id, + ) + .map_err(UserBoundKeyError::AbiError)?; + + // RAII wrappers + let credential = UniqueCredential::new(credential_and_session.credential); + let session_handle = UniqueSessionHandle::new( + credential_and_session.sessionInfo as USER_BOUND_KEY_SESSION_HANDLE, + ); + + // Create encrypted request for RetrieveAuthorizationContext + let mut encrypted_rac_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut encrypted_rac_size: u32 = 0; + let mut local_nonce: u64 = 0; + + // Create null-terminated wide string for Windows API + let formatted_key_name_with_null = U16String::from(formatted_key_name_u16str); + + unsafe { + let hr = CreateUserBoundKeyRequestForRetrieveAuthorizationContext( + session_handle.get(), + formatted_key_name_with_null.as_ptr(), + &mut local_nonce, + &mut encrypted_rac_ptr, + &mut encrypted_rac_size, + ); + check_hr(hr)?; + } + + // Take ownership of the heap-allocated buffer + let encrypted_rac_buf = unsafe { HeapBuffer::from_raw(encrypted_rac_ptr, encrypted_rac_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if encrypted_rac_buf.is_empty() { + return Err(UserBoundKeyError::InvalidData("API returned empty request")); + } + + // Convert to Vec (HeapBuffer will free on drop) + let encrypted_rac_request = encrypted_rac_buf.to_vec(); + + // Get authorization context (calls VTL0) + let auth_context_blob = untrusted::userboundkey_get_authorization_context_from_credential( + credential.get(), + &encrypted_rac_request, + &message_wstring, + window_id, + ) + .map_err(UserBoundKeyError::AbiError)?; + + // Get auth context handle + let mut auth_context_handle: USER_BOUND_KEY_AUTH_CONTEXT_HANDLE = core::ptr::null_mut(); + unsafe { + let hr = GetUserBoundKeyAuthContext( + session_handle.get(), + auth_context_blob.as_ptr() as *const core::ffi::c_void, + auth_context_blob.len() as u32, + local_nonce, + &mut auth_context_handle, + ); + check_hr(hr)?; + } + let auth_context = UniqueAuthContextHandle(auth_context_handle); + + // Validate auth context + let prop = USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY { + name: USER_BOUND_KEY_AUTH_CONTEXT_PROPERTY_NAME::UserBoundKeyAuthContextPropertyCacheConfig, + size: core::mem::size_of::() as u32, + value: cache_config as *const _ as *mut core::ffi::c_void, + }; + + unsafe { + let hr = ValidateUserBoundKeyAuthContext( + formatted_key_name_with_null.as_ptr(), + auth_context.get(), + 1, + &prop, + ); + check_hr(hr)?; + } + + // Create encrypted request for DeriveSharedSecret + let mut encrypted_dss_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut encrypted_dss_size: u32 = 0; + + unsafe { + let hr = CreateUserBoundKeyRequestForDeriveSharedSecret( + session_handle.get(), + formatted_key_name_with_null.as_ptr(), + ephemeral_public_key.as_ptr() as *const core::ffi::c_void, + ephemeral_public_key.len() as u32, + &mut local_nonce, + &mut encrypted_dss_ptr, + &mut encrypted_dss_size, + ); + check_hr(hr)?; + } + + // Take ownership of the heap-allocated buffer + let encrypted_dss_buf = unsafe { HeapBuffer::from_raw(encrypted_dss_ptr, encrypted_dss_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if encrypted_dss_buf.is_empty() { + return Err(UserBoundKeyError::InvalidData("API returned empty request")); + } + + // Convert to Vec (HeapBuffer will free on drop) + let encrypted_dss_request = encrypted_dss_buf.to_vec(); + + // Get secret from credential (calls VTL0 - prompts for Windows Hello) + let secret = untrusted::userboundkey_get_secret_from_credential( + credential.get(), + &encrypted_dss_request, + &message_wstring, + window_id, + ) + .map_err(UserBoundKeyError::AbiError)?; + + // Unprotect the user key + let mut user_key_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut user_key_size: u32 = 0; + + unsafe { + let hr = UnprotectUserBoundKey( + session_handle.get(), + auth_context.get(), + secret.as_ptr() as *const core::ffi::c_void, + secret.len() as u32, + bound_key_bytes.as_ptr() as *const core::ffi::c_void, + bound_key_bytes.len() as u32, + local_nonce, + &mut user_key_ptr, + &mut user_key_size, + ); + check_hr(hr)?; + } + + // Take ownership of the heap-allocated buffer + let user_key_buf = unsafe { HeapBuffer::from_raw(user_key_ptr, user_key_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if user_key_buf.is_empty() { + return Err(UserBoundKeyError::InvalidData( + "API returned empty user key", + )); + } + + // Convert to Vec (HeapBuffer will free on drop) + let user_key_bytes = user_key_buf.to_vec(); + + Ok((user_key_bytes, false)) +} + +/// Reseal a user-bound key with current enclave identity +/// +/// This function re-seals data that was previously sealed by an older enclave version. +/// It is used when the sealing key has rotated (detected by `is_stale_key` flag from unsealing). +/// +/// Note: The input `bound_key_bytes` should be the unsealed (decrypted) bound key data, +/// NOT the sealed blob. If you have sealed data, first unseal it with `unseal_data` +/// from the crypto module, then pass the unsealed result here. +/// +/// # Arguments +/// * `bound_key_bytes` - Previously unsealed bound key bytes (NOT sealed data) +/// * `sealing_policy` - Enclave sealing identity policy for the new seal +/// * `runtime_policy` - Runtime policy flags +/// +/// # Returns +/// Newly sealed key material that can be stored and used with future loads +pub fn reseal_user_bound_key( + bound_key_bytes: &[u8], + sealing_policy: EnclaveSealingIdentityPolicy, + runtime_policy: u32, +) -> Result, UserBoundKeyError> { + // Re-seal with current enclave identity + let resealed_data = seal_data(bound_key_bytes, sealing_policy, runtime_policy)?; + + Ok(resealed_data) +} + +/// Get attestation report for a challenge and initialize a user-bound key session. +/// +/// This is the trusted implementation called from the EDL interface. +#[allow(non_snake_case)] +#[allow(clippy::ptr_arg)] // Signature must match EDL-generated trait +pub fn userboundkey_get_attestation_report( + challenge: &Vec, +) -> Result { + let mut report_ptr: *mut core::ffi::c_void = core::ptr::null_mut(); + let mut report_size: u32 = 0; + let mut session_handle: USER_BOUND_KEY_SESSION_HANDLE = core::ptr::null_mut(); + + let hr = unsafe { + InitializeUserBoundKeySession( + challenge.as_ptr() as *const core::ffi::c_void, + challenge.len() as u32, + &mut report_ptr, + &mut report_size, + &mut session_handle, + ) + }; + + if hr < 0 { + return Err(AbiError::Hresult(hr)); + } + + // Take ownership of the heap-allocated buffer + let report_buf = unsafe { HeapBuffer::from_raw(report_ptr, report_size) }; + + // Defensive check - API contract guarantees valid output on S_OK + if report_buf.is_empty() { + return Err(AbiError::Hresult(-1)); + } + + // Convert report to Vec (HeapBuffer will free on drop) + let report = report_buf.to_vec(); + + Ok(attestationReportAndSessionInfo { + report, + sessionInfo: session_handle as u64, + }) +} + +/// Close a user-bound key session and release associated resources. +/// +/// This is the trusted implementation called from the EDL interface. +#[allow(non_snake_case)] +pub fn userboundkey_close_session(sessionInfo: u64) -> Result<(), AbiError> { + if sessionInfo != 0 { + unsafe { + let session_handle = sessionInfo as USER_BOUND_KEY_SESSION_HANDLE; + let hr = CloseUserBoundKeySession(session_handle); + if hr < 0 { + return Err(AbiError::Hresult(hr)); + } + } + } + Ok(()) +} diff --git a/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/types.rs b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/types.rs new file mode 100644 index 00000000..6be9cfa7 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-enclave/src/userboundkey/types.rs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Type definitions for user-bound key operations + +use crate::common::{CryptoError, EnclaveUtilsError}; +use alloc::string::String; +use sdk_enclave_gen::AbiError; +use windows_enclave::vertdll::RtlNtStatusToDosError; + +/// Error type for user-bound key operations +#[derive(Debug)] +pub enum UserBoundKeyError { + /// HRESULT error from Windows API + Hresult(i32), + /// NTSTATUS error from BCrypt/NT APIs + NtStatus(i32), + /// ABI layer error + AbiError(AbiError), + /// Memory allocation failed + AllocationFailed, + /// Invalid data format + InvalidData(&'static str), + /// Security policy violation + SecurityViolation(&'static str), + /// The sealing key is stale and data needs to be resealed + StaleKey, + /// Feature not implemented + NotImplemented(&'static str), + /// General cryptographic error + CryptoError(String), +} + +impl From for UserBoundKeyError { + fn from(err: AbiError) -> Self { + UserBoundKeyError::AbiError(err) + } +} + +impl From for UserBoundKeyError { + fn from(err: EnclaveUtilsError) -> Self { + match err { + EnclaveUtilsError::Hresult(hr) => UserBoundKeyError::Hresult(hr), + } + } +} + +impl From for UserBoundKeyError { + fn from(err: CryptoError) -> Self { + match err { + CryptoError::Hresult(hr) => UserBoundKeyError::Hresult(hr), + CryptoError::NtStatus(status) => UserBoundKeyError::NtStatus(status), + CryptoError::AuthTagMismatch => { + UserBoundKeyError::CryptoError(String::from("Authentication tag mismatch")) + } + CryptoError::DataTooShort => { + UserBoundKeyError::CryptoError(String::from("Data too short for tag")) + } + } + } +} + +/// Converts a Win32 error code to an HRESULT. +/// Equivalent to the HRESULT_FROM_WIN32 macro. +#[inline] +fn hresult_from_win32(error: u32) -> i32 { + if error == 0 { + 0 // S_OK + } else { + // FACILITY_WIN32 = 7, so (7 << 16) | 0x80000000 = 0x80070000 + ((error & 0x0000FFFF) | 0x80070000) as i32 + } +} + +impl UserBoundKeyError { + /// Convert the error to an HRESULT code + pub fn to_hresult(&self) -> i32 { + match self { + UserBoundKeyError::Hresult(hr) => *hr, + // NTSTATUS to HRESULT: Convert via RtlNtStatusToDosError then HRESULT_FROM_WIN32 + UserBoundKeyError::NtStatus(status) => { + // SAFETY: RtlNtStatusToDosError is a pure function that converts NTSTATUS to Win32 error + let win32_error = unsafe { RtlNtStatusToDosError(*status) }; + hresult_from_win32(win32_error) + } + UserBoundKeyError::AbiError(e) => e.to_hresult().0, + UserBoundKeyError::AllocationFailed => -2147024882, // E_OUTOFMEMORY + UserBoundKeyError::InvalidData(_) => -2147024809, // E_INVALIDARG + UserBoundKeyError::SecurityViolation(_) => -2147024891, // E_ACCESSDENIED + UserBoundKeyError::StaleKey => 0x80090325_u32 as i32, // SEC_E_BAD_PKGID or similar + UserBoundKeyError::NotImplemented(_) => -2147467263, // E_NOTIMPL + UserBoundKeyError::CryptoError(_) => -2147467259, // E_FAIL + } + } + + /// Check if this error indicates the sealing key is stale + pub fn is_stale_key(&self) -> bool { + matches!(self, UserBoundKeyError::StaleKey) + } +} diff --git a/rust/sdk/crates/libs/sdk-host/Cargo.toml b/rust/sdk/crates/libs/sdk-host/Cargo.toml index 694f9fe7..4a9ed7d2 100644 --- a/rust/sdk/crates/libs/sdk-host/Cargo.toml +++ b/rust/sdk/crates/libs/sdk-host/Cargo.toml @@ -8,12 +8,18 @@ workspace = true features = [ "Win32_System", "Win32_System_Memory", + "Win32_System_Threading", + "Win32_Security", + "Win32_Security_Authorization", "Security_Credentials", "Security_Cryptography", - "Security_Cryptography_Certificates" + "Storage_Streams" ] [dependencies] +sdk_host_gen = { path = "./generated/sdk_gen" } spin = { workspace = true } userboundkey-kcm = { workspace = true } -veil_abi_host_gen = { path = "./generated/veil_abi_gen" } \ No newline at end of file +widestring = { workspace = true } +windows-core = { workspace = true } +windows-future = { workspace = true } \ No newline at end of file diff --git a/rust/sdk/crates/libs/sdk-host/src/common/mod.rs b/rust/sdk/crates/libs/sdk-host/src/common/mod.rs new file mode 100644 index 00000000..1e4cd4e5 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-host/src/common/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Common utilities and SDK implementations for VTL0 host operations. + +pub mod sdk_impls; + +pub use sdk_impls::HostImpl; diff --git a/rust/sdk/crates/libs/sdk-host/src/common/sdk_impls.rs b/rust/sdk/crates/libs/sdk-host/src/common/sdk_impls.rs new file mode 100644 index 00000000..bdf108ac --- /dev/null +++ b/rust/sdk/crates/libs/sdk-host/src/common/sdk_impls.rs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! SDK implementation of the EDL Untrusted trait. +//! +//! This module provides the `HostImpl` struct that implements the generated +//! `Untrusted` trait by delegating to free functions in feature modules. +//! This pattern allows feature implementations to live in their own modules +//! while the trait impl acts as a thin wrapper. + +use sdk_host_gen::AbiError; +use sdk_host_gen::implementation::types::{ + credentialAndSessionInfo, edl::WString, keyCredentialCacheConfig, +}; +use sdk_host_gen::implementation::untrusted::Untrusted; + +use crate::userboundkey; + +/// SDK host implementation of the Untrusted trait. +/// +/// This struct implements all EDL untrusted functions by delegating to +/// free functions in the appropriate feature modules. +pub struct HostImpl; + +#[allow(non_snake_case)] +impl Untrusted for HostImpl { + fn userboundkey_establish_session_for_create( + enclave: u64, + keyName: &WString, + ecdhProtocol: u64, + message: &WString, + windowId: u64, + cacheConfig: &keyCredentialCacheConfig, + keyCredentialCreationOption: u32, + ) -> Result { + userboundkey::userboundkey_establish_session_for_create( + enclave, + keyName, + ecdhProtocol, + message, + windowId, + cacheConfig, + keyCredentialCreationOption, + ) + } + + fn userboundkey_establish_session_for_load( + enclave: u64, + keyName: &WString, + message: &WString, + windowId: u64, + ) -> Result { + userboundkey::userboundkey_establish_session_for_load(enclave, keyName, message, windowId) + } + + fn userboundkey_get_authorization_context_from_credential( + credential: u64, + encryptedRequest: &Vec, + message: &WString, + windowId: u64, + ) -> Result, AbiError> { + userboundkey::userboundkey_get_authorization_context_from_credential( + credential, + encryptedRequest, + message, + windowId, + ) + } + + fn userboundkey_get_secret_from_credential( + credential: u64, + encryptedRequest: &Vec, + message: &WString, + windowId: u64, + ) -> Result, AbiError> { + userboundkey::userboundkey_get_secret_from_credential( + credential, + encryptedRequest, + message, + windowId, + ) + } + + fn userboundkey_format_key_name(keyName: &WString) -> Result { + userboundkey::userboundkey_format_key_name(keyName) + } + + fn userboundkey_delete_credential(credential: u64) -> Result<(), AbiError> { + userboundkey::userboundkey_delete_credential(credential) + } +} diff --git a/rust/sdk/crates/libs/sdk-host/src/enclave.rs b/rust/sdk/crates/libs/sdk-host/src/enclave.rs new file mode 100644 index 00000000..181dfd37 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-host/src/enclave.rs @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! VBS Enclave lifecycle management APIs. +//! +//! This module provides APIs for creating, loading, initializing, and +//! cleaning up VBS enclaves. +//! +//! # Example +//! +//! ```no_run +//! use vbsenclave_sdk_host::enclave::{EnclaveHandle, megabytes}; +//! +//! // Create and initialize an enclave +//! let enclave = EnclaveHandle::create_and_initialize( +//! "my_enclave.dll", +//! megabytes(32), +//! None, // Use default owner ID +//! )?; +//! +//! // Use enclave.as_ptr() for enclave calls +//! // Enclave is automatically terminated and deleted when dropped +//! # Ok::<(), Box>(()) +//! ``` + +use std::ffi::c_void; +use widestring::U16CString; +use windows::core::{Error, HRESULT, Result}; + +// FFI declarations for enclave APIs +mod ffi { + use std::ffi::c_void; + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn CreateEnclave( + hprocess: *mut c_void, + lpaddress: *const c_void, + dwsize: usize, + dwinitialcommitment: usize, + flenclavetype: u32, + lpenclaveinformation: *const c_void, + dwinfolength: u32, + lpenclaveerror: *mut u32 + ) -> *mut c_void); + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn LoadEnclaveImageW( + lpenclaveaddress: *const c_void, + lpimagename: *const u16 + ) -> i32); + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn InitializeEnclave( + hprocess: *mut c_void, + lpaddress: *const c_void, + lpenclaveinformation: *const c_void, + dwinfolength: u32, + lpenclaveerror: *mut u32 + ) -> i32); + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn TerminateEnclave( + lpaddress: *const c_void, + fwait: i32 + ) -> i32); + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn DeleteEnclave( + lpaddress: *const c_void + ) -> i32); + + windows::core::link!("api-ms-win-core-enclave-l1-1-0.dll" "system" fn IsEnclaveTypeSupported( + flenclavetype: u32 + ) -> i32); +} + +/// VBS enclave type constant. +pub const ENCLAVE_TYPE_VBS: u32 = 0x10; + +/// Length of the enclave owner ID. +pub const IMAGE_ENCLAVE_LONG_ID_LENGTH: usize = 32; + +/// Default enclave size (32 MB). +pub const DEFAULT_ENCLAVE_SIZE: usize = 32 * 1024 * 1024; + +/// Enclave creation info for VBS enclaves. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct EnclaveCreateInfoVbs { + /// Flags for enclave creation. + pub flags: u32, + /// Owner ID for the enclave. + pub owner_id: [u8; IMAGE_ENCLAVE_LONG_ID_LENGTH], +} + +/// Enclave initialization info for VBS enclaves. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EnclaveInitInfoVbs { + /// Size of this structure. + pub length: u32, + /// Number of threads to create. + pub thread_count: u32, +} + +impl Default for EnclaveInitInfoVbs { + fn default() -> Self { + Self { + length: core::mem::size_of::() as u32, + thread_count: 1, + } + } +} + +/// Convert megabytes to bytes. +#[inline] +pub const fn megabytes(mb: usize) -> usize { + mb * 0x100000 +} + +/// Check if VBS enclaves are supported on this system. +pub fn is_vbs_supported() -> bool { + unsafe { ffi::IsEnclaveTypeSupported(ENCLAVE_TYPE_VBS) != 0 } +} + +/// Create a new VBS enclave. +/// +/// # Arguments +/// +/// * `size` - Size of the enclave in bytes. Use [`megabytes`] helper. +/// * `owner_id` - Optional owner ID. If None, uses zeros. +/// * `flags` - Enclave creation flags. +/// * `initial_commitment` - Initial memory commitment (usually 0). +/// +/// # Returns +/// +/// Raw pointer to the enclave on success. +/// +/// # Errors +/// +/// Returns an error if VBS is not supported or enclave creation fails. +pub fn create( + size: usize, + owner_id: Option<&[u8]>, + flags: u32, + initial_commitment: usize, +) -> Result<*mut c_void> { + if !is_vbs_supported() { + return Err(Error::new( + HRESULT(-2147024891i32), // E_ACCESSDENIED + "VBS enclave type not supported", + )); + } + + let mut create_info = EnclaveCreateInfoVbs { + flags, + owner_id: [0u8; IMAGE_ENCLAVE_LONG_ID_LENGTH], + }; + + if let Some(id) = owner_id { + let len = id.len().min(IMAGE_ENCLAVE_LONG_ID_LENGTH); + create_info.owner_id[..len].copy_from_slice(&id[..len]); + } + + let mut enclave_error: u32 = 0; + + let enclave = unsafe { + ffi::CreateEnclave( + -1isize as *mut c_void, // GetCurrentProcess() pseudo-handle + std::ptr::null(), // Let system choose address + size, + initial_commitment, + ENCLAVE_TYPE_VBS, + &create_info as *const _ as *const c_void, + core::mem::size_of::() as u32, + &mut enclave_error, + ) + }; + + if enclave.is_null() { + let last_error = unsafe { windows::Win32::Foundation::GetLastError() }; + return Err(Error::new( + HRESULT::from_win32(last_error.0), + "Failed to create VBS enclave", + )); + } + + Ok(enclave) +} + +/// Load an enclave image (DLL) into an existing enclave. +/// +/// # Arguments +/// +/// * `enclave` - Pointer to the enclave. +/// * `image_path` - Path to the enclave DLL (filename or full path). +/// +/// # Safety +/// +/// The caller must ensure `enclave` is a valid pointer returned by `create()`. +/// +/// # Errors +/// +/// Returns an error if loading fails. +pub unsafe fn load_image(enclave: *mut c_void, image_path: &str) -> Result<()> { + let wide_path = U16CString::from_str(image_path).map_err(|_| { + Error::new( + HRESULT(-2147024809i32), // E_INVALIDARG + "Invalid path: contains interior null", + ) + })?; + + let result = unsafe { ffi::LoadEnclaveImageW(enclave, wide_path.as_ptr()) }; + + if result == 0 { + let last_error = unsafe { windows::Win32::Foundation::GetLastError() }; + return Err(Error::new( + HRESULT::from_win32(last_error.0), + "Failed to load enclave image", + )); + } + + Ok(()) +} + +/// Initialize an enclave after loading its image. +/// +/// # Arguments +/// +/// * `enclave` - Pointer to the enclave. +/// * `thread_count` - Number of threads to create (usually 1). +/// +/// # Safety +/// +/// The caller must ensure `enclave` is a valid pointer returned by `create()`. +/// +/// # Errors +/// +/// Returns an error if initialization fails. +pub unsafe fn initialize(enclave: *mut c_void, thread_count: u32) -> Result<()> { + let init_info = EnclaveInitInfoVbs { + length: core::mem::size_of::() as u32, + thread_count, + }; + + let mut enclave_error: u32 = 0; + + let result = unsafe { + ffi::InitializeEnclave( + -1isize as *mut c_void, // GetCurrentProcess() + enclave, + &init_info as *const _ as *const c_void, + init_info.length, + &mut enclave_error, + ) + }; + + if result == 0 { + let last_error = unsafe { windows::Win32::Foundation::GetLastError() }; + return Err(Error::new( + HRESULT::from_win32(last_error.0), + "Failed to initialize enclave", + )); + } + + Ok(()) +} + +/// Terminate an enclave. +/// +/// # Arguments +/// +/// * `enclave` - Pointer to the enclave. +/// * `wait` - If true, wait for all threads to terminate. +/// +/// # Safety +/// +/// The caller must ensure `enclave` is a valid pointer returned by `create()`. +pub unsafe fn terminate(enclave: *mut c_void, wait: bool) { + unsafe { + let _ = ffi::TerminateEnclave(enclave, if wait { 1 } else { 0 }); + } +} + +/// Delete an enclave. +/// +/// # Arguments +/// +/// * `enclave` - Pointer to the enclave. +/// +/// # Safety +/// +/// The caller must ensure `enclave` is a valid pointer returned by `create()`. +pub unsafe fn delete(enclave: *mut c_void) { + unsafe { + let _ = ffi::DeleteEnclave(enclave); + } +} + +/// A handle to a VBS enclave that manages its lifecycle. +/// +/// When dropped, the enclave is automatically terminated and deleted. +pub struct EnclaveHandle { + enclave: *mut c_void, +} + +impl EnclaveHandle { + /// Create a new enclave handle from a raw pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is a valid enclave pointer + /// that was created with `CreateEnclave`. + pub unsafe fn from_raw(enclave: *mut c_void) -> Self { + Self { enclave } + } + + /// Create, load, and initialize a VBS enclave in one step. + /// + /// This is the recommended way to create an enclave. It: + /// 1. Creates the enclave + /// 2. Loads the enclave DLL + /// 3. Initializes the enclave with 2 threads (needed for callback support) + /// + /// # Arguments + /// + /// * `dll_path` - Path to the enclave DLL (filename or full path). + /// If just a filename, `LoadEnclaveImageW` will search the current directory and PATH. + /// * `size` - Size of the enclave in bytes. Use [`megabytes`] helper. + /// * `owner_id` - Optional owner ID bytes. If None, uses zeros. + /// + /// # Example + /// + /// ```no_run + /// use vbsenclave_sdk_host::enclave::{EnclaveHandle, megabytes}; + /// + /// let enclave = EnclaveHandle::create_and_initialize( + /// "my_enclave.dll", + /// megabytes(32), + /// None, + /// )?; + /// # Ok::<(), windows::core::Error>(()) + /// ``` + pub fn create_and_initialize( + dll_path: &str, + size: usize, + owner_id: Option<&[u8]>, + ) -> Result { + // Default to 2 threads for callback support (e.g., Windows Hello attestation) + Self::create_and_initialize_with_threads(dll_path, size, owner_id, 2) + } + + /// Create, load, and initialize a VBS enclave with a specific thread count. + /// + /// Use this when you need more control over the number of enclave threads. + /// For user-bound key operations, at least 2 threads are required. + /// + /// # Arguments + /// + /// * `dll_path` - Path to the enclave DLL (filename or full path). + /// * `size` - Size of the enclave in bytes. Use [`megabytes`] helper. + /// * `owner_id` - Optional owner ID bytes. If None, uses zeros. + /// * `thread_count` - Number of threads to initialize. Use at least 2 for callback support. + pub fn create_and_initialize_with_threads( + dll_path: &str, + size: usize, + owner_id: Option<&[u8]>, + thread_count: u32, + ) -> Result { + // Create the enclave + let enclave_ptr = create(size, owner_id, 0, 0)?; + + // If any subsequent step fails, we need to clean up + let handle = unsafe { Self::from_raw(enclave_ptr) }; + + // Load the enclave image + // SAFETY: enclave_ptr was just returned by create() and is valid + unsafe { load_image(enclave_ptr, dll_path)? }; + + // Initialize with specified thread count + // SAFETY: enclave_ptr is valid and image has been loaded + unsafe { initialize(enclave_ptr, thread_count)? }; + + Ok(handle) + } + + /// Get the raw enclave pointer for use with enclave calls. + #[inline] + pub fn as_ptr(&self) -> *mut c_void { + self.enclave + } + + /// Consume this handle without running the destructor. + /// + /// The caller becomes responsible for terminating and deleting the enclave. + pub fn into_raw(self) -> *mut c_void { + let ptr = self.enclave; + std::mem::forget(self); + ptr + } +} + +impl Drop for EnclaveHandle { + fn drop(&mut self) { + if !self.enclave.is_null() { + // SAFETY: self.enclave is valid because it came from create() + // and we only set it to null after this point + unsafe { + // fWait = TRUE means wait for all threads to terminate + terminate(self.enclave, true); + delete(self.enclave); + } + } + } +} diff --git a/rust/sdk/crates/libs/sdk-host/src/lib.rs b/rust/sdk/crates/libs/sdk-host/src/lib.rs index 47e9a72d..62760fbf 100644 --- a/rust/sdk/crates/libs/sdk-host/src/lib.rs +++ b/rust/sdk/crates/libs/sdk-host/src/lib.rs @@ -3,9 +3,52 @@ mod host_ffi; +pub mod common; +pub mod enclave; +pub mod userboundkey; + +// Re-export AbiError for consumers who call register_sdk_callbacks +pub use sdk_host_gen::AbiError; + +// Re-export HostImpl for consumers +pub use common::HostImpl; + // The userboundkey-kcm crate is not intended to be published as a standalone package. // Long term, these KCM APIs should be consumed from the windows-rs crate. // Until those APIs are available there, the sdk-host crate re-exports the required // KCM surface. In practice, most consumers only need // KeyCredentialManager::GetSecureId(). pub use userboundkey_kcm::KeyCredentialManager; + +/// Register all SDK VTL0 callbacks with the enclave. +/// +/// Call this function once after loading your enclave to enable SDK features. +/// This registers the necessary callbacks that allow the enclave's SDK functions +/// to communicate back to VTL0 (e.g., for Windows Hello integration). +/// +/// # Arguments +/// +/// * `enclave_ptr` - Pointer to the loaded enclave (from `EnclaveHandle::as_ptr()`) +/// +/// # Returns +/// +/// Returns `Ok(())` on success, or an error on failure. +/// +/// # Example +/// +/// ```rust,ignore +/// use vbsenclave_sdk_host::{register_sdk_callbacks, enclave::EnclaveHandle}; +/// +/// let enclave = EnclaveHandle::create_and_initialize(path, size, owner_id)?; +/// register_sdk_callbacks(enclave.as_ptr())?; +/// ``` +pub fn register_sdk_callbacks(enclave_ptr: *mut core::ffi::c_void) -> Result<(), AbiError> { + // Register SDK callbacks using the SdkHost interface and HostImpl + let sdk_wrapper = userboundkey::SdkHost::new(enclave_ptr); + sdk_wrapper.register_vtl0_callbacks::()?; + + // Future SDK features will be added here: + // #[cfg(feature = "secure_storage")] + + Ok(()) +} diff --git a/rust/sdk/crates/libs/sdk-host/src/userboundkey/mod.rs b/rust/sdk/crates/libs/sdk-host/src/userboundkey/mod.rs new file mode 100644 index 00000000..a7a5d565 --- /dev/null +++ b/rust/sdk/crates/libs/sdk-host/src/userboundkey/mod.rs @@ -0,0 +1,534 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! VTL0 User-Bound Key Implementation +//! +//! Implements EDL untrusted callbacks for user-bound key operations. +//! This module provides VTL0/host-side Windows Hello integration with VBS enclave support. + +use sdk_host_gen::AbiError; +pub use sdk_host_gen::SdkHost; +pub use sdk_host_gen::implementation::types::{ + attestationReportAndSessionInfo, credentialAndSessionInfo, edl::WString, + keyCredentialCacheConfig, +}; + +use std::sync::{Arc, Mutex}; + +use windows::{ + Security::Cryptography::CryptographicBuffer, + Storage::Streams::IBuffer, + Win32::{ + Foundation::{CloseHandle, E_FAIL, E_INVALIDARG, HANDLE, HLOCAL, LocalFree}, + Security::Authorization::ConvertSidToStringSidW, + Security::{GetTokenInformation, PSID, TOKEN_QUERY, TOKEN_USER, TokenUser}, + System::Threading::{GetCurrentProcess, OpenProcessToken}, + }, + core::HSTRING, +}; +use windows_core::{Interface, PWSTR}; + +use userboundkey_kcm::{ + AttestationChallengeHandler, ChallengeResponseKind, IBuffer as KcmIBuffer, + IKeyCredentialCacheConfigurationFactory, KeyCredential, KeyCredentialCacheConfiguration, + KeyCredentialCacheOption, KeyCredentialCreationOption, KeyCredentialManager, + KeyCredentialStatus, TimeSpan, WindowId, +}; + +/// RAII wrapper for session handle that calls back to VTL1 on cleanup +struct UniqueSessionHandle { + handle: usize, + enclave_ptr: usize, +} + +impl UniqueSessionHandle { + fn new() -> Self { + Self { + handle: 0, + enclave_ptr: 0, + } + } + + #[allow(dead_code)] + fn get(&self) -> usize { + self.handle + } + + fn release(&mut self) -> usize { + let result = self.handle; + self.handle = 0; + self.enclave_ptr = 0; + result + } + + fn set(&mut self, handle: usize, enclave_ptr: usize) { + self.reset(); + self.handle = handle; + self.enclave_ptr = enclave_ptr; + } + + fn reset(&mut self) { + if self.handle != 0 && self.enclave_ptr != 0 { + // Call back to VTL1 to close session + let enclave_interface = SdkHost::new(self.enclave_ptr as *mut core::ffi::c_void); + let _ = enclave_interface.userboundkey_close_session(self.handle as u64); + } + self.handle = 0; + self.enclave_ptr = 0; + } +} + +impl Drop for UniqueSessionHandle { + fn drop(&mut self) { + self.reset(); + } +} + +/// Convert a Windows IBuffer to a Vec +fn buffer_to_vec(buffer: &IBuffer) -> windows_core::Result> { + let mut byte_array = windows::core::Array::::new(); + CryptographicBuffer::CopyToByteArray(buffer, &mut byte_array)?; + Ok(byte_array.to_vec()) +} + +/// Convert Vec to a Windows IBuffer +fn vec_to_buffer(data: &[u8]) -> windows_core::Result { + CryptographicBuffer::CreateFromByteArray(data) +} + +/// Convert a KcmIBuffer (from userboundkey-kcm) to Vec +fn kcm_buffer_to_vec(buffer: &KcmIBuffer) -> windows_core::Result> { + // Cast KcmIBuffer to windows IBuffer + let windows_buffer: IBuffer = buffer.cast()?; + buffer_to_vec(&windows_buffer) +} + +/// Convert Vec to a KcmIBuffer +fn vec_to_kcm_buffer(data: &[u8]) -> windows_core::Result { + let windows_buffer = vec_to_buffer(data)?; + windows_buffer.cast() +} + +/// Get the algorithm name string from the ECDH algorithm handle +fn get_algorithm(ecdh_algorithm: u64) -> Result { + // BCrypt algorithm pseudo-handle values from Windows SDK bcrypt.h + const BCRYPT_ECDH_P384_ALG_HANDLE: u64 = 0x000002b1; + const BCRYPT_ECDH_P256_ALG_HANDLE: u64 = 0x000002a1; + + if ecdh_algorithm == BCRYPT_ECDH_P384_ALG_HANDLE { + Ok(HSTRING::from("ECDH_P384")) + } else if ecdh_algorithm == BCRYPT_ECDH_P256_ALG_HANDLE { + Ok(HSTRING::from("ECDH_P256")) + } else { + Err(AbiError::Hresult(E_INVALIDARG.0)) + } +} + +/// Convert keyCredentialCacheConfig to the WinRT KeyCredentialCacheConfiguration +fn convert_cache_config( + cache_config: &keyCredentialCacheConfig, +) -> windows_core::Result { + let cache_option = match cache_config.cacheOption { + 0 => KeyCredentialCacheOption::NoCache, + 1 => KeyCredentialCacheOption::CacheWhenUnlocked, + _ => KeyCredentialCacheOption::NoCache, + }; + + // Convert timeout from seconds to TimeSpan (100-nanosecond units) + let timeout = TimeSpan { + Duration: (cache_config.cacheTimeoutInSeconds as i64) * 10_000_000, + }; + + // Use activation factory to create instance + let factory = windows_core::factory::< + KeyCredentialCacheConfiguration, + IKeyCredentialCacheConfigurationFactory, + >()?; + factory.CreateInstance(cache_option, timeout, cache_config.cacheUsageCount) +} + +/// Create an attestation challenge callback that calls into VTL1 +fn create_challenge_callback( + session_info: Arc>, + enclave_ptr: usize, +) -> AttestationChallengeHandler { + AttestationChallengeHandler::new(move |challenge: windows_core::Ref| { + println!("[SDK-Host] Challenge callback invoked!"); + + // Get the IBuffer reference using ok() which returns Result<&T> + let challenge_buffer: &KcmIBuffer = challenge.ok()?; + // Convert KcmIBuffer to Vec + let challenge_vec = kcm_buffer_to_vec(challenge_buffer)?; + + println!( + "[SDK-Host] Challenge size: {} bytes, calling VTL1 for attestation report...", + challenge_vec.len() + ); + + // Call into VTL1 to get attestation report + let enclave_interface = SdkHost::new(enclave_ptr as *mut core::ffi::c_void); + let attestation_result = enclave_interface + .userboundkey_get_attestation_report(&challenge_vec) + .map_err(|e| { + println!("[SDK-Host] VTL1 attestation call failed: {:?}", e); + windows_core::Error::from(E_FAIL) + })?; + + println!( + "[SDK-Host] Got attestation report: {} bytes", + attestation_result.report.len() + ); + + // Store session handle + { + let mut session = match session_info.lock() { + Ok(guard) => guard, + Err(poisoned) => { + println!( + "[SDK-Host] Warning: session_info mutex was poisoned: {:?}", + poisoned + ); + // Recover the guard from the poisoned mutex - the data may still be usable + poisoned.into_inner() + } + }; + session.set(attestation_result.sessionInfo as usize, enclave_ptr); + } + + // Convert report to KcmIBuffer + vec_to_kcm_buffer(&attestation_result.report) + }) +} + +/// Convert a PSID to a wide string representation (Vec) +fn sid_to_wide_string(sid: PSID) -> Result, AbiError> { + unsafe { + let mut string_sid = PWSTR::null(); + ConvertSidToStringSidW(sid, &mut string_sid).map_err(|_| AbiError::Hresult(E_FAIL.0))?; + + // Convert PWSTR to Vec + let len = (0..).take_while(|&i| *string_sid.0.add(i) != 0).count(); + let slice = std::slice::from_raw_parts(string_sid.0, len); + let result = slice.to_vec(); + + // Free the memory allocated by ConvertSidToStringSidW + let _ = LocalFree(Some(HLOCAL(string_sid.0 as *mut _))); + + Ok(result) + } +} + +/// Helper to convert WString to HSTRING +fn wstring_to_hstring(ws: &WString) -> HSTRING { + HSTRING::from_wide(&ws.wchars) +} + +/// Establish a session for creating a new Windows Hello key. +#[allow(non_snake_case)] +#[allow(unused_variables)] +pub fn userboundkey_establish_session_for_create( + enclave: u64, + keyName: &WString, + ecdhProtocol: u64, + message: &WString, + windowId: u64, + cacheConfig: &keyCredentialCacheConfig, + keyCredentialCreationOption: u32, +) -> Result { + println!("[SDK-Host] userboundkey_establish_session_for_create called"); + println!( + "[SDK-Host] windowId={}, option={}", + windowId, keyCredentialCreationOption + ); + + let algorithm = get_algorithm(ecdhProtocol)?; + println!("[SDK-Host] algorithm resolved"); + + let cache_configuration = + convert_cache_config(cacheConfig).map_err(|e| AbiError::Hresult(e.code().0))?; + println!("[SDK-Host] cache config created"); + + let session_info = Arc::new(Mutex::new(UniqueSessionHandle::new())); + let enclave_ptr = enclave as usize; + + // Try to delete existing key first (ignore errors) + let key_name_hstring = wstring_to_hstring(keyName); + println!("[SDK-Host] Deleting existing key (if any)..."); + if let Ok(delete_op) = KeyCredentialManager::DeleteAsync(&key_name_hstring) { + let _ = delete_op.join(); + } + println!("[SDK-Host] Delete complete"); + + // Create the credential with VBS attestation + let message_hstring = wstring_to_hstring(message); + let win_id = WindowId { Value: windowId }; + let creation_option = KeyCredentialCreationOption(keyCredentialCreationOption as i32); + + let callback = create_challenge_callback(session_info.clone(), enclave_ptr); + + println!("[SDK-Host] Calling KeyCredentialManager::RequestCreateAsync2..."); + + let credential_result = KeyCredentialManager::RequestCreateAsync2( + &key_name_hstring, + creation_option, + &algorithm, + &message_hstring, + &cache_configuration, + win_id, + ChallengeResponseKind::VirtualizationBasedSecurityEnclave, + &callback, + ) + .map_err(|e| { + println!("[SDK-Host] RequestCreateAsync2 failed to start: {:?}", e); + AbiError::Hresult(e.code().0) + })?; + + println!("[SDK-Host] RequestCreateAsync2 started, waiting for completion (join)..."); + + let credential_result = credential_result.join().map_err(|e| { + println!("[SDK-Host] RequestCreateAsync2.join() failed: {:?}", e); + AbiError::Hresult(e.code().0) + })?; + + println!("[SDK-Host] RequestCreateAsync2 completed!"); + + // Check if operation was successful + let status = credential_result + .Status() + .map_err(|e| AbiError::Hresult(e.code().0))?; + if status != KeyCredentialStatus::Success { + return Err(AbiError::Hresult(status.0)); + } + + // Get credential and convert to raw pointer + let credential = credential_result + .Credential() + .map_err(|e| AbiError::Hresult(e.code().0))?; + let credential_ptr = windows_core::Interface::as_raw(&credential) as u64; + + // Transfer session ownership + let session_handle = { + let mut session = match session_info.lock() { + Ok(guard) => guard, + Err(poisoned) => { + println!("[SDK-Host] Warning: session_info mutex was poisoned"); + poisoned.into_inner() + } + }; + session.release() as u64 + }; + + // Add reference to credential since we're transferring ownership + // We need to prevent the credential from being released + std::mem::forget(credential); + + Ok(credentialAndSessionInfo { + credential: credential_ptr, + sessionInfo: session_handle, + }) +} + +/// Establish a session for loading an existing Windows Hello key. +#[allow(non_snake_case)] +#[allow(unused_variables)] +pub fn userboundkey_establish_session_for_load( + enclave: u64, + keyName: &WString, + message: &WString, + windowId: u64, +) -> Result { + let _ = (message, windowId); // Mark as intentionally unused + let session_info = Arc::new(Mutex::new(UniqueSessionHandle::new())); + let enclave_ptr = enclave as usize; + + let key_name_hstring = wstring_to_hstring(keyName); + let callback = create_challenge_callback(session_info.clone(), enclave_ptr); + + let credential_result = KeyCredentialManager::OpenAsync2( + &key_name_hstring, + ChallengeResponseKind::VirtualizationBasedSecurityEnclave, + &callback, + ) + .map_err(|e| AbiError::Hresult(e.code().0))? + .join() + .map_err(|e| AbiError::Hresult(e.code().0))?; + + // Check if operation was successful + let status = credential_result + .Status() + .map_err(|e| AbiError::Hresult(e.code().0))?; + if status != KeyCredentialStatus::Success { + return Err(AbiError::Hresult(status.0)); + } + + // Get credential and convert to raw pointer + let credential = credential_result + .Credential() + .map_err(|e| AbiError::Hresult(e.code().0))?; + let credential_ptr = windows_core::Interface::as_raw(&credential) as u64; + + // Transfer session ownership + let session_handle = { + let mut session = match session_info.lock() { + Ok(guard) => guard, + Err(poisoned) => { + println!("[SDK-Host] Warning: session_info mutex was poisoned"); + poisoned.into_inner() + } + }; + session.release() as u64 + }; + + // Prevent credential from being released (transfer ownership) + std::mem::forget(credential); + + Ok(credentialAndSessionInfo { + credential: credential_ptr, + sessionInfo: session_handle, + }) +} + +/// Get authorization context from a credential. +#[allow(non_snake_case)] +#[allow(unused_variables)] +#[allow(clippy::ptr_arg)] // Signature must match EDL-generated trait +pub fn userboundkey_get_authorization_context_from_credential( + credential: u64, + encryptedRequest: &Vec, + message: &WString, + windowId: u64, +) -> Result, AbiError> { + // Reconstruct KeyCredential from raw pointer + // We use from_raw and then manually AddRef since the caller owns the credential + let key_credential: KeyCredential = unsafe { + let raw_ptr = credential as *mut core::ffi::c_void; + if raw_ptr.is_null() { + return Err(AbiError::Hresult(E_INVALIDARG.0)); + } + // Clone increases ref count - the original is still owned by caller + let temp = KeyCredential::from_raw(raw_ptr); + let cloned = temp.clone(); + // Forget temp to not decrement refcount + core::mem::forget(temp); + cloned + }; + + // Convert encrypted request to IBuffer + let encrypted_buffer = + vec_to_kcm_buffer(encryptedRequest).map_err(|e| AbiError::Hresult(e.code().0))?; + + // Call RetrieveAuthorizationContext on the credential + let auth_context = key_credential + .RetrieveAuthorizationContext(&encrypted_buffer) + .map_err(|e| AbiError::Hresult(e.code().0))?; + + kcm_buffer_to_vec(&auth_context).map_err(|e| AbiError::Hresult(e.code().0)) +} + +/// Get shared secret from a credential. +#[allow(non_snake_case)] +#[allow(unused_variables)] +#[allow(clippy::ptr_arg)] // Signature must match EDL-generated trait +pub fn userboundkey_get_secret_from_credential( + credential: u64, + encryptedRequest: &Vec, + message: &WString, + windowId: u64, +) -> Result, AbiError> { + // Reconstruct KeyCredential from raw pointer + // We use from_raw and then manually AddRef since the caller owns the credential + let key_credential: KeyCredential = unsafe { + let raw_ptr = credential as *mut core::ffi::c_void; + if raw_ptr.is_null() { + return Err(AbiError::Hresult(E_INVALIDARG.0)); + } + // Clone increases ref count - the original is still owned by caller + let temp = KeyCredential::from_raw(raw_ptr); + let cloned = temp.clone(); + // Forget temp to not decrement refcount + core::mem::forget(temp); + cloned + }; + + // Convert encrypted request to IBuffer + let encrypted_buffer = + vec_to_kcm_buffer(encryptedRequest).map_err(|e| AbiError::Hresult(e.code().0))?; + + let win_id = WindowId { Value: windowId }; + let message_hstring = wstring_to_hstring(message); + + // Call RequestDeriveSharedSecretAsync on the credential + let operation_result = key_credential + .RequestDeriveSharedSecretAsync(win_id, &message_hstring, &encrypted_buffer) + .map_err(|e| AbiError::Hresult(e.code().0))? + .join() + .map_err(|e| AbiError::Hresult(e.code().0))?; + + let result_buffer = operation_result + .Result() + .map_err(|e| AbiError::Hresult(e.code().0))?; + + kcm_buffer_to_vec(&result_buffer).map_err(|e| AbiError::Hresult(e.code().0)) +} + +/// Format a key name with the current user's SID. +#[allow(non_snake_case)] +pub fn userboundkey_format_key_name(keyName: &WString) -> Result { + unsafe { + let mut process_token = HANDLE::default(); + OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut process_token) + .map_err(|_| AbiError::Hresult(E_FAIL.0))?; + + // Get token user info size + let mut return_length: u32 = 0; + let _ = GetTokenInformation(process_token, TokenUser, None, 0, &mut return_length); + + // Allocate buffer and get token user info + let mut buffer = vec![0u8; return_length as usize]; + GetTokenInformation( + process_token, + TokenUser, + Some(buffer.as_mut_ptr() as *mut _), + return_length, + &mut return_length, + ) + .map_err(|_| AbiError::Hresult(E_FAIL.0))?; + + let _ = CloseHandle(process_token); + + let token_user = &*(buffer.as_ptr() as *const TOKEN_USER); + let user_sid = token_user.User.Sid; + + // Convert SID to wide string + let sid_wide = sid_to_wide_string(user_sid)?; + + // Build formatted key: {SID}//{SID}//{keyName} + let slash_slash = widestring::u16str!("//"); + let mut result = + Vec::with_capacity(sid_wide.len() + 2 + sid_wide.len() + 2 + keyName.wchars.len()); + result.extend_from_slice(&sid_wide); + result.extend_from_slice(slash_slash.as_slice()); + result.extend_from_slice(&sid_wide); + result.extend_from_slice(slash_slash.as_slice()); + result.extend_from_slice(&keyName.wchars); + + Ok(WString { wchars: result }) + } +} + +/// Delete a credential handle. +#[allow(non_snake_case)] +pub fn userboundkey_delete_credential(credential: u64) -> Result<(), AbiError> { + if credential == 0 { + return Ok(()); + } + + // Take ownership of the credential and let it drop + unsafe { + let raw_ptr = credential as *mut core::ffi::c_void; + let _ = KeyCredential::from_raw(raw_ptr); + // Credential will be released when dropped + } + + Ok(()) +} diff --git a/rust/sdk/crates/libs/sdk.edl b/rust/sdk/crates/libs/sdk.edl new file mode 100644 index 00000000..80661570 --- /dev/null +++ b/rust/sdk/crates/libs/sdk.edl @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// EDL file for user-bound key operations between VTL0 (host) and VTL1 (enclave). +// This defines the cross-VTL interface for Windows Hello VBS enclave integration. + +enclave +{ + // Configuration for credential caching behavior + struct keyCredentialCacheConfig + { + uint32_t cacheOption; + uint32_t cacheTimeoutInSeconds; + uint32_t cacheUsageCount; + }; + + // Result from VTL1 attestation report generation + struct attestationReportAndSessionInfo + { + vector report; + uint64_t sessionInfo; + }; + + // Result from VTL0 credential establishment + struct credentialAndSessionInfo + { + uint64_t credential; + uint64_t sessionInfo; + }; + + // Trusted functions: Called from VTL0 into VTL1 (enclave) + trusted + { + // Generate an attestation report for the given challenge. + // This initializes a user-bound key session in VTL1. + attestationReportAndSessionInfo userboundkey_get_attestation_report( + vector challenge + ); + + // Close a user-bound key session and release associated resources. + void userboundkey_close_session( + uint64_t sessionInfo + ); + }; + + // Untrusted functions: Called from VTL1 into VTL0 (host) + untrusted + { + // Establish a session for creating a new Windows Hello key. + // Returns credential handle and session info on success. + credentialAndSessionInfo userboundkey_establish_session_for_create( + uint64_t enclave, + wstring keyName, + uint64_t ecdhProtocol, + wstring message, + uint64_t windowId, + keyCredentialCacheConfig cacheConfig, + uint32_t keyCredentialCreationOption + ); + + // Establish a session for loading an existing Windows Hello key. + // Returns credential handle and session info on success. + credentialAndSessionInfo userboundkey_establish_session_for_load( + uint64_t enclave, + wstring keyName, + wstring message, + uint64_t windowId + ); + + // Get authorization context from credential using encrypted KCM request. + // Returns the authorization context bytes. + vector userboundkey_get_authorization_context_from_credential( + uint64_t credential, + vector encryptedRequest, + wstring message, + uint64_t windowId + ); + + // Derive shared secret from credential using encrypted KCM request. + // Returns the encrypted shared secret bytes. + vector userboundkey_get_secret_from_credential( + uint64_t credential, + vector encryptedRequest, + wstring message, + uint64_t windowId + ); + + // Format a key name with current user's SID for Windows Hello. + // Returns the formatted key name wide string. + wstring userboundkey_format_key_name( + wstring keyName + ); + + // Delete/release a credential handle in VTL0. + void userboundkey_delete_credential( + uint64_t credential + ); + }; +}; diff --git a/rust/sdk/crates/libs/userboundkey-kcm/src/lib.rs b/rust/sdk/crates/libs/userboundkey-kcm/src/lib.rs index 14a7fece..2729b44e 100644 --- a/rust/sdk/crates/libs/userboundkey-kcm/src/lib.rs +++ b/rust/sdk/crates/libs/userboundkey-kcm/src/lib.rs @@ -1,5 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +//! Windows NGC (Next Generation Credentials) bindings for VBS enclave user-bound keys. +//! +//! This crate provides Rust bindings for the Windows KeyCredentialManager APIs +//! that support VBS enclave attestation. These APIs are generated using windows-bindgen. +//! +//! Key APIs: +//! - `KeyCredentialManager::RequestCreateAsync2` - Create credentials with VBS attestation +//! - `KeyCredentialManager::OpenAsync2` - Open credentials with VBS attestation +//! - `KeyCredentialManager::GetSecureId` - Get the secure identifier +//! - `KeyCredential::RequestDeriveSharedSecretAsync` - Derive shared secrets +//! - `KeyCredential::RetrieveAuthorizationContext` - Get authorization context + mod bindings; pub use bindings::*; diff --git a/rust/sdk/crates/libs/windows-enclave/src/lib.rs b/rust/sdk/crates/libs/windows-enclave/src/lib.rs index 394e4c5a..36516da9 100644 --- a/rust/sdk/crates/libs/windows-enclave/src/lib.rs +++ b/rust/sdk/crates/libs/windows-enclave/src/lib.rs @@ -12,6 +12,7 @@ mod manual_bindings; pub mod bcrypt { include!(concat!(env!("OUT_DIR"), "/bcrypt.rs")); + pub use super::manual_bindings::bcrypt::*; } pub mod vertdll { diff --git a/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/bcrypt.rs b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/bcrypt.rs new file mode 100644 index 00000000..e7a635cb --- /dev/null +++ b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/bcrypt.rs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Manual BCrypt bindings for types not included in the generated bindings. + +/// Version for BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO +pub const BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION: u32 = 1; + +/// Authenticated cipher mode info structure for AES-GCM +/// +/// This structure is used with BCryptEncrypt and BCryptDecrypt when using +/// authenticated encryption modes like AES-GCM. +/// +/// See: https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_authenticated_cipher_mode_info +#[repr(C)] +#[derive(Clone, Default)] +pub struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO { + pub cbSize: u32, + pub dwInfoVersion: u32, + pub pbNonce: *mut u8, + pub cbNonce: u32, + pub pbAuthData: *mut u8, + pub cbAuthData: u32, + pub pbTag: *mut u8, + pub cbTag: u32, + pub pbMacContext: *mut u8, + pub cbMacContext: u32, + pub cbAAD: u32, + pub cbData: u64, + pub dwFlags: u32, +} + +impl BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO { + /// Create a new initialized cipher mode info structure + pub fn new() -> Self { + Self { + cbSize: core::mem::size_of::() as u32, + dwInfoVersion: BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION, + ..Default::default() + } + } + + /// Create a new cipher mode info for encryption (tag will be written) + pub fn for_encrypt(nonce: &mut [u8], tag: &mut [u8]) -> Self { + Self { + cbSize: core::mem::size_of::() as u32, + dwInfoVersion: BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION, + pbNonce: nonce.as_mut_ptr(), + cbNonce: nonce.len() as u32, + pbTag: tag.as_mut_ptr(), + cbTag: tag.len() as u32, + ..Default::default() + } + } + + /// Create a new cipher mode info for decryption (tag is input) + pub fn for_decrypt(nonce: &[u8], tag: &[u8]) -> Self { + Self { + cbSize: core::mem::size_of::() as u32, + dwInfoVersion: BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION, + pbNonce: nonce.as_ptr() as *mut u8, + cbNonce: nonce.len() as u32, + pbTag: tag.as_ptr() as *mut u8, + cbTag: tag.len() as u32, + ..Default::default() + } + } +} diff --git a/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/mod.rs b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/mod.rs index 9506bc9b..efbfbcdc 100644 --- a/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/mod.rs +++ b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/mod.rs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +pub mod bcrypt; pub mod veinterop; pub mod vertdll; diff --git a/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/vertdll.rs b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/vertdll.rs index a6c9f95d..899a4350 100644 --- a/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/vertdll.rs +++ b/rust/sdk/crates/libs/windows-enclave/src/manual_bindings/vertdll.rs @@ -15,6 +15,7 @@ windows_link::link!("vertdll.dll" "system" fn EnclaveRestrictContainingProcessAc windows_link::link!("vertdll.dll" "system" fn LdrDisableThreadCalloutsForDll(baseaddress: *mut core::ffi::c_void) -> NTSTATUS); windows_link::link!("vertdll.dll" "system" fn LdrResolveDelayLoadedAPI(parentmodulebase: *const core::ffi::c_void, delayloaddescriptor: *const IMAGE_DELAYLOAD_DESCRIPTOR, failuredllhook: PDELAYLOAD_FAILURE_DLL_CALLBACK, failuresystemhook: PDELAYLOAD_FAILURE_SYSTEM_ROUTINE, thunkaddress: *mut IMAGE_THUNK_DATA32, flags: u32) -> *mut core::ffi::c_void); windows_link::link!("vertdll.dll" "system" fn RtlGetLastNtStatus() -> NTSTATUS); +windows_link::link!("vertdll.dll" "system" fn RtlNtStatusToDosError(status: NTSTATUS) -> u32); windows_link::link!("vertdll.dll" "system" fn RtlRaiseStatus(status: NTSTATUS)); windows_link::link!("vertdll.dll" "system" fn RtlUnhandledExceptionFilter(exceptionpointers: *const EXCEPTION_POINTERS) -> i32); @@ -58,3 +59,43 @@ pub const PAGE_WRITECOPY: u32 = 8u32; pub const MEM_IMAGE: PAGE_TYPE = 16777216u32; pub const MEM_MAPPED: u32 = 262144u32; pub const MEM_PRIVATE: u32 = 131072u32; + +// Enclave identity constants (used for manual struct definitions if needed) +// Note: ENCLAVE_IDENTITY and ENCLAVE_INFORMATION structs are in generated bindings +pub const IMAGE_ENCLAVE_LONG_ID_LENGTH: usize = 32; +pub const IMAGE_ENCLAVE_SHORT_ID_LENGTH: usize = 16; + +// Enclave type constants (not in generated bindings) +pub const ENCLAVE_TYPE_SGX: u32 = 0x00000001; +pub const ENCLAVE_TYPE_SGX2: u32 = 0x00000002; +pub const ENCLAVE_TYPE_VBS: u32 = 0x00000010; + +// Enclave policy flags +#[allow(dead_code)] +pub const IMAGE_ENCLAVE_POLICY_DEBUGGABLE: u32 = 0x0000_0001; + +// Enclave image flags +pub const IMAGE_ENCLAVE_FLAG_PRIMARY_IMAGE: u32 = 0x0000_0001; + +/// Enclave configuration structure. +/// See: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_enclave_config64 +#[repr(C)] +pub struct ImageEnclaveConfig { + pub size: u32, + pub minimum_required_config_size: u32, + pub policy_flags: u32, + pub number_of_imports: u32, + pub import_list: u32, + pub import_entry_size: u32, + pub family_id: [u8; IMAGE_ENCLAVE_SHORT_ID_LENGTH], + pub image_id: [u8; IMAGE_ENCLAVE_SHORT_ID_LENGTH], + pub image_version: u32, + pub security_version: u32, + pub enclave_size: usize, + pub number_of_threads: u32, + pub enclave_flags: u32, +} + +/// Minimum config size required (offset of enclave_flags field). +pub const IMAGE_ENCLAVE_MINIMUM_CONFIG_SIZE: u32 = + core::mem::offset_of!(ImageEnclaveConfig, enclave_flags) as u32; diff --git a/rust/sdk/crates/samples/userboundkey/Cargo.toml b/rust/sdk/crates/samples/userboundkey/Cargo.toml new file mode 100644 index 00000000..ffdaeb56 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/Cargo.toml @@ -0,0 +1,34 @@ +# Workspace for the user-bound key sample (host and enclave) +[workspace] +resolver = "3" +members = [ + "host", + "host/generated/userboundkey_sample_gen", + "enclave", + "enclave/generated/userboundkey_sample_gen", +] + +[workspace.package] +edition = "2024" + +[workspace.dependencies] +# SDK dependencies - use path for local development +vbsenclave-sdk-enclave = { version = "0.1.0", path = "../../libs/sdk-enclave" } +vbsenclave-sdk-host = { version = "0.1.0", path = "../../libs/sdk-host" } +windows-enclave = { version = "0.1.0", path = "../../libs/windows-enclave" } + +# External dependencies +edlcodegen-tools = { git = "https://github.com/microsoft/VbsEnclaveTooling" } +spin = "0.10.0" +widestring = { version = "1.1", default-features = false, features = ["alloc"] } +windows = "0.62.2" +windows-core = "0.62.2" + +# The enclave crate requires panic=abort (no_std cdylib). +# This applies to the entire workspace, but the host is a simple CLI +# so it doesn't critically need unwinding. +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/rust/sdk/crates/samples/userboundkey/README.md b/rust/sdk/crates/samples/userboundkey/README.md new file mode 100644 index 00000000..34cbc0a7 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/README.md @@ -0,0 +1,222 @@ +# User-Bound Key Sample + +This sample demonstrates how to use the VBS Enclave SDK's user-bound key functionality +to create Windows Hello-protected encryption keys that are sealed to the enclave. + +## Overview + +The sample consists of: + +- **`userboundkey_sample.edl`** - EDL interface defining the sample's trusted functions +- **`enclave/`** - Enclave DLL that implements the encryption/decryption logic +- **`host/`** - Host application with a menu-driven interface + +## Features + +1. **Create User-Bound Key** - Creates a new encryption key protected by Windows Hello + and sealed to the enclave using VBS enclave sealing. + +2. **Load User-Bound Key** - Loads an existing sealed key (triggers Windows Hello prompt). + +3. **Delete User-Bound Key** - Removes the sealed key file. + +4. **Encrypt Data** - Loads the key and encrypts user-provided text. + +5. **Decrypt Data** - Loads the key and decrypts previously encrypted data. + +## Security Model + +- The encryption key is generated inside the VTL1 enclave +- The key is sealed using VBS enclave sealing (bound to enclave identity) +- Windows Hello biometric/PIN is required to access the key +- The cache configuration is set securely in VTL1 (VTL0 cannot influence it) +- Key material never leaves the enclave in plaintext + +## Building + +### Prerequisites + +Building the enclave DLL requires: +- **Windows 11 Build 26100.2314 or later** or **Windows Server 2025 or later** +- Windows SDK 10.0.26100.0 or later with VBS enclave libraries (`ucrt_enclave`) +- `veinterop.lib` from SDK 10.0.26100.7463 or later (for VBS enclave interop APIs) +- Visual Studio 2022 with MSVC enclave toolchain +- A code signing certificate for VBS enclaves (see below) + +To verify your SDK has the required libraries: +```powershell +Test-Path "C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\ucrt_enclave" +``` + +### Quick Build (Recommended) + +Use the provided build script that generates EDL bindings, builds, and signs in one step: + +```powershell +cd C:\VbsEnclaveTooling\rust\sdk + +# Build with your signing certificate +.\generate_and_build_crates.ps1 -CertName "YourCertificateName" + +# For release build +.\generate_and_build_crates.ps1 -Configuration release -CertName "YourCertificateName" +``` + +The script performs: +1. **Generate EDL bindings** - Creates Rust bindings from all EDL files (SDK + samples) +2. **Build** - Compiles entire SDK workspace including samples +3. **Sign** - Signs all enclave DLLs with veiid.exe and signtool + +### EDL Generation Only + +If you're working on EDL files and want fast intellisense updates without building: + +```powershell +cd C:\VbsEnclaveTooling\rust\sdk + +# Generate EDL bindings only (fast) +.\generate_codegen_for_workspace.ps1 +``` + +### Manual Build Commands + +If you prefer manual steps: + +```powershell +# From the rust/sdk directory + +# Build host application only (always works) +cargo build -p userboundkey-sample-host + +# Build enclave (requires proper Windows SDK with ucrt_enclave) +cargo build -p userboundkey-sample-enclave + +# Sign the enclave (required before running) +..\..\scripts\sign-enclave.ps1 -DllPath "target\debug\userboundkey_sample_enclave.dll" -CertName "YourCertificateName" + +# Check compilation without linking (useful for CI) +cargo check --all-features +``` + +### Creating a Test Signing Certificate + +To create a self-signed test certificate for development: + +```powershell +# Create a self-signed certificate (run as Administrator) +$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=MyTestEnclaveCert" -CertStoreLocation Cert:\CurrentUser\My +``` + +## Running + +### Prerequisites for Running + +1. **Windows 11** with VBS (Virtualization Based Security) enabled +2. **Windows Hello** configured (PIN, fingerprint, or facial recognition) +3. **Developer Mode** enabled (optional, for debug enclaves) + +To verify VBS is enabled: +```powershell +# Check VBS status +Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard | Select-Object VirtualizationBasedSecurityStatus +# 2 = Running +``` + +### Steps to Run + +1. **Build the sample using the build script:** + ```powershell + cd C:\VbsEnclaveTooling\rust\sdk + .\generate_and_build_crates.ps1 -CertName "YourCertificateName" + ``` + +2. **Run the sample:** + ```powershell + cd C:\VbsEnclaveTooling\rust\sdk\target\debug + .\userboundkey-sample.exe + ``` + +3. **Use the interactive menu:** + ``` + === User-Bound Key Sample (Rust) === + + *** User-Bound Key Management and Encryption Menu *** + 1. Create UB Key <- First, create a new user-bound key + 2. Load UB Key <- Load existing key from file + 3. Delete UB Key <- Delete the key file + 4. Encrypt Data <- Encrypt text (requires key) + 5. Decrypt Data <- Decrypt data (requires key) + 6. Exit + ``` + +### Typical Workflow + +1. **Select option 1** to create a new user-bound key + - Windows Hello prompt will appear (PIN/biometric) + - Key is created and saved to `MyEncryptionKey-001` + +2. **Select option 4** to encrypt data + - Enter text when prompted + - Windows Hello prompt may appear to unlock the key + - Encrypted data saved to `encrypted_userbound` + +3. **Select option 5** to decrypt data + - Windows Hello prompt may appear + - Decrypted text is displayed + +### Troubleshooting + +**"Failed to load enclave"** +- Ensure VBS is enabled in Windows settings +- Check that the enclave DLL exists next to the host executable +- Try running as Administrator + +**"Windows Hello prompt does not appear"** +- Ensure Windows Hello is configured in Settings > Accounts > Sign-in options +- Try creating a new key (option 1) + +**"Key needs resealing"** +- This is normal after enclave updates +- The sample automatically handles resealing + +## Files Created + +When running the sample, the following files are created in the current directory: + +- `MyEncryptionKey-001` - The sealed encryption key +- `encrypted_userbound` - Encrypted data output + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Host Application (VTL0) │ +│ - Menu interface │ +│ - File I/O for sealed keys and encrypted data │ +│ - Calls enclave via EDL interface │ +└─────────────────────────────────────────────────────────────┘ + │ + │ EDL Interface + │ (CreateUserBoundKey, LoadAndEncrypt, LoadAndDecrypt) + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Enclave DLL (VTL1) │ +│ - Creates/loads user-bound keys via SDK │ +│ - Performs encryption/decryption │ +│ - Manages key caching │ +└─────────────────────────────────────────────────────────────┘ + │ + │ SDK userboundkey module + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Windows Hello / KCM (VTL0) │ +│ - Biometric/PIN authentication │ +│ - Key Credential Manager │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Key Reseal + +If the enclave's sealing key has changed (e.g., after an enclave update), the SDK +will automatically detect this and provide resealed key bytes. The sample handles +this by saving the resealed key back to disk. diff --git a/rust/sdk/crates/samples/userboundkey/build.ps1 b/rust/sdk/crates/samples/userboundkey/build.ps1 new file mode 100644 index 00000000..f05e356b --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/build.ps1 @@ -0,0 +1,65 @@ +# Build script for the User-Bound Key sample. +# +# This script builds and optionally signs the userboundkey sample enclave. +# +# Usage: +# .\build.ps1 # Debug build only +# .\build.ps1 -Configuration release # Release build only +# .\build.ps1 -CertName "MyCert" # Debug build + sign +# .\build.ps1 -Clean # Clean and rebuild + +param( + [ValidateSet("debug", "release")] + [string]$Configuration = "debug", + + [switch]$Clean, + + [string]$CertName +) + +$errorActionPreference = "Stop" + +# Locate shared scripts folder +. "$PSScriptRoot\..\..\..\..\scripts\get_common_paths.ps1" + +Write-Host "`n=== User-Bound Key Sample Build ===" -ForegroundColor Cyan +Write-Host "Configuration: $Configuration" +if ($CertName) { + Write-Host "Certificate: $CertName (will sign enclave)" +} + +# Clean if requested +if ($Clean) { + Write-Host "`nCleaning build artifacts..." -ForegroundColor Yellow + cargo clean +} + +# Build the sample workspace +Write-Host "`nBuilding sample..." -ForegroundColor Yellow +. "$scriptsDir\invoke_cargo_build.ps1" -Path $PSScriptRoot -Configuration $Configuration + +# Sign enclave DLL if certificate is provided +if ($CertName) { + Write-Host "`nSigning enclave DLL..." -ForegroundColor Yellow + + $signScript = Join-Path $scriptsDir "sign-enclave.ps1" + $targetDir = if ($Configuration -eq "release") { "release" } else { "debug" } + $targetPath = Join-Path $PSScriptRoot "target\$targetDir" + + $enclaveDlls = Get-ChildItem -Path $targetPath -Filter "*_enclave.dll" -ErrorAction SilentlyContinue + + if (-not $enclaveDlls) { + Write-Host "No enclave DLLs found to sign." -ForegroundColor Yellow + } else { + foreach ($dll in $enclaveDlls) { + Write-Host "Signing $($dll.Name)..." -ForegroundColor Gray + & $signScript -DllPath $dll.FullName -CertName $CertName + if ($LASTEXITCODE -ne 0) { throw "Failed to sign $($dll.Name)" } + } + Write-Host "Enclave signed." -ForegroundColor Green + } +} + +Write-Host "`n=== Sample Build Complete ===" -ForegroundColor Cyan +$targetDir = if ($Configuration -eq "release") { "release" } else { "debug" } +Write-Host "Output: $(Join-Path $PSScriptRoot "target\$targetDir")" diff --git a/rust/sdk/crates/samples/userboundkey/enclave/.cargo/config.toml b/rust/sdk/crates/samples/userboundkey/enclave/.cargo/config.toml new file mode 100644 index 00000000..7cb9a9a8 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/enclave/.cargo/config.toml @@ -0,0 +1,5 @@ +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/rust/sdk/crates/samples/userboundkey/enclave/Cargo.toml b/rust/sdk/crates/samples/userboundkey/enclave/Cargo.toml new file mode 100644 index 00000000..c061155f --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/enclave/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "userboundkey-sample-enclave" +version = "0.1.0" +edition = { workspace = true } + +[lib] +# Make sure crate builds a dll and not a static lib +crate-type = ["cdylib"] + +[build-dependencies] +edlcodegen-tools = { workspace = true } + +[dependencies] +# Generated code from userboundkey_sample.edl +userboundkey_sample_enclave_gen = { path = "generated/userboundkey_sample_gen" } +# The SDK provides the userboundkey implementation +vbsenclave-sdk-enclave = { workspace = true } +# For enclave configuration constants +windows-enclave = { workspace = true } diff --git a/rust/sdk/crates/samples/userboundkey/enclave/build.rs b/rust/sdk/crates/samples/userboundkey/enclave/build.rs new file mode 100644 index 00000000..093c74dd --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/enclave/build.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +fn main() -> Result<(), Box> { + // Emits the linker arguments needed to build this crate as a VBS enclave. + edlcodegen_tools::set_enclave_linker_flags(); + edlcodegen_tools::link_win_sdk_enclave_libs()?; + Ok(()) +} diff --git a/rust/sdk/crates/samples/userboundkey/enclave/src/edl_impls.rs b/rust/sdk/crates/samples/userboundkey/enclave/src/edl_impls.rs new file mode 100644 index 00000000..af24869e --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/enclave/src/edl_impls.rs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Enclave implementation for the user-bound key sample. +//! +//! Implements trusted EDL functions for creating, loading, and using +//! user-bound encryption keys protected by Windows Hello. + +#![allow(unused_imports)] +#![allow(non_snake_case)] + +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; +use userboundkey_sample_enclave_gen::AbiError; +use userboundkey_sample_enclave_gen::implementation::trusted::Trusted; +use userboundkey_sample_enclave_gen::implementation::types::edl::WString; +use userboundkey_sample_enclave_gen::implementation::types::{DecryptResult, EncryptResult}; +use userboundkey_sample_enclave_gen::stubs::untrusted::debug_print; + +// Import SDK functions and types +use vbsenclave_sdk_enclave::userboundkey::{ + EnclaveSealingIdentityPolicy, SymmetricKeyHandle, TAG_SIZE, U16Str, ZERO_NONCE, + create_user_bound_key, decrypt, encrypt, keyCredentialCacheConfig, load_user_bound_key, + reseal_user_bound_key, +}; + +/// Runtime policy: no dynamic debug allowed +const RUNTIME_POLICY_NO_DEBUG: u32 = 0; + +/// VTL1 function to create secure cache configuration. +/// This ensures VTL0 has no influence over cache configuration values. +fn create_secure_cache_config() -> keyCredentialCacheConfig { + keyCredentialCacheConfig { + cacheOption: 0, // NoCache - most secure option + cacheTimeoutInSeconds: 0, // No timeout when not caching + cacheUsageCount: 0, // No usage count when not caching + } +} + +/// Global encryption key storage. +/// In no_std enclave, we use a static mut with unsafe access. +static mut ENCRYPTION_KEY: Option = None; + +#[allow(static_mut_refs)] +fn is_ubk_loaded() -> bool { + unsafe { ENCRYPTION_KEY.is_some() } +} + +#[allow(static_mut_refs)] +fn set_encryption_key(key: SymmetricKeyHandle) { + unsafe { + ENCRYPTION_KEY = Some(key); + } +} + +#[allow(static_mut_refs)] +fn get_encryption_key() -> Option<&'static SymmetricKeyHandle> { + unsafe { ENCRYPTION_KEY.as_ref() } +} + +/// Helper function to ensure user-bound key is loaded. +/// Handles initial load attempt and optional reseal if needed. +fn ensure_user_bound_key_loaded( + hello_key_name: &U16Str, + pin_message: &U16Str, + window_id: u64, + secured_encryption_key_bytes: &[u8], + needs_reseal: &mut bool, + resealed_encryption_key_bytes: &mut Vec, +) -> Result<(), AbiError> { + // Only load if not already loaded + if is_ubk_loaded() { + return Ok(()); + } + + let secure_config = create_secure_cache_config(); + + // Try to load the user-bound key, with fallback to reseal on failure + let loaded_key_bytes = match load_user_bound_key( + hello_key_name, + &secure_config, + pin_message, + window_id, + secured_encryption_key_bytes, + ) { + Ok((key_bytes, stale)) => { + // If stale, mark for reseal + if stale { + *needs_reseal = true; + if let Ok(resealed) = reseal_user_bound_key( + &key_bytes, + EnclaveSealingIdentityPolicy::EnclaveIdentity, + RUNTIME_POLICY_NO_DEBUG, + ) { + *resealed_encryption_key_bytes = resealed; + } + } + key_bytes + } + Err(e) => { + // First load failed, attempt reseal and retry + let resealed_bytes = reseal_user_bound_key( + secured_encryption_key_bytes, + EnclaveSealingIdentityPolicy::EnclaveIdentity, + RUNTIME_POLICY_NO_DEBUG, + ) + .map_err(|_| AbiError::Hresult(e.to_hresult()))?; + + *needs_reseal = true; + *resealed_encryption_key_bytes = resealed_bytes.clone(); + + // Retry loading with resealed bytes + let (key_bytes, _) = load_user_bound_key( + hello_key_name, + &secure_config, + pin_message, + window_id, + &resealed_bytes, + ) + .map_err(|e2| AbiError::Hresult(e2.to_hresult()))?; + + key_bytes + } + }; + + // Create symmetric key from loaded raw key material + let symmetric_key = SymmetricKeyHandle::from_bytes(&loaded_key_bytes) + .map_err(|_| AbiError::Hresult(-2147467259))?; // E_FAIL + set_encryption_key(symmetric_key); + + Ok(()) +} + +/// Enclave implementation of the Trusted trait +pub struct EnclaveImpl; + +impl Trusted for EnclaveImpl { + /// Create a user-bound key. + fn CreateUserBoundKey( + helloKeyName: &WString, + pinMessage: &WString, + windowId: u64, + keyCredentialCreationOption: u32, + ) -> Result, AbiError> { + debug_print(&String::from("CreateUserBoundKey: Starting"))?; + + let cache_config = create_secure_cache_config(); + + debug_print(&format!( + "CreateUserBoundKey: windowId={}, option={}", + windowId, keyCredentialCreationOption + ))?; + + debug_print(&String::from( + "CreateUserBoundKey: Calling SDK create_user_bound_key...", + ))?; + + // Create user-bound key with enclave sealing + let sealed_key = create_user_bound_key( + U16Str::from_slice(&helloKeyName.wchars), + &cache_config, + U16Str::from_slice(&pinMessage.wchars), + windowId, + EnclaveSealingIdentityPolicy::EnclaveIdentity, + RUNTIME_POLICY_NO_DEBUG, + keyCredentialCreationOption, + ) + .map_err(|e| { + let _ = debug_print(&format!("CreateUserBoundKey: SDK error: {:?}", e)); + AbiError::Hresult(e.to_hresult()) + })?; + + debug_print(&format!( + "CreateUserBoundKey: Success! Got {} sealed bytes", + sealed_key.len() + ))?; + + // Return sealed key bytes - do NOT create symmetric key here + // The key is created on load, not on create. + Ok(sealed_key) + } + + /// Load user-bound key and encrypt data. + fn LoadUserBoundKeyAndEncryptData( + helloKeyName: &WString, + pinMessage: &WString, + windowId: u64, + securedEncryptionKeyBytes: &Vec, + inputData: &WString, + ) -> Result { + let mut needs_reseal = false; + let mut resealed_bytes = Vec::new(); + + // Ensure key is loaded (handles reseal if needed) + ensure_user_bound_key_loaded( + U16Str::from_slice(&helloKeyName.wchars), + U16Str::from_slice(&pinMessage.wchars), + windowId, + securedEncryptionKeyBytes, + &mut needs_reseal, + &mut resealed_bytes, + )?; + + // Get the key and encrypt + let key = get_encryption_key().ok_or(AbiError::Hresult(-2147024809))?; + + // Convert input to bytes (UTF-16 LE data) + let input_bytes: Vec = inputData + .wchars + .iter() + .flat_map(|c| c.to_le_bytes()) + .collect(); + + // Encrypt using AES-GCM with zero nonce + let (encrypted, tag) = encrypt(key, &input_bytes, &ZERO_NONCE) + .map_err(|e| AbiError::Hresult(e.to_hresult()))?; + + // Combine tag and encrypted data into single output + // Format: [tag_size (4 bytes)][tag_data][encrypted_data] + let tag_size = tag.len() as u32; + let mut combined = Vec::with_capacity(4 + tag.len() + encrypted.len()); + combined.extend_from_slice(&tag_size.to_le_bytes()); + combined.extend_from_slice(&tag); + combined.extend(encrypted); + + Ok(EncryptResult { + combinedOutputData: combined, + needsReseal: needs_reseal, + resealedEncryptionKeyBytes: resealed_bytes, + }) + } + + /// Load user-bound key and decrypt data. + fn LoadUserBoundKeyAndDecryptData( + helloKeyName: &WString, + pinMessage: &WString, + windowId: u64, + securedEncryptionKeyBytes: &Vec, + combinedInputData: &Vec, + ) -> Result { + let mut needs_reseal = false; + let mut resealed_bytes = Vec::new(); + + // Parse combined input: [tag_size (4 bytes)][tag_data][encrypted_data] + if combinedInputData.len() < 4 { + return Err(AbiError::Hresult(-2147024809)); // E_INVALIDARG + } + + // Read tag size from first 4 bytes + let tag_size = u32::from_le_bytes([ + combinedInputData[0], + combinedInputData[1], + combinedInputData[2], + combinedInputData[3], + ]) as usize; + + // Validate tag size + if tag_size == 0 || tag_size > combinedInputData.len() - 4 { + return Err(AbiError::Hresult(-2147024809)); // E_INVALIDARG + } + + // Extract tag and encrypted data + let tag = &combinedInputData[4..4 + tag_size]; + let encrypted = &combinedInputData[4 + tag_size..]; + + // Ensure key is loaded + ensure_user_bound_key_loaded( + U16Str::from_slice(&helloKeyName.wchars), + U16Str::from_slice(&pinMessage.wchars), + windowId, + securedEncryptionKeyBytes, + &mut needs_reseal, + &mut resealed_bytes, + )?; + + // Get the key and decrypt + let key = get_encryption_key().ok_or(AbiError::Hresult(-2147024809))?; + + // Convert tag slice to fixed-size array + let mut tag_array = [0u8; TAG_SIZE]; + if tag.len() != TAG_SIZE { + return Err(AbiError::Hresult(-2147024809)); // E_INVALIDARG + } + tag_array.copy_from_slice(tag); + + // Decrypt using AES-GCM with zero nonce + let decrypted_bytes = decrypt(key, encrypted, &ZERO_NONCE, &tag_array) + .map_err(|e| AbiError::Hresult(e.to_hresult()))?; + + // Convert decrypted bytes to WString (UTF-16 LE) + let utf16: Vec = decrypted_bytes + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + + // Create WString from UTF-16 data + let decrypted_string = WString { wchars: utf16 }; + + Ok(DecryptResult { + decryptedData: decrypted_string, + needsReseal: needs_reseal, + resealedEncryptionKeyBytes: resealed_bytes, + }) + } +} diff --git a/rust/sdk/crates/samples/userboundkey/enclave/src/lib.rs b/rust/sdk/crates/samples/userboundkey/enclave/src/lib.rs new file mode 100644 index 00000000..4bd971e8 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/enclave/src/lib.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! VBS Enclave for User-Bound Key Sample +//! +//! This enclave demonstrates how to use the VBS enclave SDK's user-bound key +//! functionality with Windows Hello protection. + +#![no_std] +extern crate alloc; + +mod edl_impls; + +use core::panic::PanicInfo; +use windows_enclave::vertdll::{ + IMAGE_ENCLAVE_FLAG_PRIMARY_IMAGE, IMAGE_ENCLAVE_MINIMUM_CONFIG_SIZE, + IMAGE_ENCLAVE_POLICY_DEBUGGABLE, ImageEnclaveConfig, +}; + +// Panic handler for no_std environment +#[panic_handler] +fn panic(_panic: &PanicInfo<'_>) -> ! { + loop {} +} + +// Export the sample's enclave functions (CreateUserBoundKey, LoadUserBoundKeyAndEncryptData, etc.) +userboundkey_sample_enclave_gen::export_enclave_functions!(edl_impls::EnclaveImpl); + +// Export the SDK's enclave functions (userboundkey callbacks, attestation, etc.) +// This single macro call handles all SDK features and hides internal implementation details. +vbsenclave_sdk_enclave::export_sdk_enclave_functions!(); + +/// Enclave policy flags - enable debugging in debug builds +pub const ENCLAVE_CONFIG_POLICY_FLAGS: u32 = { + #[cfg(debug_assertions)] + { + IMAGE_ENCLAVE_POLICY_DEBUGGABLE + } + #[cfg(not(debug_assertions))] + { + 0 + } +}; + +/// Enclave configuration structure required for VBS enclave loading. +/// This must be present and correctly configured for the enclave to load. +#[unsafe(no_mangle)] +#[allow(non_upper_case_globals)] +pub static __enclave_config: ImageEnclaveConfig = ImageEnclaveConfig { + size: core::mem::size_of::() as u32, + minimum_required_config_size: IMAGE_ENCLAVE_MINIMUM_CONFIG_SIZE, + policy_flags: ENCLAVE_CONFIG_POLICY_FLAGS, + number_of_imports: 0, + import_list: 0, + import_entry_size: 0, + // Family ID - unique to your application family + family_id: [0xFE, 0xFE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + // Image ID - unique to this specific enclave + image_id: [ + 0x55, 0x42, 0x4B, 0x53, // "UBKS" - User Bound Key Sample + 0x00, 0x01, 0x00, 0x00, // Version 0.1.0.0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + image_version: 1, + security_version: 1, + enclave_size: 0x2000_0000, // 512 MB - must match host's create request + number_of_threads: 16, + enclave_flags: IMAGE_ENCLAVE_FLAG_PRIMARY_IMAGE, +}; + +/// DLL entry point - required for the enclave DLL. +#[unsafe(no_mangle)] +#[allow(non_snake_case)] +pub extern "system" fn DllMain( + _dll_handle: *const core::ffi::c_void, + _reason: u32, + _reserved: *mut core::ffi::c_void, +) -> bool { + true +} diff --git a/rust/sdk/crates/samples/userboundkey/host/Cargo.toml b/rust/sdk/crates/samples/userboundkey/host/Cargo.toml new file mode 100644 index 00000000..5560dde3 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/host/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "userboundkey-sample-host" +version = "0.1.0" +edition = { workspace = true } + +[[bin]] +name = "userboundkey-sample" +path = "src/main.rs" + +[dependencies] +# Generated code from userboundkey_sample.edl +userboundkey_sample_host_gen = { path = "generated/userboundkey_sample_gen" } +# The SDK provides the enclave loading and VTL0 userboundkey callbacks +vbsenclave-sdk-host = { workspace = true } +# Windows APIs +windows = { workspace = true, features = ["Win32_Foundation", "Security_Cryptography", "Storage_Streams", "Win32_UI_WindowsAndMessaging"] } diff --git a/rust/sdk/crates/samples/userboundkey/host/src/edl_impls.rs b/rust/sdk/crates/samples/userboundkey/host/src/edl_impls.rs new file mode 100644 index 00000000..60f6630d --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/host/src/edl_impls.rs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Host-side implementation of EDL untrusted functions. + +use userboundkey_sample_host_gen::AbiError; +use userboundkey_sample_host_gen::implementation::untrusted::Untrusted; + +/// Implementation of the Untrusted trait for the sample +pub struct UntrustedImpl; + +impl Untrusted for UntrustedImpl { + /// Debug print function - prints to console from enclave + fn debug_print(message: &String) -> Result<(), AbiError> { + println!("[Enclave] {}", message); + Ok(()) + } +} diff --git a/rust/sdk/crates/samples/userboundkey/host/src/main.rs b/rust/sdk/crates/samples/userboundkey/host/src/main.rs new file mode 100644 index 00000000..8658b43b --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/host/src/main.rs @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! User-Bound Key Sample Host Application +//! +//! Demonstrates loading a VBS enclave and using user-bound keys with Windows Hello. + +#![allow(dead_code)] + +mod edl_impls; + +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; + +// VBS Enclave SDK for loading enclaves +use vbsenclave_sdk_host::KeyCredentialManager; +use vbsenclave_sdk_host::enclave::{EnclaveHandle, megabytes}; +use vbsenclave_sdk_host::register_sdk_callbacks; + +// Windows APIs for buffer conversion +use windows::Security::Cryptography::CryptographicBuffer; +use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow; +use windows::core::Interface; + +// Generated host stubs for enclave calls +use userboundkey_sample_host_gen::implementation::types::edl::WString; +use userboundkey_sample_host_gen::stubs::trusted::UntrustedImpl; + +const KEY_NAME: &str = "MyEncryptionKey-001"; +const PIN_MESSAGE: &str = "User-Bound Key Sample"; + +fn get_key_file_path() -> PathBuf { + std::env::current_dir().unwrap().join(KEY_NAME) +} + +fn get_encrypted_file_path() -> PathBuf { + std::env::current_dir().unwrap().join("encrypted_userbound") +} + +fn save_binary_data(path: &PathBuf, data: &[u8]) -> io::Result<()> { + fs::write(path, data) +} + +fn load_binary_data(path: &PathBuf) -> io::Result> { + fs::read(path) +} + +fn print_menu(is_key_loaded: bool) { + println!("\n*** User-Bound Key Management and Encryption Menu ***"); + println!("1. Create UB Key"); + println!("2. Load UB Key"); + println!("3. Delete UB Key"); + println!("4. Encrypt Data"); + println!("5. Decrypt Data"); + println!("6. Exit"); + println!( + "Key Status: {}", + if is_key_loaded { + "Loaded" + } else { + "Not Loaded" + } + ); + print!("Enter your choice: "); + io::stdout().flush().unwrap(); +} + +/// Create a WString from a Rust string (without null terminator - SDK adds it when needed) +fn to_wstring(s: &str) -> WString { + let wchars: Vec = s.encode_utf16().collect(); + WString { wchars } +} + +/// Get secure ID from Windows Hello. +/// +/// Returns the secure ID buffer that identifies this Windows Hello user. +fn get_secure_id_from_windows_hello() -> Result, Box> { + // Get the secure ID buffer from KeyCredentialManager + let secure_id_buffer = KeyCredentialManager::GetSecureId()?; + + // Cast from userboundkey_kcm::IBuffer to windows::Storage::Streams::IBuffer + let windows_buffer: windows::Storage::Streams::IBuffer = secure_id_buffer.cast()?; + + // Convert IBuffer to Vec using CryptographicBuffer + let mut byte_array = windows::core::Array::::new(); + CryptographicBuffer::CopyToByteArray(&windows_buffer, &mut byte_array)?; + + Ok(byte_array.to_vec()) +} + +/// Load the enclave DLL and return the wrapper interface +fn load_enclave( + enclave_path: &str, +) -> Result<(EnclaveHandle, UntrustedImpl), Box> { + println!("Loading enclave from: {}", enclave_path); + + // Get the secure ID from Windows Hello - required for user-bound key operations + let owner_id = get_secure_id_from_windows_hello()?; + println!( + "Got secure ID from Windows Hello ({} bytes)", + owner_id.len() + ); + + // Create, load, and initialize the enclave (512MB) + let enclave = + EnclaveHandle::create_and_initialize(enclave_path, megabytes(512), Some(&owner_id))?; + + // Create the wrapper interface for enclave calls + let wrapper = UntrustedImpl::new(enclave.as_ptr()); + + // Register the sample's VTL0 callbacks (for debug_print, etc.) + wrapper + .register_vtl0_callbacks::() + .map_err(|e| { + format!( + "Failed to register sample callbacks: 0x{:08X}", + e.to_hresult().0 + ) + })?; + + // Register all SDK VTL0 callbacks so enclave can call back to host + // This single function call handles all SDK features (userboundkey, etc.) + register_sdk_callbacks(enclave.as_ptr()).map_err(|e| { + format!( + "Failed to register SDK callbacks: 0x{:08X}", + e.to_hresult().0 + ) + })?; + + println!("Enclave loaded and callbacks registered successfully."); + Ok((enclave, wrapper)) +} + +fn main() -> Result<(), Box> { + println!("=== User-Bound Key Sample (Rust) ==="); + println!(); + + // Load the enclave (LoadEnclaveImageW searches current directory and PATH) + let (_enclave_handle, enclave) = load_enclave("userboundkey_sample_enclave.dll")?; + + let key_file_path = get_key_file_path(); + let encrypted_file_path = get_encrypted_file_path(); + let mut secured_key_bytes: Vec = Vec::new(); + let mut is_key_loaded = false; + + // Try to load existing key from file + if key_file_path.exists() + && let Ok(data) = load_binary_data(&key_file_path) + { + secured_key_bytes = data; + is_key_loaded = true; + println!("Found existing key file: {}", key_file_path.display()); + } + + // Window ID - use foreground window for Windows Hello prompts + // GetForegroundWindow returns the handle of the window with which the user is currently working + let window_id: u64 = unsafe { GetForegroundWindow().0 as u64 }; + + loop { + print_menu(is_key_loaded); + + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_err() { + continue; + } + + let choice: u32 = match input.trim().parse() { + Ok(n) => n, + Err(_) => { + println!("Invalid input."); + continue; + } + }; + + match choice { + 1 => { + // Create User-Bound Key + println!("\n[Create User-Bound Key]"); + println!("Window ID: 0x{:X}", window_id); + println!("Calling enclave... (Windows Hello prompt should appear)"); + + let key_name = to_wstring(KEY_NAME); + let pin_message = to_wstring(PIN_MESSAGE); + + // KeyCredentialCreationOption::ReplaceExisting = 1 + const REPLACE_EXISTING: u32 = 1; + + match enclave.CreateUserBoundKey( + &key_name, + &pin_message, + window_id, + REPLACE_EXISTING, + ) { + Ok(key_bytes) => { + if save_binary_data(&key_file_path, &key_bytes).is_ok() { + println!("Key created and saved to: {}", key_file_path.display()); + secured_key_bytes = key_bytes; + is_key_loaded = true; + } else { + eprintln!("Failed to save key to file."); + } + } + Err(e) => { + eprintln!("Failed to create key: {:?}", e); + } + } + } + 2 => { + // Load User-Bound Key + println!("\n[Load User-Bound Key]"); + if key_file_path.exists() { + match load_binary_data(&key_file_path) { + Ok(data) => { + secured_key_bytes = data; + is_key_loaded = true; + println!("Key loaded from: {}", key_file_path.display()); + } + Err(e) => { + eprintln!("Failed to load key: {}", e); + } + } + } else { + println!("No key file found. Create one first."); + } + } + 3 => { + // Delete User-Bound Key + println!("\n[Delete User-Bound Key]"); + if fs::remove_file(&key_file_path).is_ok() { + println!("Key file deleted."); + secured_key_bytes.clear(); + is_key_loaded = false; + } else { + println!("No key file to delete."); + } + } + 4 => { + // Encrypt Data + println!("\n[Encrypt Data]"); + if !is_key_loaded { + println!("Load or create a key first."); + continue; + } + + print!("Enter text to encrypt: "); + io::stdout().flush().unwrap(); + let mut text = String::new(); + if io::stdin().read_line(&mut text).is_err() { + continue; + } + + let key_name = to_wstring(KEY_NAME); + let pin_message = to_wstring(PIN_MESSAGE); + let input_data = to_wstring(text.trim()); + + match enclave.LoadUserBoundKeyAndEncryptData( + &key_name, + &pin_message, + window_id, + &secured_key_bytes, + &input_data, + ) { + Ok(result) => { + // Check if key needs resealing + if result.needsReseal && !result.resealedEncryptionKeyBytes.is_empty() { + println!("Key was resealed, updating stored key..."); + secured_key_bytes = result.resealedEncryptionKeyBytes; + if let Err(e) = save_binary_data(&key_file_path, &secured_key_bytes) { + eprintln!("Warning: Failed to save resealed key: {}", e); + } + } + + // Save encrypted data + if save_binary_data(&encrypted_file_path, &result.combinedOutputData) + .is_ok() + { + println!("Encrypted data saved to: {}", encrypted_file_path.display()); + println!("Encrypted {} bytes.", result.combinedOutputData.len()); + } else { + eprintln!("Failed to save encrypted data."); + } + } + Err(e) => { + eprintln!("Encryption failed: {:?}", e); + } + } + } + 5 => { + // Decrypt Data + println!("\n[Decrypt Data]"); + if !encrypted_file_path.exists() { + println!("No encrypted data file found."); + continue; + } + if !is_key_loaded { + println!("Load or create a key first."); + continue; + } + + let combined_data = match load_binary_data(&encrypted_file_path) { + Ok(data) => data, + Err(e) => { + eprintln!("Failed to load encrypted data: {}", e); + continue; + } + }; + + let key_name = to_wstring(KEY_NAME); + let pin_message = to_wstring(PIN_MESSAGE); + + match enclave.LoadUserBoundKeyAndDecryptData( + &key_name, + &pin_message, + window_id, + &secured_key_bytes, + &combined_data, + ) { + Ok(result) => { + // Check if key needs resealing + if result.needsReseal && !result.resealedEncryptionKeyBytes.is_empty() { + println!("Key was resealed, updating stored key..."); + secured_key_bytes = result.resealedEncryptionKeyBytes; + if let Err(e) = save_binary_data(&key_file_path, &secured_key_bytes) { + eprintln!("Warning: Failed to save resealed key: {}", e); + } + } + + // Convert WString to Rust String + let decrypted_text: String = result + .decryptedData + .wchars + .iter() + .take_while(|&&c| c != 0) + .map(|&c| char::from_u32(c as u32).unwrap_or('?')) + .collect(); + + println!("Decrypted data: {}", decrypted_text); + } + Err(e) => { + eprintln!("Decryption failed: {:?}", e); + } + } + } + 6 => { + println!("Exiting."); + break; + } + _ => println!("Invalid choice."), + } + } + + // EnclaveHandle will automatically terminate and delete the enclave on drop + println!("Enclave cleanup complete."); + Ok(()) +} diff --git a/rust/sdk/crates/samples/userboundkey/userboundkey_sample.edl b/rust/sdk/crates/samples/userboundkey/userboundkey_sample.edl new file mode 100644 index 00000000..1313f1a9 --- /dev/null +++ b/rust/sdk/crates/samples/userboundkey/userboundkey_sample.edl @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Sample EDL for user-bound key operations. +// Note: This sample uses the SDK's userboundkey module which has its own +// EDL callbacks. The SDK host registers those callbacks separately. + +enclave +{ + // Result from encrypt operation + struct EncryptResult + { + // Combined output: [tag_size(4 bytes)][tag][encrypted_data] + vector combinedOutputData; + bool needsReseal; + vector resealedEncryptionKeyBytes; + }; + + // Result from decrypt operation + struct DecryptResult + { + wstring decryptedData; + bool needsReseal; + vector resealedEncryptionKeyBytes; + }; + + // Trusted functions: Called from host (VTL0) into enclave (VTL1) + trusted + { + // Create a new user-bound encryption key protected by Windows Hello. + // Returns the sealed key bytes that should be stored by the host. + vector CreateUserBoundKey( + wstring helloKeyName, + wstring pinMessage, + uintptr_t windowId, + uint32_t keyCredentialCreationOption + ); + + // Load an existing user-bound key and encrypt data. + // Handles reseal if the key was sealed with an older enclave version. + EncryptResult LoadUserBoundKeyAndEncryptData( + wstring helloKeyName, + wstring pinMessage, + uintptr_t windowId, + vector securedEncryptionKeyBytes, + wstring inputData + ); + + // Load an existing user-bound key and decrypt data. + // Handles reseal if the key was sealed with an older enclave version. + DecryptResult LoadUserBoundKeyAndDecryptData( + wstring helloKeyName, + wstring pinMessage, + uintptr_t windowId, + vector securedEncryptionKeyBytes, + vector combinedInputData + ); + }; + + // Untrusted functions: Called from enclave (VTL1) to host (VTL0) + untrusted + { + // Debug print function - prints to console from enclave + void debug_print(string message); + }; +}; diff --git a/rust/sdk/generate_and_build_crates.ps1 b/rust/sdk/generate_and_build_crates.ps1 index 358e5828..4f78e7a4 100644 --- a/rust/sdk/generate_and_build_crates.ps1 +++ b/rust/sdk/generate_and_build_crates.ps1 @@ -1,9 +1,25 @@ -# Generates the edlcodegen bindings for the host and enclave sdk crates from -# the veil_abi EDL file and builds the sdk workspace. +# Complete build pipeline for the VBS Enclave SDK workspace. +# +# This script performs the full build process: +# 1. Generates EDL bindings for all SDK and sample crates +# 2. Compiles the SDK workspace with Cargo +# 3. Optionally compiles sample workspaces (use -IncludeSamples) +# 4. Optionally signs enclave DLLs for deployment +# +# Usage: +# .\generate_and_build_crates.ps1 # Debug build (SDK only) +# .\generate_and_build_crates.ps1 -IncludeSamples # Debug build + samples +# .\generate_and_build_crates.ps1 -Configuration release # Release build (SDK only) +# .\generate_and_build_crates.ps1 -CertName "MyCert" # Debug build + sign +# .\generate_and_build_crates.ps1 -IncludeSamples -CertName "MyCert" # Full build + sign param( [ValidateSet("debug", "release")] - [string]$Configuration = "debug" + [string]$Configuration = "debug", + + [switch]$IncludeSamples, + + [string]$CertName ) $errorActionPreference = "Stop" @@ -11,10 +27,79 @@ $errorActionPreference = "Stop" # Locate shared scripts folder . "$PSScriptRoot\..\scripts\get_common_paths.ps1" -# Generate edlcodegen crates for the workspace first before attempting -# to build the sdk workspace. +Write-Host "`n=== VBS Enclave SDK Build ===" -ForegroundColor Cyan +Write-Host "Configuration: $Configuration" +if ($IncludeSamples) { + Write-Host "Samples: Included" +} +if ($CertName) { + Write-Host "Certificate: $CertName (will sign enclaves)" +} + +# Step 1: Generate EDL bindings for SDK and samples +Write-Host "`nGenerating EDL bindings..." -ForegroundColor Yellow . "$PSScriptRoot\generate_codegen_for_workspace.ps1" -# Build the sdk workspace now that codegeneration is completed. +# Step 2: Build the SDK workspace with Cargo +Write-Host "`nBuilding SDK workspace..." -ForegroundColor Yellow . "$scriptsDir\invoke_cargo_build.ps1" -Path $PSScriptRoot -Configuration $Configuration +# Step 3: Sign SDK enclave DLLs if certificate is provided +if ($CertName) { + Write-Host "`nSigning SDK enclave DLLs..." -ForegroundColor Yellow + + $signScript = Join-Path $scriptsDir "sign-enclave.ps1" + $targetDir = if ($Configuration -eq "release") { "release" } else { "debug" } + $targetPath = Join-Path $PSScriptRoot "target\$targetDir" + + $enclaveDlls = Get-ChildItem -Path $targetPath -Filter "*_enclave.dll" -ErrorAction SilentlyContinue + + if (-not $enclaveDlls) { + Write-Host "No SDK enclave DLLs found to sign." -ForegroundColor Yellow + } else { + foreach ($dll in $enclaveDlls) { + Write-Host "Signing $($dll.Name)..." -ForegroundColor Gray + & $signScript -DllPath $dll.FullName -CertName $CertName + if ($LASTEXITCODE -ne 0) { throw "Failed to sign $($dll.Name)" } + } + Write-Host "SDK enclaves signed." -ForegroundColor Green + } +} + +# Step 4: Build samples (only if -IncludeSamples is specified) +# Each sample has its own build.ps1 script that handles building and signing +$sampleScripts = @( + (Join-Path $PSScriptRoot "crates\samples\userboundkey\build.ps1") +) + +if ($IncludeSamples) { + Write-Host "`nBuilding samples..." -ForegroundColor Yellow + + foreach ($sampleScript in $sampleScripts) { + if (Test-Path $sampleScript) { + $sampleName = Split-Path (Split-Path $sampleScript -Parent) -Leaf + Write-Host "Building sample: $sampleName" -ForegroundColor Gray + + if ($CertName) { + & $sampleScript -Configuration $Configuration -CertName $CertName + } else { + & $sampleScript -Configuration $Configuration + } + if ($LASTEXITCODE -ne 0) { throw "Failed to build sample: $sampleName" } + } + } +} + +Write-Host "`n=== SDK Build Complete ===" -ForegroundColor Cyan +$targetDir = if ($Configuration -eq "release") { "release" } else { "debug" } +Write-Host "SDK output: $(Join-Path $PSScriptRoot "target\$targetDir")" +if ($IncludeSamples) { + Write-Host "Sample output: $(Join-Path $PSScriptRoot "crates\samples\userboundkey\target\$targetDir")" +} +if (-not $CertName) { + Write-Host "" + Write-Host "Tip: Use -CertName to sign enclave DLLs after build." -ForegroundColor Gray +} +if (-not $IncludeSamples) { + Write-Host "Tip: Use -IncludeSamples to also build sample applications." -ForegroundColor Gray +} \ No newline at end of file diff --git a/rust/sdk/generate_codegen_for_workspace.ps1 b/rust/sdk/generate_codegen_for_workspace.ps1 index ae47bf29..c73aa7c0 100644 --- a/rust/sdk/generate_codegen_for_workspace.ps1 +++ b/rust/sdk/generate_codegen_for_workspace.ps1 @@ -1,4 +1,15 @@ -# Generates the edlcodegen bindings for all host and enclave sdk crates. +# Generates EDL code bindings for the entire SDK workspace. +# +# This script generates Rust bindings from EDL files for: +# - SDK core libraries (sdk namespace) +# - Sample applications (userboundkey_sample) +# +# Use this script when: +# - Working on EDL interface definitions +# - Need updated IntelliSense after EDL changes +# - Want fast iteration without full compilation +# +# The generated bindings are automatically formatted with rustfmt. $errorActionPreference = "Stop" @@ -6,22 +17,33 @@ $errorActionPreference = "Stop" . "$PSScriptRoot\..\scripts\get_common_paths.ps1" # SDK crate paths -$edlPath = Join-Path $repoRoot "src\VbsEnclaveSDK\veil_abi.edl" $enclaveSdkCrate = Join-Path $SdkWorkspacePath "crates/libs/sdk-enclave" $hostSdkCrate = Join-Path $SdkWorkspacePath "crates/libs/sdk-host" -# Generate edlcodegen crates for the sdk-host and sdk-enclave crates +# Generate SDK EDL bindings (user-bound key APIs and other SDK features) +$sdkEdl = Join-Path $SdkWorkspacePath "crates\libs\sdk.edl" . "$scriptsDir\generate_codegen_crates.ps1" ` -HostAppOutDir "$hostSdkCrate\generated" ` -EnclaveOutDir "$enclaveSdkCrate\generated" ` - -EdlPath $edlPath ` - -Namespace "veil_abi" ` - -Vtl0ClassName "export_interface" + -EdlPath $sdkEdl ` + -Namespace "sdk" ` + -Vtl0ClassName "SdkHost" -# Below this comment, Call the generate_codegen_crates.ps1 script -# on any other crates in the sdk workspace that need codegen bindings. -# E.g for any sample crates. +# Generate sample EDL bindings (userboundkey sample application interface) +$sampleDir = Join-Path $SdkWorkspacePath "crates\samples\userboundkey" +$sampleEdl = Join-Path $sampleDir "userboundkey_sample.edl" +$libsImportDir = Join-Path $SdkWorkspacePath "crates\libs" -# Format the workspace after code generation to make sure all generated code -# is properly formatted. -. "$scriptsDir\invoke_rustfmt_workspace.ps1" -WorkspacePath $PSScriptRoot \ No newline at end of file +. "$scriptsDir\generate_codegen_crates.ps1" ` + -HostAppOutDir "$sampleDir\host\generated" ` + -EnclaveOutDir "$sampleDir\enclave\generated" ` + -EdlPath $sampleEdl ` + -Namespace "userboundkey_sample" ` + -ImportDir $libsImportDir ` + -Vtl0ClassName "UntrustedImpl" + +# Format SDK workspace +. "$scriptsDir\invoke_rustfmt_workspace.ps1" -WorkspacePath $PSScriptRoot + +# Format sample workspace (now in its own workspace) +. "$scriptsDir\invoke_rustfmt_workspace.ps1" -WorkspacePath $sampleDir \ No newline at end of file