From 703e74e2dd49406350ec821180789f5f1e7edf54 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Mon, 6 Apr 2026 08:33:35 -0700 Subject: [PATCH 1/8] A+ quality: zero-copy optimizations and Cargo.toml standardization Zero-copy improvements: - AKV: Store cose_key_cbor as ArcSlice in CoseKeyHeaderContributor (clone = refcount bump) - AKV: Pre-compute kid bytes as ArcSlice in KeyIdHeaderContributor - AKV: Change COSE_Key cache from Vec to ArcSlice (zero-copy cache hits) - MST: Return Vec from read_receipts instead of Vec> Cargo.toml standardization: - Convert 27 crates from dot notation to brace notation (edition/license) - Fix invalid edition '2024' in cose_openssl - Fix hardcoded editions/licenses in 3 crates - Add missing descriptions to 5 crates - Add missing [lints.rust] sections to 2 crates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- native/rust/cose_openssl/Cargo.toml | 5 +- native/rust/cose_openssl/src/cose.rs | 6 +- native/rust/cose_openssl/src/ossl_wrappers.rs | 2 +- native/rust/did/x509/Cargo.toml | 4 +- native/rust/did/x509/ffi/Cargo.toml | 4 +- .../src/signing/akv_signing_key.rs | 9 +- .../signing/cose_key_header_contributor.rs | 23 +- .../src/signing/key_id_header_contributor.rs | 14 +- .../extension_packs/certificates/Cargo.toml | 4 +- .../certificates/ffi/Cargo.toml | 4 +- .../certificates/local/Cargo.toml | 4 +- .../certificates/local/ffi/Cargo.toml | 4 +- .../mst/src/validation/pack.rs | 745 +++++++++--------- native/rust/primitives/cbor/Cargo.toml | 5 +- .../rust/primitives/cbor/everparse/Cargo.toml | 5 +- native/rust/primitives/cose/Cargo.toml | 4 +- native/rust/primitives/cose/sign1/Cargo.toml | 4 +- .../rust/primitives/cose/sign1/ffi/Cargo.toml | 4 +- native/rust/primitives/crypto/Cargo.toml | 4 +- .../rust/primitives/crypto/openssl/Cargo.toml | 4 +- .../primitives/crypto/openssl/ffi/Cargo.toml | 4 +- native/rust/signing/core/Cargo.toml | 4 +- native/rust/signing/core/ffi/Cargo.toml | 4 +- native/rust/signing/factories/Cargo.toml | 4 +- native/rust/signing/factories/ffi/Cargo.toml | 4 +- native/rust/signing/headers/Cargo.toml | 5 +- native/rust/signing/headers/ffi/Cargo.toml | 4 +- native/rust/validation/core/Cargo.toml | 5 +- native/rust/validation/core/ffi/Cargo.toml | 3 +- native/rust/validation/demo/Cargo.toml | 9 +- native/rust/validation/primitives/Cargo.toml | 4 +- .../rust/validation/primitives/ffi/Cargo.toml | 3 +- native/rust/validation/test_utils/Cargo.toml | 4 +- 33 files changed, 461 insertions(+), 454 deletions(-) diff --git a/native/rust/cose_openssl/Cargo.toml b/native/rust/cose_openssl/Cargo.toml index 7ab661ea..d60eceff 100644 --- a/native/rust/cose_openssl/Cargo.toml +++ b/native/rust/cose_openssl/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "cose-openssl" version = "0.1.0" -edition = "2024" +edition = { workspace = true } +license = { workspace = true } +description = "Low-level OpenSSL bindings for COSE signing and verification" [lib] crate-type = ["lib"] @@ -11,6 +13,7 @@ pqc = [] [lints.rust] warnings = "deny" +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] openssl-sys = "0.9" diff --git a/native/rust/cose_openssl/src/cose.rs b/native/rust/cose_openssl/src/cose.rs index ee5e6ce3..0609164c 100644 --- a/native/rust/cose_openssl/src/cose.rs +++ b/native/rust/cose_openssl/src/cose.rs @@ -1,7 +1,7 @@ -use crate::cbor::{CborSlice, CborValue, serialize_array}; +use crate::cbor::{serialize_array, CborSlice, CborValue}; use crate::ossl_wrappers::{ - EvpKey, KeyType, WhichEC, WhichRSA, ecdsa_der_to_fixed, ecdsa_fixed_to_der, - rsa_pss_md_for_cose_alg, + ecdsa_der_to_fixed, ecdsa_fixed_to_der, rsa_pss_md_for_cose_alg, EvpKey, KeyType, WhichEC, + WhichRSA, }; #[cfg(feature = "pqc")] diff --git a/native/rust/cose_openssl/src/ossl_wrappers.rs b/native/rust/cose_openssl/src/ossl_wrappers.rs index 9a598cc9..1bb6ac3a 100644 --- a/native/rust/cose_openssl/src/ossl_wrappers.rs +++ b/native/rust/cose_openssl/src/ossl_wrappers.rs @@ -6,7 +6,7 @@ use std::ptr; // Not exposed by openssl-sys 0.9, but available at link time (OpenSSL 3.0+). unsafe extern "C" { fn EVP_PKEY_is_a(pkey: *const ossl::EVP_PKEY, name: *const std::ffi::c_char) - -> std::ffi::c_int; + -> std::ffi::c_int; fn EVP_PKEY_get_group_name( pkey: *const ossl::EVP_PKEY, diff --git a/native/rust/did/x509/Cargo.toml b/native/rust/did/x509/Cargo.toml index cec4bf22..eb092b1b 100644 --- a/native/rust/did/x509/Cargo.toml +++ b/native/rust/did/x509/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "did_x509" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } version = "0.1.0" description = "DID:x509 identifier parsing, building, validation and resolution" diff --git a/native/rust/did/x509/ffi/Cargo.toml b/native/rust/did/x509/ffi/Cargo.toml index 56d7b5b5..f4001ce0 100644 --- a/native/rust/did/x509/ffi/Cargo.toml +++ b/native/rust/did/x509/ffi/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "did_x509_ffi" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } version = "0.1.0" description = "C/C++ FFI for DID:x509 parsing, building, validation and resolution" diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs index 1ef5f17e..e5523828 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs @@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex}; +use cose_sign1_primitives::ArcSlice; use cose_sign1_signing::{CryptographicKeyType, SigningKeyMetadata, SigningServiceKey}; use crypto_primitives::{CryptoError, CryptoSigner}; @@ -61,8 +62,8 @@ pub struct AzureKeyVaultSigningKey { pub(crate) crypto_client: Arc>, pub(crate) algorithm: i64, pub(crate) metadata: SigningKeyMetadata, - /// Cached COSE_Key bytes (lazily computed). - pub(crate) cached_cose_key: Arc>>>, + /// Cached COSE_Key bytes (lazily computed). Stored as ArcSlice for zero-copy sharing. + pub(crate) cached_cose_key: Arc>>, } impl AzureKeyVaultSigningKey { @@ -105,7 +106,7 @@ impl AzureKeyVaultSigningKey { /// Builds a COSE_Key representation of the public key. /// /// Uses double-checked locking for caching (matches V2 pattern). - pub fn get_cose_key_bytes(&self) -> Result, AkvError> { + pub fn get_cose_key_bytes(&self) -> Result { // First check without locking (fast path) { let guard = self @@ -128,7 +129,7 @@ impl AzureKeyVaultSigningKey { } // Build COSE_Key map - let cose_key_bytes = self.build_cose_key_cbor()?; + let cose_key_bytes: ArcSlice = self.build_cose_key_cbor()?.into(); *guard = Some(cose_key_bytes.clone()); Ok(cose_key_bytes) } diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/cose_key_header_contributor.rs b/native/rust/extension_packs/azure_key_vault/src/signing/cose_key_header_contributor.rs index d067edee..2ec704f2 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/cose_key_header_contributor.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/cose_key_header_contributor.rs @@ -6,7 +6,7 @@ //! Embeds the public key as a COSE_Key structure in COSE headers, //! defaulting to UNPROTECTED headers with label -65537. -use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue}; +use cose_sign1_primitives::{ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue}; use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, HeaderMergeStrategy}; /// Private-use label for embedded COSE_Key public key. @@ -26,8 +26,9 @@ pub enum CoseKeyHeaderLocation { /// Header contributor that embeds a COSE_Key public key structure. /// /// Maps V2's `PublicKeyHeaderContributor`. +/// Stores the key as `ArcSlice` so cloning is a cheap refcount bump. pub struct CoseKeyHeaderContributor { - cose_key_cbor: Vec, + cose_key_cbor: ArcSlice, location: CoseKeyHeaderLocation, } @@ -38,20 +39,20 @@ impl CoseKeyHeaderContributor { /// /// * `cose_key_cbor` - The CBOR-encoded COSE_Key map /// * `location` - Where to place the header (defaults to Unprotected) - pub fn new(cose_key_cbor: Vec, location: CoseKeyHeaderLocation) -> Self { + pub fn new(cose_key_cbor: impl Into, location: CoseKeyHeaderLocation) -> Self { Self { - cose_key_cbor, + cose_key_cbor: cose_key_cbor.into(), location, } } /// Creates a contributor that places the key in unprotected headers. - pub fn unprotected(cose_key_cbor: Vec) -> Self { + pub fn unprotected(cose_key_cbor: impl Into) -> Self { Self::new(cose_key_cbor, CoseKeyHeaderLocation::Unprotected) } /// Creates a contributor that places the key in protected headers. - pub fn protected(cose_key_cbor: Vec) -> Self { + pub fn protected(cose_key_cbor: impl Into) -> Self { Self::new(cose_key_cbor, CoseKeyHeaderLocation::Protected) } } @@ -69,10 +70,7 @@ impl HeaderContributor for CoseKeyHeaderContributor { if self.location == CoseKeyHeaderLocation::Protected { let label = CoseHeaderLabel::Int(COSE_KEY_LABEL); if headers.get(&label).is_none() { - headers.insert( - label, - CoseHeaderValue::Bytes(self.cose_key_cbor.clone().into()), - ); + headers.insert(label, CoseHeaderValue::Bytes(self.cose_key_cbor.clone())); } } } @@ -85,10 +83,7 @@ impl HeaderContributor for CoseKeyHeaderContributor { if self.location == CoseKeyHeaderLocation::Unprotected { let label = CoseHeaderLabel::Int(COSE_KEY_LABEL); if headers.get(&label).is_none() { - headers.insert( - label, - CoseHeaderValue::Bytes(self.cose_key_cbor.clone().into()), - ); + headers.insert(label, CoseHeaderValue::Bytes(self.cose_key_cbor.clone())); } } } diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/key_id_header_contributor.rs b/native/rust/extension_packs/azure_key_vault/src/signing/key_id_header_contributor.rs index e88033f3..d3acb293 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/key_id_header_contributor.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/key_id_header_contributor.rs @@ -5,14 +5,15 @@ //! //! Adds the `kid` (label 4) header to PROTECTED headers with the full AKV key URI. -use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue}; +use cose_sign1_primitives::{ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue}; use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, HeaderMergeStrategy}; /// Header contributor that adds the AKV key identifier to protected headers. /// /// Maps V2's kid header contribution in `AzureKeyVaultSigningService`. +/// Stores the kid bytes as `ArcSlice` so contributing is a cheap refcount bump. pub struct KeyIdHeaderContributor { - key_id: String, + kid_bytes: ArcSlice, } impl KeyIdHeaderContributor { @@ -22,7 +23,9 @@ impl KeyIdHeaderContributor { /// /// * `key_id` - The full AKV key URI (e.g., `https://{vault}.vault.azure.net/keys/{name}/{version}`) pub fn new(key_id: String) -> Self { - Self { key_id } + Self { + kid_bytes: ArcSlice::from(key_id.into_bytes()), + } } } @@ -38,10 +41,7 @@ impl HeaderContributor for KeyIdHeaderContributor { ) { let kid_label = CoseHeaderLabel::Int(4); if headers.get(&kid_label).is_none() { - headers.insert( - kid_label, - CoseHeaderValue::Bytes(self.key_id.as_bytes().to_vec().into()), - ); + headers.insert(kid_label, CoseHeaderValue::Bytes(self.kid_bytes.clone())); } } diff --git a/native/rust/extension_packs/certificates/Cargo.toml b/native/rust/extension_packs/certificates/Cargo.toml index 829738b7..1d757771 100644 --- a/native/rust/extension_packs/certificates/Cargo.toml +++ b/native/rust/extension_packs/certificates/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_certificates" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } description = "X.509 certificate trust pack for COSE Sign1 validation and signing" [lib] diff --git a/native/rust/extension_packs/certificates/ffi/Cargo.toml b/native/rust/extension_packs/certificates/ffi/Cargo.toml index 88ddbc7c..e2b4785d 100644 --- a/native/rust/extension_packs/certificates/ffi/Cargo.toml +++ b/native/rust/extension_packs/certificates/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_certificates_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } description = "C/C++ FFI projections for cose_sign1_certificates trust pack" [lib] diff --git a/native/rust/extension_packs/certificates/local/Cargo.toml b/native/rust/extension_packs/certificates/local/Cargo.toml index 40b43fba..889d5ea6 100644 --- a/native/rust/extension_packs/certificates/local/Cargo.toml +++ b/native/rust/extension_packs/certificates/local/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cose_sign1_certificates_local" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } version = "0.1.0" description = "Local certificate creation, ephemeral certs, chain building, and key loading" diff --git a/native/rust/extension_packs/certificates/local/ffi/Cargo.toml b/native/rust/extension_packs/certificates/local/ffi/Cargo.toml index dbe0d621..1ef50edc 100644 --- a/native/rust/extension_packs/certificates/local/ffi/Cargo.toml +++ b/native/rust/extension_packs/certificates/local/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_certificates_local_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } description = "C/C++ FFI projections for ephemeral certificate generation" [lib] diff --git a/native/rust/extension_packs/mst/src/validation/pack.rs b/native/rust/extension_packs/mst/src/validation/pack.rs index b0f5f58c..3b082a19 100644 --- a/native/rust/extension_packs/mst/src/validation/pack.rs +++ b/native/rust/extension_packs/mst/src/validation/pack.rs @@ -1,373 +1,372 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::validation::facts::{ - MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, - MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, - MstReceiptStatementSha256Fact, MstReceiptTrustedFact, -}; -use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; -use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderValue}; -use cose_sign1_validation::fluent::*; -use cose_sign1_validation_primitives::error::TrustError; -use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFactProducer}; -use cose_sign1_validation_primitives::ids::sha256_of_bytes; -use cose_sign1_validation_primitives::plan::CompiledTrustPlan; -use cose_sign1_validation_primitives::subject::TrustSubject; -use once_cell::sync::Lazy; -use std::collections::HashSet; - -use crate::validation::receipt_verify::{ - verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, -}; - -pub mod fluent_ext { - pub use crate::validation::fluent_ext::*; -} - -/// Encode bytes as lowercase hex string. -fn hex_encode(bytes: &[u8]) -> String { - bytes - .iter() - .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { - use std::fmt::Write; - // write! to a String is infallible; this expect is defensive. - write!(s, "{:02x}", b).expect("hex formatting to String cannot fail"); - s - }) -} - -/// COSE header label used by MST receipts (matches .NET): 394. -pub const MST_RECEIPT_HEADER_LABEL: i64 = 394; - -#[derive(Clone, Debug, Default)] -pub struct MstTrustPack { - /// If true, allow the verifier to fetch JWKS online when offline keys are missing or do not - /// contain the required `kid`. - /// - /// This is an operational switch. Trust decisions (e.g., issuer allowlisting) belong in policy. - pub allow_network: bool, - - /// Offline JWKS JSON used to resolve receipt signing keys by `kid`. - /// - /// This enables deterministic verification for test vectors without requiring network access. - pub offline_jwks_json: Option, - - /// Optional api-version to use for the CodeTransparency `/jwks` endpoint. - /// If not set, the verifier will try without an api-version parameter. - pub jwks_api_version: Option, -} - -impl MstTrustPack { - /// Create an MST pack with the given options. - pub fn new( - allow_network: bool, - offline_jwks_json: Option, - jwks_api_version: Option, - ) -> Self { - Self { - allow_network, - offline_jwks_json, - jwks_api_version, - } - } - - /// Create an MST pack configured for offline-only verification. - /// - /// This disables network fetching and uses the provided JWKS JSON to resolve receipt signing - /// keys. - pub fn offline_with_jwks(jwks_json: impl Into) -> Self { - Self { - allow_network: false, - offline_jwks_json: Some(jwks_json.into()), - jwks_api_version: None, - } - } - - /// Create an MST pack configured to allow online JWKS fetching. - /// - /// This is an operational switch only; issuer allowlisting should still be expressed via trust - /// policy. - pub fn online() -> Self { - Self { - allow_network: true, - offline_jwks_json: None, - jwks_api_version: None, - } - } -} - -impl TrustFactProducer for MstTrustPack { - /// Stable producer name used for diagnostics/audit. - fn name(&self) -> &'static str { - "cose_sign1_transparent_mst::MstTrustPack" - } - - /// Produce MST-related facts for the current subject. - /// - /// - On `Message` subjects: projects each receipt into a derived `CounterSignature` subject. - /// - On `CounterSignature` subjects: verifies the receipt and emits MST facts. - fn produce(&self, ctx: &mut TrustFactContext<'_>) -> Result<(), TrustError> { - // MST receipts are modeled as counter-signatures: - // - On the Message subject, we *project* each receipt into a derived CounterSignature subject. - // - On the CounterSignature subject, we produce MST-specific facts (present/trusted). - - match ctx.subject().kind { - "Message" => { - // If the COSE message is unavailable, counter-signature discovery is Missing. - if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - } - - let receipts = read_receipts(ctx)?; - - let message_subject = match ctx.cose_sign1_bytes() { - Some(bytes) => TrustSubject::message(bytes), - None => TrustSubject::message(b"seed"), - }; - - let mut seen: HashSet = - HashSet::new(); - - for r in receipts { - let cs_subject = - TrustSubject::counter_signature(&message_subject, r.as_slice()); - let cs_key_subject = TrustSubject::counter_signature_signing_key(&cs_subject); - - ctx.observe(CounterSignatureSubjectFact { - subject: cs_subject, - is_protected_header: false, - })?; - ctx.observe(CounterSignatureSigningKeySubjectFact { - subject: cs_key_subject, - is_protected_header: false, - })?; - - let id = sha256_of_bytes(r.as_slice()); - if seen.insert(id) { - ctx.observe(UnknownCounterSignatureBytesFact { - counter_signature_id: id, - raw_counter_signature_bytes: std::sync::Arc::from(r.into_boxed_slice()), - })?; - } - } - - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - "CounterSignature" => { - // If the COSE message is unavailable, we can't map this subject to a receipt. - if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - } - - let receipts = read_receipts(ctx)?; - - let Some(message_bytes) = ctx.cose_sign1_bytes() else { - // Fallback: without bytes we can't compute the same subject IDs. - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - let message_subject = TrustSubject::message(message_bytes); - - let mut matched_receipt: Option> = None; - for r in receipts { - let cs = TrustSubject::counter_signature(&message_subject, r.as_slice()); - if cs.id == ctx.subject().id { - matched_receipt = Some(r); - break; - } - } - - let Some(receipt_bytes) = matched_receipt else { - // Not an MST receipt counter-signature; leave as Available(empty). - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - // Receipt identified. - ctx.observe(MstReceiptPresentFact { present: true })?; - - // Get provider from message (required for receipt verification) - let Some(_msg) = ctx.cose_sign1_message() else { - ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some("no message in context for verification".to_string()), - })?; - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - let jwks_json = self.offline_jwks_json.as_deref(); - let factory = OpenSslJwkVerifierFactory; - let out = verify_mst_receipt(ReceiptVerifyInput { - statement_bytes_with_receipts: message_bytes, - receipt_bytes: receipt_bytes.as_slice(), - offline_jwks_json: jwks_json, - allow_network_fetch: self.allow_network, - jwks_api_version: self.jwks_api_version.as_deref(), - client: None, // Creates temporary client per-issuer - jwk_verifier_factory: &factory, - }); - - match out { - Ok(v) => { - ctx.observe(MstReceiptTrustedFact { - trusted: v.trusted, - details: v.details.clone(), - })?; - - ctx.observe(MstReceiptIssuerFact { - issuer: v.issuer.clone(), - })?; - ctx.observe(MstReceiptKidFact { kid: v.kid.clone() })?; - ctx.observe(MstReceiptStatementSha256Fact { - sha256_hex: hex_encode(&v.statement_sha256), - })?; - ctx.observe(MstReceiptStatementCoverageFact { - coverage: "sha256(COSE_Sign1 bytes with unprotected headers cleared)" - .to_string(), - })?; - ctx.observe(MstReceiptSignatureVerifiedFact { verified: true })?; - - ctx.observe(CounterSignatureEnvelopeIntegrityFact { - sig_structure_intact: v.trusted, - details: Some( - "covers: sha256(COSE_Sign1 bytes with unprotected headers cleared)" - .to_string(), - ), - })?; - } - Err(e @ ReceiptVerifyError::UnsupportedVds(_)) => { - // Non-Microsoft receipts can coexist with MST receipts. - // Make the fact Available(false) so AnyOf semantics can still succeed. - ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some(e.to_string()), - })?; - } - Err(e) => ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some(e.to_string()), - })?, - } - - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - _ => { - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - } - } - - /// Return the set of fact keys this pack can produce. - fn provides(&self) -> &'static [FactKey] { - static PROVIDED: Lazy<[FactKey; 11]> = Lazy::new(|| { - [ - // Counter-signature projection (message-scoped) - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - // MST-specific facts (counter-signature scoped) - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - ] - }); - &*PROVIDED - } -} - -impl CoseSign1TrustPack for MstTrustPack { - /// Short display name for this trust pack. - fn name(&self) -> &'static str { - "MstTrustPack" - } - - /// Return a `TrustFactProducer` instance for this pack. - fn fact_producer(&self) -> std::sync::Arc { - std::sync::Arc::new(self.clone()) - } - - /// Return the default trust plan for MST-only validation. - /// - /// This plan requires that a counter-signature receipt is trusted. - fn default_trust_plan(&self) -> Option { - use crate::validation::fluent_ext::MstReceiptTrustedWhereExt; - - // Secure-by-default MST policy: - // - require a receipt to be trusted (verification must be enabled) - let bundled = TrustPlanBuilder::new(vec![std::sync::Arc::new(self.clone())]) - .for_counter_signature(|cs| { - cs.require::(|f| f.require_receipt_trusted()) - }) - .compile() - .expect("default trust plan should be satisfiable by the MST trust pack"); - - Some(bundled.plan().clone()) - } -} - -/// Read all MST receipt blobs from the current message. -/// -/// Prefers the parsed message view when available; returns empty when no message or receipts. -fn read_receipts(ctx: &TrustFactContext<'_>) -> Result>, TrustError> { - if let Some(msg) = ctx.cose_sign1_message() { - let label = CoseHeaderLabel::Int(MST_RECEIPT_HEADER_LABEL); - match msg.unprotected.get(&label) { - None => return Ok(Vec::new()), - Some(CoseHeaderValue::Array(arr)) => { - let mut result = Vec::new(); - for v in arr { - if let CoseHeaderValue::Bytes(b) = v { - result.push(b.to_vec()); - } else { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - } - return Ok(result); - } - Some(CoseHeaderValue::Bytes(_)) => { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - Some(_) => { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - } - } - - // Without a parsed message, we cannot read receipts - Ok(Vec::new()) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::validation::facts::{ + MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, + MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, + MstReceiptStatementSha256Fact, MstReceiptTrustedFact, +}; +use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; +use cose_sign1_primitives::{ArcSlice, CoseHeaderLabel, CoseHeaderValue}; +use cose_sign1_validation::fluent::*; +use cose_sign1_validation_primitives::error::TrustError; +use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFactProducer}; +use cose_sign1_validation_primitives::ids::sha256_of_bytes; +use cose_sign1_validation_primitives::plan::CompiledTrustPlan; +use cose_sign1_validation_primitives::subject::TrustSubject; +use once_cell::sync::Lazy; +use std::collections::HashSet; + +use crate::validation::receipt_verify::{ + verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, +}; + +pub mod fluent_ext { + pub use crate::validation::fluent_ext::*; +} + +/// Encode bytes as lowercase hex string. +fn hex_encode(bytes: &[u8]) -> String { + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { + use std::fmt::Write; + // write! to a String is infallible; this expect is defensive. + write!(s, "{:02x}", b).expect("hex formatting to String cannot fail"); + s + }) +} + +/// COSE header label used by MST receipts (matches .NET): 394. +pub const MST_RECEIPT_HEADER_LABEL: i64 = 394; + +#[derive(Clone, Debug, Default)] +pub struct MstTrustPack { + /// If true, allow the verifier to fetch JWKS online when offline keys are missing or do not + /// contain the required `kid`. + /// + /// This is an operational switch. Trust decisions (e.g., issuer allowlisting) belong in policy. + pub allow_network: bool, + + /// Offline JWKS JSON used to resolve receipt signing keys by `kid`. + /// + /// This enables deterministic verification for test vectors without requiring network access. + pub offline_jwks_json: Option, + + /// Optional api-version to use for the CodeTransparency `/jwks` endpoint. + /// If not set, the verifier will try without an api-version parameter. + pub jwks_api_version: Option, +} + +impl MstTrustPack { + /// Create an MST pack with the given options. + pub fn new( + allow_network: bool, + offline_jwks_json: Option, + jwks_api_version: Option, + ) -> Self { + Self { + allow_network, + offline_jwks_json, + jwks_api_version, + } + } + + /// Create an MST pack configured for offline-only verification. + /// + /// This disables network fetching and uses the provided JWKS JSON to resolve receipt signing + /// keys. + pub fn offline_with_jwks(jwks_json: impl Into) -> Self { + Self { + allow_network: false, + offline_jwks_json: Some(jwks_json.into()), + jwks_api_version: None, + } + } + + /// Create an MST pack configured to allow online JWKS fetching. + /// + /// This is an operational switch only; issuer allowlisting should still be expressed via trust + /// policy. + pub fn online() -> Self { + Self { + allow_network: true, + offline_jwks_json: None, + jwks_api_version: None, + } + } +} + +impl TrustFactProducer for MstTrustPack { + /// Stable producer name used for diagnostics/audit. + fn name(&self) -> &'static str { + "cose_sign1_transparent_mst::MstTrustPack" + } + + /// Produce MST-related facts for the current subject. + /// + /// - On `Message` subjects: projects each receipt into a derived `CounterSignature` subject. + /// - On `CounterSignature` subjects: verifies the receipt and emits MST facts. + fn produce(&self, ctx: &mut TrustFactContext<'_>) -> Result<(), TrustError> { + // MST receipts are modeled as counter-signatures: + // - On the Message subject, we *project* each receipt into a derived CounterSignature subject. + // - On the CounterSignature subject, we produce MST-specific facts (present/trusted). + + match ctx.subject().kind { + "Message" => { + // If the COSE message is unavailable, counter-signature discovery is Missing. + if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + + let receipts = read_receipts(ctx)?; + + let message_subject = match ctx.cose_sign1_bytes() { + Some(bytes) => TrustSubject::message(bytes), + None => TrustSubject::message(b"seed"), + }; + + let mut seen: HashSet = + HashSet::new(); + + for r in receipts { + let cs_subject = TrustSubject::counter_signature(&message_subject, &r); + let cs_key_subject = TrustSubject::counter_signature_signing_key(&cs_subject); + + ctx.observe(CounterSignatureSubjectFact { + subject: cs_subject, + is_protected_header: false, + })?; + ctx.observe(CounterSignatureSigningKeySubjectFact { + subject: cs_key_subject, + is_protected_header: false, + })?; + + let id = sha256_of_bytes(&r); + if seen.insert(id) { + ctx.observe(UnknownCounterSignatureBytesFact { + counter_signature_id: id, + raw_counter_signature_bytes: std::sync::Arc::from(r.as_bytes()), + })?; + } + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + "CounterSignature" => { + // If the COSE message is unavailable, we can't map this subject to a receipt. + if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + + let receipts = read_receipts(ctx)?; + + let Some(message_bytes) = ctx.cose_sign1_bytes() else { + // Fallback: without bytes we can't compute the same subject IDs. + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + let message_subject = TrustSubject::message(message_bytes); + + let mut matched_receipt: Option = None; + for r in receipts { + let cs = TrustSubject::counter_signature(&message_subject, &r); + if cs.id == ctx.subject().id { + matched_receipt = Some(r); + break; + } + } + + let Some(receipt_bytes) = matched_receipt else { + // Not an MST receipt counter-signature; leave as Available(empty). + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + // Receipt identified. + ctx.observe(MstReceiptPresentFact { present: true })?; + + // Get provider from message (required for receipt verification) + let Some(_msg) = ctx.cose_sign1_message() else { + ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some("no message in context for verification".to_string()), + })?; + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + let jwks_json = self.offline_jwks_json.as_deref(); + let factory = OpenSslJwkVerifierFactory; + let out = verify_mst_receipt(ReceiptVerifyInput { + statement_bytes_with_receipts: message_bytes, + receipt_bytes: &receipt_bytes, + offline_jwks_json: jwks_json, + allow_network_fetch: self.allow_network, + jwks_api_version: self.jwks_api_version.as_deref(), + client: None, // Creates temporary client per-issuer + jwk_verifier_factory: &factory, + }); + + match out { + Ok(v) => { + ctx.observe(MstReceiptTrustedFact { + trusted: v.trusted, + details: v.details.clone(), + })?; + + ctx.observe(MstReceiptIssuerFact { + issuer: v.issuer.clone(), + })?; + ctx.observe(MstReceiptKidFact { kid: v.kid.clone() })?; + ctx.observe(MstReceiptStatementSha256Fact { + sha256_hex: hex_encode(&v.statement_sha256), + })?; + ctx.observe(MstReceiptStatementCoverageFact { + coverage: "sha256(COSE_Sign1 bytes with unprotected headers cleared)" + .to_string(), + })?; + ctx.observe(MstReceiptSignatureVerifiedFact { verified: true })?; + + ctx.observe(CounterSignatureEnvelopeIntegrityFact { + sig_structure_intact: v.trusted, + details: Some( + "covers: sha256(COSE_Sign1 bytes with unprotected headers cleared)" + .to_string(), + ), + })?; + } + Err(e @ ReceiptVerifyError::UnsupportedVds(_)) => { + // Non-Microsoft receipts can coexist with MST receipts. + // Make the fact Available(false) so AnyOf semantics can still succeed. + ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some(e.to_string()), + })?; + } + Err(e) => ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some(e.to_string()), + })?, + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + _ => { + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + } + } + + /// Return the set of fact keys this pack can produce. + fn provides(&self) -> &'static [FactKey] { + static PROVIDED: Lazy<[FactKey; 11]> = Lazy::new(|| { + [ + // Counter-signature projection (message-scoped) + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + // MST-specific facts (counter-signature scoped) + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + ] + }); + &*PROVIDED + } +} + +impl CoseSign1TrustPack for MstTrustPack { + /// Short display name for this trust pack. + fn name(&self) -> &'static str { + "MstTrustPack" + } + + /// Return a `TrustFactProducer` instance for this pack. + fn fact_producer(&self) -> std::sync::Arc { + std::sync::Arc::new(self.clone()) + } + + /// Return the default trust plan for MST-only validation. + /// + /// This plan requires that a counter-signature receipt is trusted. + fn default_trust_plan(&self) -> Option { + use crate::validation::fluent_ext::MstReceiptTrustedWhereExt; + + // Secure-by-default MST policy: + // - require a receipt to be trusted (verification must be enabled) + let bundled = TrustPlanBuilder::new(vec![std::sync::Arc::new(self.clone())]) + .for_counter_signature(|cs| { + cs.require::(|f| f.require_receipt_trusted()) + }) + .compile() + .expect("default trust plan should be satisfiable by the MST trust pack"); + + Some(bundled.plan().clone()) + } +} + +/// Read all MST receipt blobs from the current message. +/// +/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. +fn read_receipts(ctx: &TrustFactContext<'_>) -> Result, TrustError> { + if let Some(msg) = ctx.cose_sign1_message() { + let label = CoseHeaderLabel::Int(MST_RECEIPT_HEADER_LABEL); + match msg.unprotected.get(&label) { + None => return Ok(Vec::new()), + Some(CoseHeaderValue::Array(arr)) => { + let mut result = Vec::new(); + for v in arr { + if let CoseHeaderValue::Bytes(b) = v { + result.push(b.clone()); + } else { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + } + return Ok(result); + } + Some(CoseHeaderValue::Bytes(_)) => { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + Some(_) => { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + } + } + + // Without a parsed message, we cannot read receipts + Ok(Vec::new()) +} diff --git a/native/rust/primitives/cbor/Cargo.toml b/native/rust/primitives/cbor/Cargo.toml index 5c443674..67ca878a 100644 --- a/native/rust/primitives/cbor/Cargo.toml +++ b/native/rust/primitives/cbor/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "cbor_primitives" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } +description = "CBOR serialization traits for pluggable CBOR providers" [lib] test = false diff --git a/native/rust/primitives/cbor/everparse/Cargo.toml b/native/rust/primitives/cbor/everparse/Cargo.toml index 0ae3bcb8..3a7b3e38 100644 --- a/native/rust/primitives/cbor/everparse/Cargo.toml +++ b/native/rust/primitives/cbor/everparse/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "cbor_primitives_everparse" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } +description = "EverParse-verified CBOR provider for cbor_primitives traits" [lib] test = false diff --git a/native/rust/primitives/cose/Cargo.toml b/native/rust/primitives/cose/Cargo.toml index db4022b7..78322e1a 100644 --- a/native/rust/primitives/cose/Cargo.toml +++ b/native/rust/primitives/cose/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_primitives" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" # Required for std::sync::OnceLock description = "RFC 9052 COSE types and constants — headers, algorithms, and CBOR provider" diff --git a/native/rust/primitives/cose/sign1/Cargo.toml b/native/rust/primitives/cose/sign1/Cargo.toml index 3ac8b432..956608d6 100644 --- a/native/rust/primitives/cose/sign1/Cargo.toml +++ b/native/rust/primitives/cose/sign1/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_primitives" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" # Required for std::sync::OnceLock description = "Core types and traits for CoseSign1 signing and verification with pluggable CBOR" diff --git a/native/rust/primitives/cose/sign1/ffi/Cargo.toml b/native/rust/primitives/cose/sign1/ffi/Cargo.toml index ca8c707f..426ff961 100644 --- a/native/rust/primitives/cose/sign1/ffi/Cargo.toml +++ b/native/rust/primitives/cose/sign1/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_primitives_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "C/C++ FFI projections for cose_sign1_primitives types and message verification" diff --git a/native/rust/primitives/crypto/Cargo.toml b/native/rust/primitives/crypto/Cargo.toml index 3aa43a6a..1eee96c5 100644 --- a/native/rust/primitives/crypto/Cargo.toml +++ b/native/rust/primitives/crypto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crypto_primitives" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } version = "0.1.0" description = "Cryptographic backend traits for pluggable crypto providers" diff --git a/native/rust/primitives/crypto/openssl/Cargo.toml b/native/rust/primitives/crypto/openssl/Cargo.toml index a99357c8..84b2d010 100644 --- a/native/rust/primitives/crypto/openssl/Cargo.toml +++ b/native/rust/primitives/crypto/openssl/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_crypto_openssl" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "OpenSSL-based cryptographic provider for COSE operations (safe Rust bindings)" diff --git a/native/rust/primitives/crypto/openssl/ffi/Cargo.toml b/native/rust/primitives/crypto/openssl/ffi/Cargo.toml index 26aed02e..bc051ef2 100644 --- a/native/rust/primitives/crypto/openssl/ffi/Cargo.toml +++ b/native/rust/primitives/crypto/openssl/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_crypto_openssl_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "C/C++ FFI projections for OpenSSL crypto provider" diff --git a/native/rust/signing/core/Cargo.toml b/native/rust/signing/core/Cargo.toml index 27705aa9..d0d3c983 100644 --- a/native/rust/signing/core/Cargo.toml +++ b/native/rust/signing/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cose_sign1_signing" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } version = "0.1.0" description = "Core signing abstractions for COSE_Sign1 messages" diff --git a/native/rust/signing/core/ffi/Cargo.toml b/native/rust/signing/core/ffi/Cargo.toml index 490968bc..4eb5eaba 100644 --- a/native/rust/signing/core/ffi/Cargo.toml +++ b/native/rust/signing/core/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_signing_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "C/C++ FFI for COSE_Sign1 message signing operations. Provides builder pattern and callback-based key support for C/C++ consumers." diff --git a/native/rust/signing/factories/Cargo.toml b/native/rust/signing/factories/Cargo.toml index 61b3d405..6268dc7f 100644 --- a/native/rust/signing/factories/Cargo.toml +++ b/native/rust/signing/factories/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_factories" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } description = "Factory patterns for creating COSE_Sign1 messages with signing services" [lib] diff --git a/native/rust/signing/factories/ffi/Cargo.toml b/native/rust/signing/factories/ffi/Cargo.toml index 4d740fa4..a4ac490b 100644 --- a/native/rust/signing/factories/ffi/Cargo.toml +++ b/native/rust/signing/factories/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_factories_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "C/C++ FFI for COSE_Sign1 message factory. Provides direct and indirect signature creation for C/C++ consumers." diff --git a/native/rust/signing/headers/Cargo.toml b/native/rust/signing/headers/Cargo.toml index c0671452..1ddd9a4d 100644 --- a/native/rust/signing/headers/Cargo.toml +++ b/native/rust/signing/headers/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "cose_sign1_headers" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } +description = "CWT claims and header management for COSE Sign1 messages" version = "0.1.0" [lib] diff --git a/native/rust/signing/headers/ffi/Cargo.toml b/native/rust/signing/headers/ffi/Cargo.toml index 3a941e07..9d5efc72 100644 --- a/native/rust/signing/headers/ffi/Cargo.toml +++ b/native/rust/signing/headers/ffi/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_headers_ffi" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } rust-version = "1.70" description = "C/C++ FFI for COSE Sign1 CWT Claims. Provides CWT Claims creation, serialization, and deserialization for C/C++ consumers." diff --git a/native/rust/validation/core/Cargo.toml b/native/rust/validation/core/Cargo.toml index 5b820298..d3ca3fae 100644 --- a/native/rust/validation/core/Cargo.toml +++ b/native/rust/validation/core/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "cose_sign1_validation" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } +description = "Core validation engine for COSE Sign1 messages with trust-based policy" [lib] test = false diff --git a/native/rust/validation/core/ffi/Cargo.toml b/native/rust/validation/core/ffi/Cargo.toml index 83ceb683..95e40809 100644 --- a/native/rust/validation/core/ffi/Cargo.toml +++ b/native/rust/validation/core/ffi/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "cose_sign1_validation_ffi" version = "0.1.0" -edition = "2021" +edition = { workspace = true } +license = { workspace = true } [lib] crate-type = ["cdylib", "staticlib", "rlib"] diff --git a/native/rust/validation/demo/Cargo.toml b/native/rust/validation/demo/Cargo.toml index 3a1fec53..64762fc2 100644 --- a/native/rust/validation/demo/Cargo.toml +++ b/native/rust/validation/demo/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_validation_demo" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } publish = false [[bin]] @@ -24,4 +24,7 @@ ring.workspace = true hex.workspace = true base64.workspace = true rcgen = "0.14" -x509-parser.workspace = true \ No newline at end of file +x509-parser.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } \ No newline at end of file diff --git a/native/rust/validation/primitives/Cargo.toml b/native/rust/validation/primitives/Cargo.toml index 72887b16..7a24b938 100644 --- a/native/rust/validation/primitives/Cargo.toml +++ b/native/rust/validation/primitives/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_validation_primitives" version = "0.1.0" -edition.workspace = true -license.workspace = true +edition = { workspace = true } +license = { workspace = true } [lib] test = false diff --git a/native/rust/validation/primitives/ffi/Cargo.toml b/native/rust/validation/primitives/ffi/Cargo.toml index c2b36b16..b48a5d7c 100644 --- a/native/rust/validation/primitives/ffi/Cargo.toml +++ b/native/rust/validation/primitives/ffi/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "cose_sign1_validation_primitives_ffi" version = "0.1.0" -edition = "2021" +edition = { workspace = true } +license = { workspace = true } [lib] crate-type = ["staticlib", "cdylib", "rlib"] diff --git a/native/rust/validation/test_utils/Cargo.toml b/native/rust/validation/test_utils/Cargo.toml index 4eba6231..18ab9a62 100644 --- a/native/rust/validation/test_utils/Cargo.toml +++ b/native/rust/validation/test_utils/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cose_sign1_validation_test_utils" version = "0.1.0" -edition = "2021" -license = "MIT" +edition = { workspace = true } +license = { workspace = true } [lib] test = false From 52c2b758edefe8024e764ef5e370c1d24c82092d Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Mon, 6 Apr 2026 11:34:15 -0700 Subject: [PATCH 2/8] fix: improve allocation patterns in validation crates Task 1 - ValidationResult.validator_name: Changed from String to Cow<'static, str> to avoid allocating when the name is a compile-time constant. Updated constructors to accept impl Into>. Changed metadata key insertions from .to_string() to .into() for clarity. Documented the metadata BTreeMap key-type trade-off (public API, cold-path allocations). Task 2 - ValidationResult clones: Documented as structurally required. The same ValidationResult value populates both its stage slot and the overall slot in CoseSign1ValidationResult, necessitating a clone. Task 3 - EngineState HashMap values: Changed EngineState.missing and EngineState.errors from HashMap<..., String> to HashMap<..., Arc>. Changed TrustFactSet::Missing { reason } and TrustFactSet::Error { message } from String to Arc. get_fact_set() now uses Arc::clone() (cheap refcount bump) instead of String::clone() (full heap allocation) on every call. Task 4 - TrustDecision.reasons: Changed from Vec to Vec>. Static deny reasons (11+ instances in rules.rs) now use Cow::Borrowed() avoiding heap allocation entirely. Dynamic reasons from format!() use Cow::Owned(). Updated all callers in rules.rs, fluent.rs, and validator.rs. Task 5 - plan.rs clones: Skipped. Vec clones are just Arc refcount bumps, not deep copies. Acceptable as-is. Task 6 - trust_producers.clone(): Skipped. TrustFactEngine::new() takes ownership of the Vec; the clone is a Vec> clone (refcount bumps only). The engine must own its producers. Also fixed a pre-existing type mismatch in azure_artifact_signing where Arc::clone() was assigned to Option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/validation/mod.rs | 2 +- native/rust/validation/core/src/validator.rs | 71 ++++---- .../tests/async_and_streaming_coverage.rs | 2 +- .../core/tests/v2_validator_parity.rs | 2 +- .../core/tests/validator_async_tests.rs | 2 +- .../tests/validator_final_coverage_gaps.rs | 4 +- .../tests/validator_simple_coverage_gaps.rs | 2 +- .../primitives/examples/trust_plan_minimal.rs | 159 +++++++++--------- .../validation/primitives/src/decision.rs | 12 +- .../rust/validation/primitives/src/facts.rs | 22 ++- .../rust/validation/primitives/src/fluent.rs | 10 +- .../rust/validation/primitives/src/rules.rs | 77 +++++---- .../primitives/tests/coverage_boost.rs | 6 +- .../primitives/tests/facts_coverage.rs | 4 +- .../primitives/tests/final_targeted_rules.rs | 1 - .../tests/ids_subject_decision_tests.rs | 3 +- .../tests/rule_property_edge_cases.rs | 1 - .../tests/rules_policy_audit_tests.rs | 15 +- 18 files changed, 212 insertions(+), 183 deletions(-) diff --git a/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs b/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs index 7a8b69ba..6d476fe0 100644 --- a/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs +++ b/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs @@ -39,7 +39,7 @@ impl TrustFactProducer for AasFactProducer { // (these are produced by X509CertificateTrustPack if an x5chain is present). if let Ok(cose_sign1_validation_primitives::facts::TrustFactSet::Available(identities)) = ctx.get_fact_set::(ctx.subject()) { if let Some(identity) = identities.first() { - issuer_cn = Some(identity.issuer.clone()); + issuer_cn = Some(identity.issuer.to_string()); if identity.issuer.contains("Microsoft") { is_ats_issued = true; } diff --git a/native/rust/validation/core/src/validator.rs b/native/rust/validation/core/src/validator.rs index 74a5224e..eea7528d 100644 --- a/native/rust/validation/core/src/validator.rs +++ b/native/rust/validation/core/src/validator.rs @@ -23,6 +23,7 @@ use cose_sign1_validation_primitives::subject::TrustSubject; use cose_sign1_validation_primitives::{ CoseHeaderLocation, CoseSign1Message, TrustDecision, TrustEvaluationOptions, }; +use std::borrow::Cow; use std::collections::BTreeMap; use std::future::Future; use std::io::Read; @@ -70,12 +71,20 @@ pub struct ValidationFailure { /// Result for a single validation stage. /// /// Stages may attach structured `metadata` to aid troubleshooting and auditing. +/// +/// ## Allocation trade-off +/// `metadata` uses `BTreeMap` for the key type even though most keys are static +/// strings. This is intentional: the map type is public, and changing keys to `Cow<'static, str>` +/// would cascade through all consumers. The key allocations are cold-path and not performance +/// critical compared to the hot-path fact engine lookups. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ValidationResult { /// Stage outcome. pub kind: ValidationResultKind, /// Friendly stage name (e.g. "Signature"). - pub validator_name: String, + /// + /// Uses `Cow<'static, str>` to avoid allocating when the name is a compile-time constant. + pub validator_name: Cow<'static, str>, /// Failures when `kind == Failure`. pub failures: Vec, /// Arbitrary stage metadata. @@ -99,7 +108,7 @@ impl ValidationResult { /// /// If `metadata` is `None`, the metadata map is empty. pub fn success( - validator_name: impl Into, + validator_name: impl Into>, metadata: Option>, ) -> Self { Self { @@ -113,11 +122,14 @@ impl ValidationResult { /// Create a not-applicable stage result. /// /// If `reason` is `Some` and non-empty, it is stored under [`Self::METADATA_REASON_KEY`]. - pub fn not_applicable(validator_name: impl Into, reason: Option<&str>) -> Self { + pub fn not_applicable( + validator_name: impl Into>, + reason: Option<&str>, + ) -> Self { let mut metadata = BTreeMap::new(); if let Some(r) = reason { if !r.trim().is_empty() { - metadata.insert(Self::METADATA_REASON_KEY.to_string(), r.to_string()); + metadata.insert(Self::METADATA_REASON_KEY.into(), r.to_string()); } } Self { @@ -129,7 +141,10 @@ impl ValidationResult { } /// Create a failed stage result. - pub fn failure(validator_name: impl Into, failures: Vec) -> Self { + pub fn failure( + validator_name: impl Into>, + failures: Vec, + ) -> Self { Self { kind: ValidationResultKind::Failure, validator_name: validator_name.into(), @@ -140,7 +155,7 @@ impl ValidationResult { /// Convenience helper for a single failure message. pub fn failure_message( - validator_name: impl Into, + validator_name: impl Into>, message: impl Into, error_code: Option<&str>, ) -> Self { @@ -707,6 +722,7 @@ impl CoseSign1Validator { // Preserve existing behavior when key resolution fails and we don't have an // integrity-attesting counter-signature to fall back to. if !trust_result.is_valid() || !counter_sig_bypassed { + // Clone required: resolution_result appears in both its stage slot and `overall`. return Ok(CoseSign1ValidationResult { resolution: resolution_result.clone(), trust: ValidationResult::not_applicable( @@ -728,8 +744,8 @@ impl CoseSign1Validator { // Bypass primary signature verification. let mut resolution_metadata = BTreeMap::new(); resolution_metadata.insert( - Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.to_string(), - Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.to_string(), + Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.into(), + Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.into(), ); let resolution_result = ValidationResult::success( Self::STAGE_NAME_KEY_MATERIAL_RESOLUTION, @@ -992,8 +1008,8 @@ impl CoseSign1Validator { let mut resolution_metadata = BTreeMap::new(); resolution_metadata.insert( - Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.to_string(), - Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.to_string(), + Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.into(), + Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.into(), ); let resolution_result = ValidationResult::success( Self::STAGE_NAME_KEY_MATERIAL_RESOLUTION, @@ -1247,13 +1263,13 @@ impl CoseSign1Validator { let mut metadata = BTreeMap::new(); if !diagnostics.is_empty() { - metadata.insert("Diagnostics".to_string(), diagnostics.join("\n")); + metadata.insert("Diagnostics".into(), diagnostics.join("\n")); } ( ValidationResult { kind: ValidationResultKind::Failure, - validator_name: Self::STAGE_NAME_KEY_MATERIAL_RESOLUTION.to_string(), + validator_name: Cow::Borrowed(Self::STAGE_NAME_KEY_MATERIAL_RESOLUTION), failures: vec![ValidationFailure { message: Self::ERROR_MESSAGE_NO_SIGNING_KEY_RESOLVED.to_string(), error_code: Some(Self::ERROR_CODE_NO_SIGNING_KEY_RESOLVED.to_string()), @@ -1357,22 +1373,22 @@ impl CoseSign1Validator { .iter() .map(|r| ValidationFailure { error_code: Some(Self::ERROR_CODE_TRUST_PLAN_NOT_SATISFIED.to_string()), - message: r.clone(), + message: r.to_string(), ..ValidationFailure::default() }) .collect() }; let mut metadata = BTreeMap::new(); - metadata.insert("TrustDecision".to_string(), format!("{decision:?}")); + metadata.insert("TrustDecision".into(), format!("{decision:?}")); if let Some(a) = audit { - metadata.insert("TrustDecisionAudit".to_string(), format!("{a:?}")); + metadata.insert("TrustDecisionAudit".into(), format!("{a:?}")); } return Ok(( ValidationResult { kind: ValidationResultKind::Failure, - validator_name: Self::STAGE_NAME_KEY_MATERIAL_TRUST.to_string(), + validator_name: Cow::Borrowed(Self::STAGE_NAME_KEY_MATERIAL_TRUST), failures, metadata, }, @@ -1383,11 +1399,11 @@ impl CoseSign1Validator { let mut metadata = BTreeMap::new(); if self.options.trust_evaluation_options.bypass_trust { - metadata.insert("BypassTrust".to_string(), "true".to_string()); + metadata.insert("BypassTrust".into(), "true".into()); } - metadata.insert("TrustDecision".to_string(), format!("{decision:?}")); + metadata.insert("TrustDecision".into(), format!("{decision:?}")); if let Some(a) = audit { - metadata.insert("TrustDecisionAudit".to_string(), format!("{a:?}")); + metadata.insert("TrustDecisionAudit".into(), format!("{a:?}")); } let signature_stage_metadata = if attempt_signature_bypass { @@ -1432,8 +1448,8 @@ impl CoseSign1Validator { if integrity_facts.iter().any(|f| f.sig_structure_intact) { let mut metadata = BTreeMap::new(); metadata.insert( - Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.to_string(), - Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.to_string(), + Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE.into(), + Self::METADATA_VALUE_SIGNATURE_VERIFICATION_BYPASSED.into(), ); if let Some(details) = integrity_facts @@ -1441,10 +1457,7 @@ impl CoseSign1Validator { .find_map(|f| f.details.as_deref()) .map(str::to_string) { - metadata.insert( - Self::METADATA_KEY_SIGNATURE_BYPASS_DETAILS.to_string(), - details, - ); + metadata.insert(Self::METADATA_KEY_SIGNATURE_BYPASS_DETAILS.into(), details); } return Some(metadata); @@ -1533,8 +1546,8 @@ impl CoseSign1Validator { let mut metadata = BTreeMap::new(); metadata.insert( - Self::METADATA_KEY_SELECTED_VALIDATOR.to_string(), - "streaming".to_string(), + Self::METADATA_KEY_SELECTED_VALIDATOR.into(), + "streaming".into(), ); // Use streaming verification via VerifyingContext @@ -1649,8 +1662,8 @@ impl CoseSign1Validator { let mut metadata = BTreeMap::new(); metadata.insert( - Self::METADATA_KEY_SELECTED_VALIDATOR.to_string(), - "non-streaming".to_string(), + Self::METADATA_KEY_SELECTED_VALIDATOR.into(), + "non-streaming".into(), ); match cose_key.verify(&sig_structure, message.signature()) { diff --git a/native/rust/validation/core/tests/async_and_streaming_coverage.rs b/native/rust/validation/core/tests/async_and_streaming_coverage.rs index f9f1cec4..78511d60 100644 --- a/native/rust/validation/core/tests/async_and_streaming_coverage.rs +++ b/native/rust/validation/core/tests/async_and_streaming_coverage.rs @@ -300,7 +300,7 @@ fn test_validate_async_trust_failure() { |_engine: &TrustFactEngine, _subject: &TrustSubject| -> Result { Ok(TrustDecision { is_trusted: false, - reasons: vec!["denied by test rule".to_string()], + reasons: vec!["denied by test rule".into()], }) }, )); diff --git a/native/rust/validation/core/tests/v2_validator_parity.rs b/native/rust/validation/core/tests/v2_validator_parity.rs index 70d7255d..7facf983 100644 --- a/native/rust/validation/core/tests/v2_validator_parity.rs +++ b/native/rust/validation/core/tests/v2_validator_parity.rs @@ -321,7 +321,7 @@ fn v2_validate_when_bypassing_trust_succeeds_and_includes_bypass_metadata() { .add_trust_source(Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| { - Ok(TrustDecision::denied(vec!["would-fail".to_string()])) + Ok(TrustDecision::denied(vec!["would-fail".into()])) }, ))) .build() diff --git a/native/rust/validation/core/tests/validator_async_tests.rs b/native/rust/validation/core/tests/validator_async_tests.rs index 1b1eca26..1d13872e 100644 --- a/native/rust/validation/core/tests/validator_async_tests.rs +++ b/native/rust/validation/core/tests/validator_async_tests.rs @@ -582,7 +582,7 @@ fn validate_async_trust_denied_short_circuits() { |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { Ok(TrustDecision { is_trusted: false, - reasons: vec!["denied".to_string()], + reasons: vec!["denied".into()], }) }, )); diff --git a/native/rust/validation/core/tests/validator_final_coverage_gaps.rs b/native/rust/validation/core/tests/validator_final_coverage_gaps.rs index 806c12ab..9d75a91d 100644 --- a/native/rust/validation/core/tests/validator_final_coverage_gaps.rs +++ b/native/rust/validation/core/tests/validator_final_coverage_gaps.rs @@ -256,7 +256,7 @@ fn test_cloneable_types() { let result = ValidationResult { kind: ValidationResultKind::Success, - validator_name: "test".to_string(), + validator_name: "test".into(), failures: vec![failure], metadata: BTreeMap::new(), }; @@ -788,7 +788,7 @@ fn test_async_post_signature_validation_default_impl() { let (message, _) = create_test_message(); let trust_decision = TrustDecision { is_trusted: true, - reasons: vec!["mock trusted decision".to_string()], + reasons: vec!["mock trusted decision".into()], }; let cose_key: Arc = Arc::new(MockVerifier { should_succeed: true, diff --git a/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs b/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs index 5b7de781..72cf2835 100644 --- a/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs +++ b/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs @@ -223,7 +223,7 @@ fn test_cloneable_types() { let result = ValidationResult { kind: ValidationResultKind::Success, - validator_name: "test".to_string(), + validator_name: "test".into(), failures: vec![failure], metadata: BTreeMap::new(), }; diff --git a/native/rust/validation/primitives/examples/trust_plan_minimal.rs b/native/rust/validation/primitives/examples/trust_plan_minimal.rs index ee9ea630..f50a3f88 100644 --- a/native/rust/validation/primitives/examples/trust_plan_minimal.rs +++ b/native/rust/validation/primitives/examples/trust_plan_minimal.rs @@ -1,79 +1,80 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use cose_sign1_validation_primitives::facts::{ - FactKey, TrustFactContext, TrustFactEngine, TrustFactProducer, -}; -use cose_sign1_validation_primitives::policy::TrustPolicyBuilder; -use cose_sign1_validation_primitives::rules::FnRule; -use cose_sign1_validation_primitives::subject::TrustSubject; -use cose_sign1_validation_primitives::TrustDecision; -use once_cell::sync::Lazy; -use std::sync::Arc; - -#[derive(Debug)] -struct ExampleFact { - pub value: String, -} - -struct ExampleProducer; - -impl TrustFactProducer for ExampleProducer { - fn name(&self) -> &'static str { - "ExampleProducer" - } - - fn produce( - &self, - ctx: &mut TrustFactContext<'_>, - ) -> Result<(), cose_sign1_validation_primitives::error::TrustError> { - // Only produce this fact when it is requested. - if ctx.requested_fact().type_id == FactKey::of::().type_id { - ctx.observe(ExampleFact { - value: "hello".to_string(), - })?; - } - - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - - fn provides(&self) -> &'static [FactKey] { - static PROVIDED: Lazy<[FactKey; 1]> = Lazy::new(|| [FactKey::of::()]); - &*PROVIDED - } -} - -fn main() { - let policy = TrustPolicyBuilder::new() - .require_fact(FactKey::of::()) - .add_trust_source(Arc::new(FnRule::new( - "trust_if_example_fact_present", - |engine: &TrustFactEngine, subject: &TrustSubject| { - let facts = engine.get_facts::(subject)?; - if facts.is_empty() { - Ok(TrustDecision::denied(vec![ - "Missing ExampleFact".to_string() - ])) - } else { - let _ = facts.iter().map(|f| f.value.len()).sum::(); - Ok(TrustDecision::trusted_reason("ExampleFactPresent")) - } - }, - ))) - .build(); - - let plan = policy.compile(); - - let engine = TrustFactEngine::new(vec![Arc::new(ExampleProducer)]); - let subject = TrustSubject::root("Message", b"seed"); - - let decision = plan - .evaluate(&engine, &subject, &Default::default()) - .expect("trust evaluation failed"); - - // Example-only: in production, avoid logging full trust decision details. - println!("decision resolved: is_trusted={}", decision.is_trusted); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation_primitives::facts::{ + FactKey, TrustFactContext, TrustFactEngine, TrustFactProducer, +}; +use cose_sign1_validation_primitives::policy::TrustPolicyBuilder; +use cose_sign1_validation_primitives::rules::FnRule; +use cose_sign1_validation_primitives::subject::TrustSubject; +use cose_sign1_validation_primitives::TrustDecision; +use once_cell::sync::Lazy; +use std::borrow::Cow; +use std::sync::Arc; + +#[derive(Debug)] +struct ExampleFact { + pub value: String, +} + +struct ExampleProducer; + +impl TrustFactProducer for ExampleProducer { + fn name(&self) -> &'static str { + "ExampleProducer" + } + + fn produce( + &self, + ctx: &mut TrustFactContext<'_>, + ) -> Result<(), cose_sign1_validation_primitives::error::TrustError> { + // Only produce this fact when it is requested. + if ctx.requested_fact().type_id == FactKey::of::().type_id { + ctx.observe(ExampleFact { + value: "hello".to_string(), + })?; + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + + fn provides(&self) -> &'static [FactKey] { + static PROVIDED: Lazy<[FactKey; 1]> = Lazy::new(|| [FactKey::of::()]); + &*PROVIDED + } +} + +fn main() { + let policy = TrustPolicyBuilder::new() + .require_fact(FactKey::of::()) + .add_trust_source(Arc::new(FnRule::new( + "trust_if_example_fact_present", + |engine: &TrustFactEngine, subject: &TrustSubject| { + let facts = engine.get_facts::(subject)?; + if facts.is_empty() { + Ok(TrustDecision::denied(vec![Cow::Borrowed( + "Missing ExampleFact", + )])) + } else { + let _ = facts.iter().map(|f| f.value.len()).sum::(); + Ok(TrustDecision::trusted_reason("ExampleFactPresent")) + } + }, + ))) + .build(); + + let plan = policy.compile(); + + let engine = TrustFactEngine::new(vec![Arc::new(ExampleProducer)]); + let subject = TrustSubject::root("Message", b"seed"); + + let decision = plan + .evaluate(&engine, &subject, &Default::default()) + .expect("trust evaluation failed"); + + // Example-only: in production, avoid logging full trust decision details. + println!("decision resolved: is_trusted={}", decision.is_trusted); +} diff --git a/native/rust/validation/primitives/src/decision.rs b/native/rust/validation/primitives/src/decision.rs index 08fdb480..658a20e8 100644 --- a/native/rust/validation/primitives/src/decision.rs +++ b/native/rust/validation/primitives/src/decision.rs @@ -1,15 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::borrow::Cow; + /// Outcome of trust evaluation for a subject. /// /// `reasons` is a human-readable list intended for diagnostics and audit logs. +/// Uses `Cow<'static, str>` to avoid allocating for static deny reasons that are +/// known at compile time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TrustDecision { /// Whether the subject is trusted. pub is_trusted: bool, /// Diagnostic reasons (denials or trust reasons). - pub reasons: Vec, + pub reasons: Vec>, } impl TrustDecision { @@ -22,7 +26,7 @@ impl TrustDecision { } /// Trusted with explicit reasons. - pub fn trusted_with(reasons: Vec) -> Self { + pub fn trusted_with(reasons: Vec>) -> Self { if reasons.is_empty() { return Self::trusted(); } @@ -33,12 +37,12 @@ impl TrustDecision { } /// Trusted with a single diagnostic reason. - pub fn trusted_reason(reason: impl Into) -> Self { + pub fn trusted_reason(reason: impl Into>) -> Self { Self::trusted_with(vec![reason.into()]) } /// Denied with explicit reasons. - pub fn denied(reasons: Vec) -> Self { + pub fn denied(reasons: Vec>) -> Self { Self { is_trusted: false, reasons, diff --git a/native/rust/validation/primitives/src/facts.rs b/native/rust/validation/primitives/src/facts.rs index fde85921..24e45880 100644 --- a/native/rust/validation/primitives/src/facts.rs +++ b/native/rust/validation/primitives/src/facts.rs @@ -25,9 +25,13 @@ pub enum TrustFactSet { /// Facts are available (may be empty). Available(Vec>), /// Fact type is missing for this subject (with an explanatory reason). - Missing { reason: String }, + /// + /// Uses `Arc` to avoid cloning the reason string on each `get_fact_set` call. + Missing { reason: Arc }, /// Fact production failed (message is intended for diagnostics). - Error { message: String }, + /// + /// Uses `Arc` to avoid cloning the message string on each `get_fact_set` call. + Error { message: Arc }, } impl TrustFactSet { @@ -175,8 +179,8 @@ impl<'a> TrustFactContext<'a> { struct EngineState { facts: HashMap>>>, produced: HashSet<(SubjectId, TypeId)>, - missing: HashMap<(SubjectId, TypeId), String>, - errors: HashMap<(SubjectId, TypeId), String>, + missing: HashMap<(SubjectId, TypeId), Arc>, + errors: HashMap<(SubjectId, TypeId), Arc>, } /// Fact engine responsible for: @@ -277,7 +281,7 @@ impl TrustFactEngine { match self.get_fact_set::(subject)? { TrustFactSet::Available(v) => Ok(v), TrustFactSet::Missing { .. } => Ok(Vec::new()), - TrustFactSet::Error { message } => Err(TrustError::FactProduction(message)), + TrustFactSet::Error { message } => Err(TrustError::FactProduction(message.to_string())), } } @@ -291,13 +295,13 @@ impl TrustFactEngine { let state = self.state.lock().expect("lock poisoned"); if let Some(message) = state.errors.get(&(subject.id, TypeId::of::())) { return Ok(TrustFactSet::Error { - message: message.clone(), + message: Arc::clone(message), }); } if let Some(reason) = state.missing.get(&(subject.id, TypeId::of::())) { return Ok(TrustFactSet::Missing { - reason: reason.clone(), + reason: Arc::clone(reason), }); } @@ -390,13 +394,13 @@ impl TrustFactEngine { /// Marks a specific subject/type as missing. fn mark_missing(&self, subject: SubjectId, type_id: TypeId, reason: String) { let mut state = self.state.lock().expect("lock poisoned"); - state.missing.insert((subject, type_id), reason); + state.missing.insert((subject, type_id), Arc::from(reason)); } /// Marks a specific subject/type as errored. fn mark_error(&self, subject: SubjectId, type_id: TypeId, message: String) { let mut state = self.state.lock().expect("lock poisoned"); - state.errors.insert((subject, type_id), message); + state.errors.insert((subject, type_id), Arc::from(message)); } /// Records an observed fact value for the subject and optionally emits an audit event. diff --git a/native/rust/validation/primitives/src/fluent.rs b/native/rust/validation/primitives/src/fluent.rs index 01bd0c9c..d5d3262c 100644 --- a/native/rust/validation/primitives/src/fluent.rs +++ b/native/rust/validation/primitives/src/fluent.rs @@ -198,10 +198,12 @@ where if derived.is_empty() { return Ok(match self.on_empty { OnEmptyBehavior::Allow => crate::decision::TrustDecision::trusted(), - OnEmptyBehavior::Deny => crate::decision::TrustDecision::denied(vec![format!( - "No subjects in scope {}", - self.scope.scope_name() - )]), + OnEmptyBehavior::Deny => { + crate::decision::TrustDecision::denied(vec![std::borrow::Cow::Owned(format!( + "No subjects in scope {}", + self.scope.scope_name() + ))]) + } }); } diff --git a/native/rust/validation/primitives/src/rules.rs b/native/rust/validation/primitives/src/rules.rs index f1ddbe87..70d98f68 100644 --- a/native/rust/validation/primitives/src/rules.rs +++ b/native/rust/validation/primitives/src/rules.rs @@ -19,6 +19,7 @@ use crate::subject::TrustSubject; #[cfg(feature = "regex")] use regex::Regex; use std::any::Any; +use std::borrow::Cow; use std::sync::Arc; use std::sync::Mutex; @@ -158,9 +159,9 @@ impl TrustRule for AnyOf { subject: &TrustSubject, ) -> Result { if self.rules.is_empty() { - return Ok(TrustDecision::denied(vec![ - "No trust sources were satisfied".to_string(), - ])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed( + "No trust sources were satisfied", + )])); } let mut reasons = Vec::new(); @@ -195,7 +196,7 @@ impl TrustRule for Not { ) -> Result { let d = self.rule.evaluate(engine, subject)?; Ok(if d.is_trusted { - TrustDecision::denied(vec![self.reason.to_string()]) + TrustDecision::denied(vec![Cow::Borrowed(self.reason)]) } else { TrustDecision::trusted() }) @@ -269,14 +270,14 @@ where if values.iter().any(|v| predicate(v.as_ref())) { Ok(TrustDecision::trusted()) } else { - Ok(TrustDecision::denied(vec![deny_reason.to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])) } } - TrustFactSet::Missing { reason } => Ok(TrustDecision::denied(vec![format!( - "{deny_reason}: {reason}" + TrustFactSet::Missing { reason } => Ok(TrustDecision::denied(vec![Cow::Owned( + format!("{deny_reason}: {reason}"), )])), - TrustFactSet::Error { message } => Ok(TrustDecision::denied(vec![format!( - "{deny_reason}: {message}" + TrustFactSet::Error { message } => Ok(TrustDecision::denied(vec![Cow::Owned( + format!("{deny_reason}: {message}"), )])), } }, @@ -504,29 +505,29 @@ where let values = match set { TrustFactSet::Available(values) => values, TrustFactSet::Missing { reason } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {reason}" - )])) + ))])) } TrustFactSet::Error { message } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {message}" - )])) + ))])) } }; let Some(selected) = select_fact(&values, &selector) else { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); }; let Some(actual) = selected.get_property(property) else { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); }; Ok(if predicate.matches(actual) { TrustDecision::trusted() } else { - TrustDecision::denied(vec![deny_reason.to_string()]) + TrustDecision::denied(vec![Cow::Borrowed(deny_reason)]) }) }, )) @@ -557,21 +558,21 @@ where let values = match set { TrustFactSet::Available(values) => values, TrustFactSet::Missing { reason } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {reason}" - )])) + ))])) } TrustFactSet::Error { message } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {message}" - )])) + ))])) } }; Ok(if select_fact(&values, &selector).is_some() { TrustDecision::trusted() } else { - TrustDecision::denied(vec![deny_reason.to_string()]) + TrustDecision::denied(vec![Cow::Borrowed(deny_reason)]) }) }, )) @@ -605,7 +606,7 @@ where return Ok(match missing { MissingBehavior::Allow => TrustDecision::trusted(), MissingBehavior::Deny => { - TrustDecision::denied(vec![deny_reason.to_string()]) + TrustDecision::denied(vec![Cow::Borrowed(deny_reason)]) } }) } @@ -616,7 +617,9 @@ where } else { match missing { MissingBehavior::Allow => TrustDecision::trusted(), - MissingBehavior::Deny => TrustDecision::denied(vec![deny_reason.to_string()]), + MissingBehavior::Deny => { + TrustDecision::denied(vec![Cow::Borrowed(deny_reason)]) + } } }) }, @@ -701,52 +704,54 @@ where let left_values = match left_set { TrustFactSet::Available(values) => values, TrustFactSet::Missing { reason } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {reason}" - )])) + ))])) } TrustFactSet::Error { message } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {message}" - )])) + ))])) } }; let right_values = match right_set { TrustFactSet::Available(values) => values, TrustFactSet::Missing { reason } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {reason}" - )])) + ))])) } TrustFactSet::Error { message } => { - return Ok(TrustDecision::denied(vec![format!( + return Ok(TrustDecision::denied(vec![Cow::Owned(format!( "{deny_reason}: {message}" - )])) + ))])) } }; let Some(left) = select_fact(&left_values, &left_selector) else { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); }; let right = select_fact(&right_values, &right_selector); let Some(right) = right else { return Ok(match missing_right { MissingBehavior::Allow => TrustDecision::trusted(), - MissingBehavior::Deny => TrustDecision::denied(vec![deny_reason.to_string()]), + MissingBehavior::Deny => { + TrustDecision::denied(vec![Cow::Borrowed(deny_reason)]) + } }); }; for (left_prop, right_prop) in &property_pairs { let Some(left_val) = left.get_property(left_prop) else { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); }; let Some(right_val) = right.get_property(right_prop) else { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); }; if left_val != right_val { - return Ok(TrustDecision::denied(vec![deny_reason.to_string()])); + return Ok(TrustDecision::denied(vec![Cow::Borrowed(deny_reason)])); } } diff --git a/native/rust/validation/primitives/tests/coverage_boost.rs b/native/rust/validation/primitives/tests/coverage_boost.rs index cd8d8ade..2fe4c908 100644 --- a/native/rust/validation/primitives/tests/coverage_boost.rs +++ b/native/rust/validation/primitives/tests/coverage_boost.rs @@ -1442,7 +1442,7 @@ fn not_rule_denied_inner_returns_trusted() { let deny_rule: TrustRuleRef = Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["inner denied".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("inner denied")])) }, )); @@ -1479,7 +1479,7 @@ fn any_of_first_denied_second_trusted() { let deny: TrustRuleRef = Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["no".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("no")])) }, )); let allow: TrustRuleRef = allow_all("allow"); @@ -1734,7 +1734,7 @@ fn compiled_plan_or_plans_multiple() { let deny_rule: TrustRuleRef = Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["no".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("no")])) }, )); let plan2 = CompiledTrustPlan::new(vec![], vec![], vec![deny_rule], vec![]); diff --git a/native/rust/validation/primitives/tests/facts_coverage.rs b/native/rust/validation/primitives/tests/facts_coverage.rs index 6131e4d9..42ba2e48 100644 --- a/native/rust/validation/primitives/tests/facts_coverage.rs +++ b/native/rust/validation/primitives/tests/facts_coverage.rs @@ -203,7 +203,7 @@ fn mark_error_causes_get_fact_set_to_return_error() { let fact_set = engine.get_fact_set::(&subject).unwrap(); match fact_set { TrustFactSet::Error { message } => { - assert_eq!(message, "production failed"); + assert_eq!(&*message, "production failed"); } other => panic!("expected Error, got: {other:?}"), } @@ -240,7 +240,7 @@ fn mark_missing_causes_get_fact_set_to_return_missing() { let fact_set = engine.get_fact_set::(&subject).unwrap(); match fact_set { TrustFactSet::Missing { reason } => { - assert_eq!(reason, "not available"); + assert_eq!(&*reason, "not available"); } other => panic!("expected Missing, got: {other:?}"), } diff --git a/native/rust/validation/primitives/tests/final_targeted_rules.rs b/native/rust/validation/primitives/tests/final_targeted_rules.rs index 41e855dc..42e9b2f7 100644 --- a/native/rust/validation/primitives/tests/final_targeted_rules.rs +++ b/native/rust/validation/primitives/tests/final_targeted_rules.rs @@ -15,7 +15,6 @@ //! - AuditedRule (line 778) use cose_sign1_validation_primitives::audit::TrustDecisionAuditBuilder; -use cose_sign1_validation_primitives::decision::TrustDecision; use cose_sign1_validation_primitives::error::TrustError; use cose_sign1_validation_primitives::fact_properties::{ FactProperties, FactValue, FactValueOwned, diff --git a/native/rust/validation/primitives/tests/ids_subject_decision_tests.rs b/native/rust/validation/primitives/tests/ids_subject_decision_tests.rs index 869e3b04..5982476a 100644 --- a/native/rust/validation/primitives/tests/ids_subject_decision_tests.rs +++ b/native/rust/validation/primitives/tests/ids_subject_decision_tests.rs @@ -9,6 +9,7 @@ use cose_sign1_validation_primitives::ids::{ }; use cose_sign1_validation_primitives::subject::TrustSubject; use cose_sign1_validation_primitives::TrustDecision; +use std::borrow::Cow; #[test] fn subject_id_to_hex_is_64_chars() { @@ -82,7 +83,7 @@ fn trust_decision_helpers_behave_as_expected() { assert!(one.is_trusted); assert_eq!(vec!["ok".to_string()], one.reasons); - let denied = TrustDecision::denied(vec!["no".to_string()]); + let denied = TrustDecision::denied(vec![Cow::Borrowed("no")]); assert!(!denied.is_trusted); assert_eq!(vec!["no".to_string()], denied.reasons); } diff --git a/native/rust/validation/primitives/tests/rule_property_edge_cases.rs b/native/rust/validation/primitives/tests/rule_property_edge_cases.rs index 87a7874b..8d765275 100644 --- a/native/rust/validation/primitives/tests/rule_property_edge_cases.rs +++ b/native/rust/validation/primitives/tests/rule_property_edge_cases.rs @@ -3,7 +3,6 @@ //! Additional coverage tests for rule selection and property matching edge cases. -use cose_sign1_validation_primitives::decision::TrustDecision; use cose_sign1_validation_primitives::error::TrustError; use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; use cose_sign1_validation_primitives::facts::{ diff --git a/native/rust/validation/primitives/tests/rules_policy_audit_tests.rs b/native/rust/validation/primitives/tests/rules_policy_audit_tests.rs index 2f7abef2..649f9c1e 100644 --- a/native/rust/validation/primitives/tests/rules_policy_audit_tests.rs +++ b/native/rust/validation/primitives/tests/rules_policy_audit_tests.rs @@ -12,6 +12,7 @@ use cose_sign1_validation_primitives::{ subject::TrustSubject, TrustDecision, }; +use std::borrow::Cow; use std::sync::Mutex; use std::sync::{ atomic::{AtomicUsize, Ordering}, @@ -71,13 +72,13 @@ fn all_of_aggregates_denial_reasons() { let r2: TrustRuleRef = Arc::new(FnRule::new( "r2", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["nope".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("nope")])) }, )); let r3: TrustRuleRef = Arc::new(FnRule::new( "r3", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["still nope".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("still nope")])) }, )); @@ -125,7 +126,7 @@ fn any_of_short_circuits_on_first_trusted() { "r2", move |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { r2_called.fetch_add(100, Ordering::SeqCst); - Ok(TrustDecision::denied(vec!["should not run".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("should not run")])) }, )); @@ -154,7 +155,7 @@ fn not_inverts_decision_and_emits_reason() { let inner: TrustRuleRef = Arc::new(FnRule::new( "inner", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["deny".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("deny")])) }, )); let d = not_with_reason("not", inner, "custom") @@ -172,7 +173,7 @@ fn audited_rule_records_audit_event() { let inner: TrustRuleRef = Arc::new(FnRule::new( "inner", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["x".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("x")])) }, )); @@ -232,7 +233,7 @@ fn policy_builder_adds_rules_and_compiles() { let deny: TrustRuleRef = Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["no".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("no")])) }, )); @@ -375,7 +376,7 @@ fn compiled_plan_from_rule_and_bypass_paths() { let deny: TrustRuleRef = Arc::new(FnRule::new( "deny", |_e: &TrustFactEngine, _s: &TrustSubject| -> Result { - Ok(TrustDecision::denied(vec!["no".to_string()])) + Ok(TrustDecision::denied(vec![Cow::Borrowed("no")])) }, )); From 052199e0fa072b4a2d124c5c1861a87955480615 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Mon, 6 Apr 2026 15:31:13 -0700 Subject: [PATCH 3/8] Zero-allocation optimizations across signing, certificates, MST, and AAS crates Tier 0 (payload-scale): - SigningContext: Added Borrowed(&[u8]) variant, factory uses from_slice() eliminating full payload copy - MST JWKS cache: Arc wrapping, get() returns refcount bump not 5-50KB deep clone - MST proof blobs: extract_proof_blobs() returns Vec instead of Vec> Tier 1 (per-validation hot paths): - Certificates: 23 fact struct fields String -> Arc, ParsedCert fields -> Arc - MST proofs: Hash fields [u8;32] fixed arrays, path Vec<(bool,[u8;32])> - CWT claims: claims_bytes Vec -> ArcSlice Tier 2 (per-operation): - AAS digest: Hash digests stay on stack as GenericArray, eliminated 4x heap alloc per signature - AAS validation: eku_oids uses .to_string() for Vec compatibility All 7,886 tests pass. Clippy clean. Zero regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/signing/aas_crypto_signer.rs | 35 +- .../src/validation/mod.rs | 2 +- .../certificates/src/validation/facts.rs | 85 +- .../certificates/src/validation/pack.rs | 18 +- .../certificates/tests/cert_fact_sets.rs | 2 +- .../tests/chain_trust_more_coverage.rs | 8 +- .../certificates/tests/coverage_boost.rs | 2 +- .../certificates/tests/coverage_close_gaps.rs | 2 +- .../certificates/tests/deep_cert_coverage.rs | 2074 ++++++++--------- .../tests/fact_properties_coverage.rs | 21 +- .../tests/fact_properties_more.rs | 47 +- .../mst/src/validation/jwks_cache.rs | 17 +- .../mst/src/validation/receipt_verify.rs | 85 +- .../mst/src/validation/verify.rs | 2 +- .../mst/tests/deep_mst_coverage.rs | 41 +- .../mst/tests/final_targeted_mst_coverage.rs | 51 +- .../mst/tests/internal_helper_coverage.rs | 69 +- .../receipt_verify_comprehensive_coverage.rs | 75 +- native/rust/signing/core/src/context.rs | 20 +- native/rust/signing/core/src/signer.rs | 4 +- native/rust/signing/core/src/traits.rs | 4 +- .../rust/signing/core/tests/context_tests.rs | 10 + .../signing/factories/src/direct/factory.rs | 4 +- .../headers/src/cwt_claims_contributor.rs | 11 +- .../src/cwt_claims_header_contributor.rs | 9 +- 25 files changed, 1304 insertions(+), 1394 deletions(-) diff --git a/native/rust/extension_packs/azure_artifact_signing/src/signing/aas_crypto_signer.rs b/native/rust/extension_packs/azure_artifact_signing/src/signing/aas_crypto_signer.rs index 451a5950..628d299c 100644 --- a/native/rust/extension_packs/azure_artifact_signing/src/signing/aas_crypto_signer.rs +++ b/native/rust/extension_packs/azure_artifact_signing/src/signing/aas_crypto_signer.rs @@ -33,17 +33,32 @@ impl CryptoSigner for AasCryptoSigner { // COSE sign expects us to sign the Sig_structure bytes. // AAS expects a pre-computed digest. Hash here based on algorithm. use sha2::Digest; - let digest = match self.algorithm_name.as_str() { - "RS256" | "PS256" | "ES256" => sha2::Sha256::digest(data).to_vec(), - "RS384" | "PS384" | "ES384" => sha2::Sha384::digest(data).to_vec(), - "RS512" | "PS512" | "ES512" => sha2::Sha512::digest(data).to_vec(), - _ => sha2::Sha256::digest(data).to_vec(), - }; - let (signature, _cert_der) = self - .source - .sign_digest(&self.algorithm_name, &digest) - .map_err(|e| CryptoError::SigningFailed(e.to_string()))?; + // Keep digests on the stack as fixed-size arrays instead of heap-allocating + // via to_vec(). sign_digest accepts &[u8], so we pass a slice reference. + let (signature, _cert_der) = match self.algorithm_name.as_str() { + "RS256" | "PS256" | "ES256" => { + let digest = sha2::Sha256::digest(data); + self.source + .sign_digest(&self.algorithm_name, digest.as_slice()) + } + "RS384" | "PS384" | "ES384" => { + let digest = sha2::Sha384::digest(data); + self.source + .sign_digest(&self.algorithm_name, digest.as_slice()) + } + "RS512" | "PS512" | "ES512" => { + let digest = sha2::Sha512::digest(data); + self.source + .sign_digest(&self.algorithm_name, digest.as_slice()) + } + _ => { + let digest = sha2::Sha256::digest(data); + self.source + .sign_digest(&self.algorithm_name, digest.as_slice()) + } + } + .map_err(|e| CryptoError::SigningFailed(e.to_string()))?; Ok(signature) } diff --git a/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs b/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs index 6d476fe0..6f9be600 100644 --- a/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs +++ b/native/rust/extension_packs/azure_artifact_signing/src/validation/mod.rs @@ -49,7 +49,7 @@ impl TrustFactProducer for AasFactProducer { // Check EKU facts for Microsoft-specific OIDs if let Ok(cose_sign1_validation_primitives::facts::TrustFactSet::Available(ekus)) = ctx.get_fact_set::(ctx.subject()) { for eku in &ekus { - eku_oids.push(eku.oid_value.clone()); + eku_oids.push(eku.oid_value.to_string()); if eku.oid_value.starts_with("1.3.6.1.4.1.311") { is_ats_issued = true; } diff --git a/native/rust/extension_packs/certificates/src/validation/facts.rs b/native/rust/extension_packs/certificates/src/validation/facts.rs index 7d5a82a5..eb09ed9b 100644 --- a/native/rust/extension_packs/certificates/src/validation/facts.rs +++ b/native/rust/extension_packs/certificates/src/validation/facts.rs @@ -4,13 +4,14 @@ use cose_sign1_primitives::ArcSlice; use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; use std::borrow::Cow; +use std::sync::Arc; #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509SigningCertificateIdentityFact { - pub certificate_thumbprint: String, - pub subject: String, - pub issuer: String, - pub serial_number: String, + pub certificate_thumbprint: Arc, + pub subject: Arc, + pub issuer: Arc, + pub serial_number: Arc, pub not_before_unix_seconds: i64, pub not_after_unix_seconds: i64, } @@ -144,44 +145,44 @@ pub mod typed_fields { #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509SigningCertificateIdentityAllowedFact { - pub certificate_thumbprint: String, - pub subject: String, - pub issuer: String, + pub certificate_thumbprint: Arc, + pub subject: Arc, + pub issuer: Arc, pub is_allowed: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509SigningCertificateEkuFact { - pub certificate_thumbprint: String, - pub oid_value: String, + pub certificate_thumbprint: Arc, + pub oid_value: Arc, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509SigningCertificateKeyUsageFact { - pub certificate_thumbprint: String, + pub certificate_thumbprint: Arc, pub usages: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509SigningCertificateBasicConstraintsFact { - pub certificate_thumbprint: String, + pub certificate_thumbprint: Arc, pub is_ca: bool, pub path_len_constraint: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509X5ChainCertificateIdentityFact { - pub certificate_thumbprint: String, - pub subject: String, - pub issuer: String, + pub certificate_thumbprint: Arc, + pub subject: Arc, + pub issuer: Arc, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509ChainElementIdentityFact { pub index: usize, - pub certificate_thumbprint: String, - pub subject: String, - pub issuer: String, + pub certificate_thumbprint: Arc, + pub subject: Arc, + pub issuer: Arc, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -196,27 +197,27 @@ pub struct X509ChainTrustedFact { pub chain_built: bool, pub is_trusted: bool, pub status_flags: u32, - pub status_summary: Option, + pub status_summary: Option>, pub element_count: usize, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CertificateSigningKeyTrustFact { - pub thumbprint: String, - pub subject: String, - pub issuer: String, + pub thumbprint: Arc, + pub subject: Arc, + pub issuer: Arc, pub chain_built: bool, pub chain_trusted: bool, pub chain_status_flags: u32, - pub chain_status_summary: Option, + pub chain_status_summary: Option>, } /// Fact capturing the public key algorithm OID; this stays robust for PQC/unknown algorithms. #[derive(Debug, Clone, PartialEq, Eq)] pub struct X509PublicKeyAlgorithmFact { - pub certificate_thumbprint: String, - pub algorithm_oid: String, - pub algorithm_name: Option, + pub certificate_thumbprint: Arc, + pub algorithm_oid: Arc, + pub algorithm_name: Option>, pub is_pqc: bool, } @@ -225,11 +226,11 @@ impl FactProperties for X509SigningCertificateIdentityFact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - self.certificate_thumbprint.as_str(), + &self.certificate_thumbprint, ))), - "subject" => Some(FactValue::Str(Cow::Borrowed(self.subject.as_str()))), - "issuer" => Some(FactValue::Str(Cow::Borrowed(self.issuer.as_str()))), - "serial_number" => Some(FactValue::Str(Cow::Borrowed(self.serial_number.as_str()))), + "subject" => Some(FactValue::Str(Cow::Borrowed(&self.subject))), + "issuer" => Some(FactValue::Str(Cow::Borrowed(&self.issuer))), + "serial_number" => Some(FactValue::Str(Cow::Borrowed(&self.serial_number))), "not_before_unix_seconds" => Some(FactValue::I64(self.not_before_unix_seconds)), "not_after_unix_seconds" => Some(FactValue::I64(self.not_after_unix_seconds)), _ => None, @@ -243,10 +244,10 @@ impl FactProperties for X509ChainElementIdentityFact { match name { "index" => Some(FactValue::Usize(self.index)), "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - self.certificate_thumbprint.as_str(), + &self.certificate_thumbprint, ))), - "subject" => Some(FactValue::Str(Cow::Borrowed(self.subject.as_str()))), - "issuer" => Some(FactValue::Str(Cow::Borrowed(self.issuer.as_str()))), + "subject" => Some(FactValue::Str(Cow::Borrowed(&self.subject))), + "issuer" => Some(FactValue::Str(Cow::Borrowed(&self.issuer))), _ => None, } } @@ -274,8 +275,8 @@ impl FactProperties for X509ChainTrustedFact { "element_count" => Some(FactValue::Usize(self.element_count)), "status_summary" => self .status_summary - .as_ref() - .map(|v| FactValue::Str(Cow::Borrowed(v.as_str()))), + .as_deref() + .map(|v| FactValue::Str(Cow::Borrowed(v))), _ => None, } } @@ -286,13 +287,13 @@ impl FactProperties for X509PublicKeyAlgorithmFact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - self.certificate_thumbprint.as_str(), + &self.certificate_thumbprint, ))), - "algorithm_oid" => Some(FactValue::Str(Cow::Borrowed(self.algorithm_oid.as_str()))), + "algorithm_oid" => Some(FactValue::Str(Cow::Borrowed(&self.algorithm_oid))), "algorithm_name" => self .algorithm_name - .as_ref() - .map(|v| FactValue::Str(Cow::Borrowed(v.as_str()))), + .as_deref() + .map(|v| FactValue::Str(Cow::Borrowed(v))), "is_pqc" => Some(FactValue::Bool(self.is_pqc)), _ => None, } @@ -304,10 +305,10 @@ impl FactProperties for X509PublicKeyAlgorithmFact { pub(crate) struct ParsedCert { /// Certificate DER bytes — zero-copy ArcSlice when parsed from COSE message buffer. pub der: ArcSlice, - pub thumbprint_sha1_hex: String, - pub subject: String, - pub issuer: String, - pub serial_hex: String, + pub thumbprint_sha1_hex: Arc, + pub subject: Arc, + pub issuer: Arc, + pub serial_hex: Arc, pub not_before_unix_seconds: i64, pub not_after_unix_seconds: i64, } diff --git a/native/rust/extension_packs/certificates/src/validation/pack.rs b/native/rust/extension_packs/certificates/src/validation/pack.rs index b0cc64cc..8112a7c1 100644 --- a/native/rust/extension_packs/certificates/src/validation/pack.rs +++ b/native/rust/extension_packs/certificates/src/validation/pack.rs @@ -311,12 +311,12 @@ impl X509CertificateTrustPack { let mut sha256_hasher = sha2::Sha256::new(); sha256_hasher.update(&*der); - let thumb = hex_encode_upper(&sha256_hasher.finalize()); + let thumb: Arc = Arc::from(hex_encode_upper(&sha256_hasher.finalize())); - let subject = cert.subject().to_string(); - let issuer = cert.issuer().to_string(); + let subject: Arc = Arc::from(cert.subject().to_string()); + let issuer: Arc = Arc::from(cert.issuer().to_string()); - let serial_hex = hex_encode_upper(&cert.serial.to_bytes_be()); + let serial_hex: Arc = Arc::from(hex_encode_upper(&cert.serial.to_bytes_be())); let not_before_unix_seconds = cert.validity().not_before.timestamp(); let not_after_unix_seconds = cert.validity().not_after.timestamp(); @@ -448,7 +448,7 @@ impl X509CertificateTrustPack { let is_pqc = self.is_pqc_oid(&oid); ctx.observe(X509PublicKeyAlgorithmFact { certificate_thumbprint: cert.thumbprint_sha1_hex.clone(), - algorithm_oid: oid, + algorithm_oid: Arc::from(oid), algorithm_name: None, is_pqc, })?; @@ -461,7 +461,7 @@ impl X509CertificateTrustPack { let emit = |oid: &str| { ctx.observe(X509SigningCertificateEkuFact { certificate_thumbprint: cert.thumbprint_sha1_hex.clone(), - oid_value: oid.to_string(), + oid_value: Arc::from(oid), }) }; @@ -667,12 +667,12 @@ impl X509CertificateTrustPack { }; let is_trusted = self.options.trust_embedded_chain_as_trusted && well_formed; - let (status_flags, status_summary) = if is_trusted { + let (status_flags, status_summary): (u32, Option>) = if is_trusted { (0u32, None) } else if self.options.trust_embedded_chain_as_trusted { - (1u32, Some("EmbeddedChainNotWellFormed".into())) + (1u32, Some(Arc::from("EmbeddedChainNotWellFormed"))) } else { - (1u32, Some("TrustEvaluationDisabled".into())) + (1u32, Some(Arc::from("TrustEvaluationDisabled"))) }; ctx.observe(X509ChainTrustedFact { diff --git a/native/rust/extension_packs/certificates/tests/cert_fact_sets.rs b/native/rust/extension_packs/certificates/tests/cert_fact_sets.rs index 6e76b328..5f576367 100644 --- a/native/rust/extension_packs/certificates/tests/cert_fact_sets.rs +++ b/native/rust/extension_packs/certificates/tests/cert_fact_sets.rs @@ -88,7 +88,7 @@ fn signing_certificate_facts_are_available_when_x5chain_present() { .unwrap(); match eku { TrustFactSet::Available(v) => { - assert!(v.iter().any(|f| f.oid_value == "1.3.6.1.5.5.7.3.3")); + assert!(v.iter().any(|f| &*f.oid_value == "1.3.6.1.5.5.7.3.3")); } _ => panic!("expected Available EKU facts"), } diff --git a/native/rust/extension_packs/certificates/tests/chain_trust_more_coverage.rs b/native/rust/extension_packs/certificates/tests/chain_trust_more_coverage.rs index 55f20655..6a643af7 100644 --- a/native/rust/extension_packs/certificates/tests/chain_trust_more_coverage.rs +++ b/native/rust/extension_packs/certificates/tests/chain_trust_more_coverage.rs @@ -175,8 +175,8 @@ fn chain_trust_reports_trust_evaluation_disabled_when_not_trusting_embedded_chai assert!(v[0].chain_built); assert!(!v[0].is_trusted); assert_eq!( - Some("TrustEvaluationDisabled".to_string()), - v[0].status_summary + v[0].status_summary.as_deref(), + Some("TrustEvaluationDisabled") ); } @@ -229,7 +229,7 @@ fn chain_trust_reports_not_well_formed_when_trusting_embedded_chain_but_chain_is assert!(v[0].chain_built); assert!(!v[0].is_trusted); assert_eq!( - Some("EmbeddedChainNotWellFormed".to_string()), - v[0].status_summary + v[0].status_summary.as_deref(), + Some("EmbeddedChainNotWellFormed") ); } diff --git a/native/rust/extension_packs/certificates/tests/coverage_boost.rs b/native/rust/extension_packs/certificates/tests/coverage_boost.rs index 0440b968..bde1c488 100644 --- a/native/rust/extension_packs/certificates/tests/coverage_boost.rs +++ b/native/rust/extension_packs/certificates/tests/coverage_boost.rs @@ -465,7 +465,7 @@ fn produce_signing_cert_facts_with_any_eku() { .unwrap(); match eku { TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); assert!(oids.contains(&"1.3.6.1.5.5.7.3.1")); // server_auth assert!(oids.contains(&"1.3.6.1.5.5.7.3.2")); // client_auth assert!(oids.contains(&"1.3.6.1.5.5.7.3.3")); // code_signing diff --git a/native/rust/extension_packs/certificates/tests/coverage_close_gaps.rs b/native/rust/extension_packs/certificates/tests/coverage_close_gaps.rs index 4ebc43a0..1d254833 100644 --- a/native/rust/extension_packs/certificates/tests/coverage_close_gaps.rs +++ b/native/rust/extension_packs/certificates/tests/coverage_close_gaps.rs @@ -296,7 +296,7 @@ fn all_standard_eku_oids_emitted() { .unwrap(); match eku { TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); assert!(oids.contains(&"1.3.6.1.5.5.7.3.1"), "ServerAuth missing"); assert!(oids.contains(&"1.3.6.1.5.5.7.3.2"), "ClientAuth missing"); assert!(oids.contains(&"1.3.6.1.5.5.7.3.3"), "CodeSigning missing"); diff --git a/native/rust/extension_packs/certificates/tests/deep_cert_coverage.rs b/native/rust/extension_packs/certificates/tests/deep_cert_coverage.rs index 821eb6e8..b9057351 100644 --- a/native/rust/extension_packs/certificates/tests/deep_cert_coverage.rs +++ b/native/rust/extension_packs/certificates/tests/deep_cert_coverage.rs @@ -1,1037 +1,1037 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Deep coverage tests for certificates pack.rs and certificate_header_contributor.rs. -//! -//! Targets uncovered lines in: -//! - validation/pack.rs: counter-signature paths, chain identity/validity iteration, -//! chain trust well-formed logic, EKU extraction paths, key usage bit scanning, -//! basic constraints, identity pinning denied path, produce() dispatch branches, -//! and chain-trust summary fields. -//! - signing/certificate_header_contributor.rs: build_x5t / build_x5chain encoding -//! and contribute_protected_headers / contribute_unprotected_headers via -//! HeaderContributor trait. - -use std::sync::Arc; - -use cbor_primitives::{CborEncoder, CborProvider}; -use cbor_primitives_everparse::EverParseCborProvider; -use cose_sign1_certificates::validation::facts::*; -use cose_sign1_certificates::validation::pack::{ - CertificateTrustOptions, X509CertificateTrustPack, -}; -use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message}; -use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, SigningContext}; -use cose_sign1_validation_primitives::facts::{TrustFactEngine, TrustFactSet}; -use cose_sign1_validation_primitives::subject::TrustSubject; -use crypto_primitives::{CryptoError, CryptoSigner}; -use rcgen::{ - CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, - PKCS_ECDSA_P256_SHA256, -}; - -// --------------------------------------------------------------------------- -// Helper: generate a self-signed cert with specific extensions -// --------------------------------------------------------------------------- - -/// Generate a real DER certificate with the requested extensions. -fn generate_cert_with_extensions( - cn: &str, - is_ca: Option, - key_usages: &[KeyUsagePurpose], - ekus: &[ExtendedKeyUsagePurpose], -) -> (Vec, KeyPair) { - let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let mut params = CertificateParams::new(vec![format!("{}.example", cn)]).unwrap(); - params.distinguished_name.push(DnType::CommonName, cn); - - if let Some(path_len) = is_ca { - params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Constrained(path_len)); - } else { - params.is_ca = IsCa::NoCa; - } - - params.key_usages = key_usages.to_vec(); - params.extended_key_usages = ekus.to_vec(); - - let cert = params.self_signed(&kp).unwrap(); - (cert.der().to_vec(), kp) -} - -/// Generate a simple self-signed leaf certificate. -fn generate_leaf(cn: &str) -> (Vec, KeyPair) { - generate_cert_with_extensions(cn, None, &[], &[]) -} - -/// Generate a CA cert with optional path length. -fn generate_ca(cn: &str, path_len: u8) -> (Vec, KeyPair) { - generate_cert_with_extensions( - cn, - Some(path_len), - &[KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign], - &[], - ) -} - -// --------------------------------------------------------------------------- -// Helper: build a COSE_Sign1 message with an x5chain in the protected header -// --------------------------------------------------------------------------- - -fn protected_map_with_x5chain(certs: &[&[u8]]) -> Vec { - let p = EverParseCborProvider; - let mut enc = p.encoder(); - enc.encode_map(2).unwrap(); - // alg: ES256 - enc.encode_i64(1).unwrap(); - enc.encode_i64(-7).unwrap(); - // x5chain - enc.encode_i64(33).unwrap(); - enc.encode_array(certs.len()).unwrap(); - for c in certs { - enc.encode_bstr(c).unwrap(); - } - enc.into_bytes() -} - -fn cose_sign1_from_protected(protected_map: &[u8]) -> Vec { - let p = EverParseCborProvider; - let mut enc = p.encoder(); - enc.encode_array(4).unwrap(); - enc.encode_bstr(protected_map).unwrap(); - enc.encode_map(0).unwrap(); - enc.encode_null().unwrap(); - enc.encode_bstr(b"sig").unwrap(); - enc.into_bytes() -} - -/// Build a COSE_Sign1 with DER certs in x5chain. -fn build_cose_with_chain(chain: &[&[u8]]) -> Vec { - let pm = protected_map_with_x5chain(chain); - cose_sign1_from_protected(&pm) -} - -/// Create engine from pack + COSE bytes (also parses message). -fn engine_from(pack: X509CertificateTrustPack, cose: &[u8]) -> TrustFactEngine { - let msg = CoseSign1Message::parse(cose).unwrap(); - TrustFactEngine::new(vec![Arc::new(pack)]) - .with_cose_sign1_bytes(Arc::from(cose.to_vec().into_boxed_slice())) - .with_cose_sign1_message(Arc::new(msg)) -} - -/// Shorthand: primary signing key subject from cose bytes. -fn signing_key(cose: &[u8]) -> TrustSubject { - let msg = TrustSubject::message(cose); - TrustSubject::primary_signing_key(&msg) -} - -// ========================================================================= -// pack.rs — EKU extraction paths (lines 457-482) -// ========================================================================= - -#[test] -fn produce_eku_facts_with_code_signing() { - let (cert, _kp) = generate_cert_with_extensions( - "code-signer", - None, - &[KeyUsagePurpose::DigitalSignature], - &[ExtendedKeyUsagePurpose::CodeSigning], - ); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let eku = eng - .get_fact_set::(&sk) - .unwrap(); - match eku { - TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.3"), - "expected code_signing OID, got {:?}", - oids - ); - } - _ => panic!("expected Available EKU facts"), - } -} - -#[test] -fn produce_eku_facts_with_server_and_client_auth() { - let (cert, _kp) = generate_cert_with_extensions( - "auth-cert", - None, - &[KeyUsagePurpose::DigitalSignature], - &[ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ], - ); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let eku = eng - .get_fact_set::(&sk) - .unwrap(); - match eku { - TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.1"), - "expected server_auth OID" - ); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.2"), - "expected client_auth OID" - ); - } - _ => panic!("expected Available EKU facts"), - } -} - -#[test] -fn produce_eku_facts_with_email_protection() { - let (cert, _kp) = generate_cert_with_extensions( - "email-cert", - None, - &[KeyUsagePurpose::DigitalSignature], - &[ExtendedKeyUsagePurpose::EmailProtection], - ); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let eku = eng - .get_fact_set::(&sk) - .unwrap(); - match eku { - TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.4"), - "expected email_protection OID, got {:?}", - oids - ); - } - _ => panic!("expected Available EKU facts"), - } -} - -#[test] -fn produce_eku_facts_with_time_stamping() { - let (cert, _kp) = generate_cert_with_extensions( - "ts-cert", - None, - &[KeyUsagePurpose::DigitalSignature], - &[ExtendedKeyUsagePurpose::TimeStamping], - ); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let eku = eng - .get_fact_set::(&sk) - .unwrap(); - match eku { - TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.8"), - "expected time_stamping OID, got {:?}", - oids - ); - } - _ => panic!("expected Available EKU facts"), - } -} - -#[test] -fn produce_eku_facts_with_ocsp_signing() { - let (cert, _kp) = generate_cert_with_extensions( - "ocsp-cert", - None, - &[KeyUsagePurpose::DigitalSignature], - &[ExtendedKeyUsagePurpose::OcspSigning], - ); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let eku = eng - .get_fact_set::(&sk) - .unwrap(); - match eku { - TrustFactSet::Available(v) => { - let oids: Vec<&str> = v.iter().map(|f| f.oid_value.as_str()).collect(); - assert!( - oids.contains(&"1.3.6.1.5.5.7.3.9"), - "expected ocsp_signing OID, got {:?}", - oids - ); - } - _ => panic!("expected Available EKU facts"), - } -} - -// ========================================================================= -// pack.rs — Key usage bit scanning (lines 491-517) -// ========================================================================= - -#[test] -fn produce_key_usage_digital_signature() { - let (cert, _kp) = - generate_cert_with_extensions("ds-cert", None, &[KeyUsagePurpose::DigitalSignature], &[]); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ku = eng - .get_fact_set::(&sk) - .unwrap(); - match ku { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].usages.contains(&"DigitalSignature".to_string())); - } - _ => panic!("expected Available key usage facts"), - } -} - -#[test] -fn produce_key_usage_key_cert_sign_and_crl_sign() { - let (cert, _kp) = generate_ca("ca-ku", 0); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ku = eng - .get_fact_set::(&sk) - .unwrap(); - match ku { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!( - v[0].usages.contains(&"KeyCertSign".to_string()), - "got {:?}", - v[0].usages - ); - assert!( - v[0].usages.contains(&"CrlSign".to_string()), - "got {:?}", - v[0].usages - ); - } - _ => panic!("expected Available key usage facts"), - } -} - -#[test] -fn produce_key_usage_key_encipherment() { - let (cert, _kp) = - generate_cert_with_extensions("ke-cert", None, &[KeyUsagePurpose::KeyEncipherment], &[]); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ku = eng - .get_fact_set::(&sk) - .unwrap(); - match ku { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!( - v[0].usages.contains(&"KeyEncipherment".to_string()), - "got {:?}", - v[0].usages - ); - } - _ => panic!("expected Available key usage facts"), - } -} - -#[test] -fn produce_key_usage_content_commitment() { - let (cert, _kp) = - generate_cert_with_extensions("cc-cert", None, &[KeyUsagePurpose::ContentCommitment], &[]); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ku = eng - .get_fact_set::(&sk) - .unwrap(); - match ku { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - // ContentCommitment maps to NonRepudiation in RFC 5280. - assert!( - v[0].usages.contains(&"NonRepudiation".to_string()), - "got {:?}", - v[0].usages - ); - } - _ => panic!("expected Available key usage facts"), - } -} - -#[test] -fn produce_key_usage_key_agreement() { - let (cert, _kp) = - generate_cert_with_extensions("ka-cert", None, &[KeyUsagePurpose::KeyAgreement], &[]); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ku = eng - .get_fact_set::(&sk) - .unwrap(); - match ku { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!( - v[0].usages.contains(&"KeyAgreement".to_string()), - "got {:?}", - v[0].usages - ); - } - _ => panic!("expected Available key usage facts"), - } -} - -// ========================================================================= -// pack.rs — Basic constraints facts (lines 526-540) -// ========================================================================= - -#[test] -fn produce_basic_constraints_ca_with_path_length() { - let (cert, _kp) = generate_ca("ca-bc", 3); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let bc = eng - .get_fact_set::(&sk) - .unwrap(); - match bc { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].is_ca); - assert_eq!(v[0].path_len_constraint, Some(3)); - } - _ => panic!("expected Available basic constraints facts"), - } -} - -#[test] -fn produce_basic_constraints_not_ca() { - let (cert, _kp) = generate_leaf("leaf-bc"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let bc = eng - .get_fact_set::(&sk) - .unwrap(); - match bc { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(!v[0].is_ca); - } - _ => panic!("expected Available basic constraints facts"), - } -} - -// ========================================================================= -// pack.rs — Chain identity facts with multi-element chain (lines 575-595) -// ========================================================================= - -#[test] -fn produce_chain_element_identity_and_validity_for_multi_cert_chain() { - let (leaf, _) = generate_leaf("leaf.multi"); - let (root, _) = generate_ca("root.multi", 0); - let cose = build_cose_with_chain(&[&leaf, &root]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let elems = eng - .get_fact_set::(&sk) - .unwrap(); - match elems { - TrustFactSet::Available(mut v) => { - v.sort_by_key(|e| e.index); - assert_eq!(v.len(), 2); - assert_eq!(v[0].index, 0); - assert_eq!(v[1].index, 1); - assert!(v[0].subject.contains("leaf.multi")); - assert!(v[1].subject.contains("root.multi")); - } - _ => panic!("expected Available chain element identity facts"), - } - - let validity = eng - .get_fact_set::(&sk) - .unwrap(); - match validity { - TrustFactSet::Available(mut v) => { - v.sort_by_key(|e| e.index); - assert_eq!(v.len(), 2); - assert!(v[0].not_before_unix_seconds <= v[0].not_after_unix_seconds); - assert!(v[1].not_before_unix_seconds <= v[1].not_after_unix_seconds); - } - _ => panic!("expected Available chain element validity facts"), - } -} - -// ========================================================================= -// pack.rs — Chain identity missing when no cose_sign1_bytes (lines 554-562) -// ========================================================================= - -#[test] -fn chain_identity_missing_when_no_cose_bytes() { - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let engine = TrustFactEngine::new(vec![Arc::new(pack)]); - let subject = TrustSubject::root("PrimarySigningKey", b"seed-no-bytes"); - - let x5 = engine - .get_fact_set::(&subject) - .unwrap(); - assert!( - x5.is_missing(), - "expected Missing for chain identity without cose bytes" - ); - - let elems = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(elems.is_missing()); - - let validity = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(validity.is_missing()); -} - -// ========================================================================= -// pack.rs — Chain identity missing when no x5chain in headers (lines 565-573) -// ========================================================================= - -#[test] -fn chain_identity_missing_when_no_x5chain_header() { - // Build a COSE message with only an alg header, no x5chain. - let p = EverParseCborProvider; - let mut hdr_enc = p.encoder(); - hdr_enc.encode_map(1).unwrap(); - hdr_enc.encode_i64(1).unwrap(); - hdr_enc.encode_i64(-7).unwrap(); - let pm = hdr_enc.into_bytes(); - - let cose = cose_sign1_from_protected(&pm); - let msg = CoseSign1Message::parse(&cose).unwrap(); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let engine = TrustFactEngine::new(vec![Arc::new(pack)]) - .with_cose_sign1_bytes(Arc::from(cose.clone().into_boxed_slice())) - .with_cose_sign1_message(Arc::new(msg)); - - let sk = signing_key(&cose); - - let x5 = engine - .get_fact_set::(&sk) - .unwrap(); - assert!(x5.is_missing(), "expected Missing when no x5chain"); -} - -// ========================================================================= -// pack.rs — Chain trust well-formed logic (lines 630-672) -// ========================================================================= - -#[test] -fn chain_trust_trusted_when_well_formed_and_trust_embedded_enabled() { - // A single self-signed cert: issuer == subject (well-formed root). - let (cert, _) = generate_leaf("self-signed-trusted"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::trust_embedded_chain_as_trusted(); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ct = eng.get_fact_set::(&sk).unwrap(); - match ct { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].chain_built); - assert!(v[0].is_trusted, "self-signed cert should be trusted"); - assert_eq!(v[0].status_flags, 0); - assert!(v[0].status_summary.is_none()); - assert_eq!(v[0].element_count, 1); - } - _ => panic!("expected Available chain trust"), - } - - let skt = eng - .get_fact_set::(&sk) - .unwrap(); - match skt { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].chain_built); - assert!(v[0].chain_trusted); - assert_eq!(v[0].chain_status_flags, 0); - assert!(v[0].chain_status_summary.is_none()); - } - _ => panic!("expected Available signing key trust"), - } -} - -#[test] -fn chain_trust_not_well_formed_when_issuer_mismatch() { - // Two self-signed certs that do NOT chain: issuer(0) != subject(1) - let (c1, _) = generate_leaf("leaf-one"); - let (c2, _) = generate_leaf("leaf-two"); - let cose = build_cose_with_chain(&[&c1, &c2]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions { - trust_embedded_chain_as_trusted: true, - ..Default::default() - }); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ct = eng.get_fact_set::(&sk).unwrap(); - match ct { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].chain_built); - assert!(!v[0].is_trusted); - assert_eq!(v[0].status_flags, 1); - assert_eq!( - v[0].status_summary.as_deref(), - Some("EmbeddedChainNotWellFormed") - ); - } - _ => panic!("expected Available chain trust"), - } -} - -#[test] -fn chain_trust_disabled_when_not_trusting_embedded() { - let (cert, _) = generate_leaf("disabled-trust"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions { - trust_embedded_chain_as_trusted: false, - ..Default::default() - }); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let ct = eng.get_fact_set::(&sk).unwrap(); - match ct { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(!v[0].is_trusted); - assert_eq!(v[0].status_flags, 1); - assert_eq!( - v[0].status_summary.as_deref(), - Some("TrustEvaluationDisabled") - ); - } - _ => panic!("expected Available chain trust"), - } -} - -// ========================================================================= -// pack.rs — Chain trust missing when no chain present (lines 621-628) -// ========================================================================= - -#[test] -fn chain_trust_missing_when_chain_empty() { - let p = EverParseCborProvider; - let mut hdr_enc = p.encoder(); - hdr_enc.encode_map(1).unwrap(); - hdr_enc.encode_i64(1).unwrap(); - hdr_enc.encode_i64(-7).unwrap(); - let pm = hdr_enc.into_bytes(); - let cose = cose_sign1_from_protected(&pm); - let msg = CoseSign1Message::parse(&cose).unwrap(); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let engine = TrustFactEngine::new(vec![Arc::new(pack)]) - .with_cose_sign1_bytes(Arc::from(cose.clone().into_boxed_slice())) - .with_cose_sign1_message(Arc::new(msg)); - let sk = signing_key(&cose); - - let ct = engine.get_fact_set::(&sk).unwrap(); - assert!(ct.is_missing(), "expected Missing when no x5chain"); - - let skt = engine - .get_fact_set::(&sk) - .unwrap(); - assert!(skt.is_missing()); -} - -// ========================================================================= -// pack.rs — Signing cert facts missing without cose bytes (lines 393-397) -// ========================================================================= - -#[test] -fn signing_cert_facts_missing_without_cose_bytes() { - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let engine = TrustFactEngine::new(vec![Arc::new(pack)]); - let subject = TrustSubject::root("PrimarySigningKey", b"no-cose"); - - let id = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(id.is_missing()); - - let allowed = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(allowed.is_missing()); - - let eku = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(eku.is_missing()); - - let ku = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(ku.is_missing()); - - let bc = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(bc.is_missing()); - - let alg = engine - .get_fact_set::(&subject) - .unwrap(); - assert!(alg.is_missing()); -} - -// ========================================================================= -// pack.rs — Identity pinning denied (lines 413-423 allowed=false path) -// ========================================================================= - -#[test] -fn identity_pinning_denies_non_matching_thumbprint() { - let (cert, _) = generate_leaf("deny-me"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions { - identity_pinning_enabled: true, - allowed_thumbprints: vec![ - "0000000000000000000000000000000000000000000000000000000000000000".to_string(), - ], - ..Default::default() - }); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let allowed = eng - .get_fact_set::(&sk) - .unwrap(); - match allowed { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(!v[0].is_allowed, "thumbprint should be denied"); - } - _ => panic!("expected Available identity allowed fact"), - } -} - -// ========================================================================= -// pack.rs — Public key algorithm + PQC OID matching (lines 430-442) -// ========================================================================= - -#[test] -fn public_key_algorithm_fact_produced() { - let (cert, _) = generate_leaf("alg-check"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let alg = eng.get_fact_set::(&sk).unwrap(); - match alg { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - // EC key OID should contain 1.2.840.10045 - assert!( - v[0].algorithm_oid.contains("1.2.840.10045"), - "got OID: {}", - v[0].algorithm_oid - ); - assert!(!v[0].is_pqc); - } - _ => panic!("expected Available public key algorithm fact"), - } -} - -#[test] -fn pqc_oid_flag_set_when_matching() { - let (cert, _) = generate_leaf("pqc-check"); - let cose = build_cose_with_chain(&[&cert]); - - // First discover the real OID. - let pack1 = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng1 = engine_from(pack1, &cose); - let sk = signing_key(&cose); - let real_oid = match eng1 - .get_fact_set::(&sk) - .unwrap() - { - TrustFactSet::Available(v) => v[0].algorithm_oid.clone(), - _ => panic!("need real OID"), - }; - - // Now pretend it's PQC by adding its OID to the list. - let pack2 = X509CertificateTrustPack::new(CertificateTrustOptions { - pqc_algorithm_oids: vec![real_oid.clone()], - ..Default::default() - }); - let eng2 = engine_from(pack2, &cose); - let alg = eng2 - .get_fact_set::(&sk) - .unwrap(); - match alg { - TrustFactSet::Available(v) => { - assert!(v[0].is_pqc, "expected PQC flag set for OID {}", real_oid); - } - _ => panic!("expected Available"), - } -} - -// ========================================================================= -// pack.rs — produce() dispatch for chain identity fact request (line 721) -// ========================================================================= - -#[test] -fn produce_dispatches_to_chain_identity_group_via_chain_element_identity_request() { - let (cert, _) = generate_leaf("dispatch-chain-elem"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - // Requesting X509ChainElementIdentityFact triggers the chain identity group. - let elems = eng - .get_fact_set::(&sk) - .unwrap(); - match elems { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert_eq!(v[0].index, 0); - } - _ => panic!("expected Available chain element identity facts"), - } -} - -// ========================================================================= -// pack.rs — chain trust facts via CertificateSigningKeyTrustFact dispatch (line 728) -// ========================================================================= - -#[test] -fn produce_dispatches_to_chain_trust_via_signing_key_trust_request() { - let (cert, _) = generate_leaf("dispatch-skt"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::trust_embedded_chain_as_trusted(); - let eng = engine_from(pack, &cose); - let sk = signing_key(&cose); - - let skt = eng - .get_fact_set::(&sk) - .unwrap(); - match skt { - TrustFactSet::Available(v) => { - assert_eq!(v.len(), 1); - assert!(v[0].chain_built); - assert!(v[0].chain_trusted); - } - _ => panic!("expected Available signing key trust"), - } -} - -// ========================================================================= -// pack.rs — non-signing-key subjects produce Available(empty) (line 387-390) -// ========================================================================= - -#[test] -fn non_signing_key_subject_produces_empty_for_all_cert_facts() { - let (cert, _) = generate_leaf("non-sk"); - let cose = build_cose_with_chain(&[&cert]); - let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); - let eng = engine_from(pack, &cose); - let msg_subject = TrustSubject::message(&cose); - - // Message subject is NOT a signing-key subject. - let id = eng - .get_fact_set::(&msg_subject) - .unwrap(); - match id { - TrustFactSet::Available(v) => assert!(v.is_empty()), - _ => panic!("expected Available(empty)"), - } - - let x5 = eng - .get_fact_set::(&msg_subject) - .unwrap(); - match x5 { - TrustFactSet::Available(v) => assert!(v.is_empty()), - _ => panic!("expected Available(empty)"), - } - - let ct = eng - .get_fact_set::(&msg_subject) - .unwrap(); - match ct { - TrustFactSet::Available(v) => assert!(v.is_empty()), - _ => panic!("expected Available(empty)"), - } -} - -// ========================================================================= -// certificate_header_contributor.rs — build_x5t / build_x5chain encoding -// and contribute_protected_headers / contribute_unprotected_headers -// (lines 54-58, 77-86, 95-104) -// ========================================================================= - -fn generate_test_cert() -> Vec { - let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let params = CertificateParams::new(vec!["test.example.com".to_string()]).unwrap(); - let cert = params.self_signed(&kp).unwrap(); - cert.der().to_vec() -} - -struct MockSigner; -impl CryptoSigner for MockSigner { - fn sign(&self, _data: &[u8]) -> Result, CryptoError> { - Ok(vec![1, 2, 3]) - } - fn algorithm(&self) -> i64 { - -7 - } - fn key_id(&self) -> Option<&[u8]> { - None - } - fn key_type(&self) -> &str { - "EC" - } -} - -use cose_sign1_certificates::signing::certificate_header_contributor::CertificateHeaderContributor; - -#[test] -fn header_contributor_builds_x5t_and_x5chain_for_multi_cert_chain() { - let leaf = generate_test_cert(); - let intermediate = generate_test_cert(); - let root = generate_test_cert(); - let chain: Vec<&[u8]> = vec![&leaf, &intermediate, &root]; - - let contributor = CertificateHeaderContributor::new(&leaf, &chain).unwrap(); - let mut headers = CoseHeaderMap::new(); - let signing_ctx = SigningContext::from_bytes(vec![]); - let signer = MockSigner; - let ctx = HeaderContributorContext::new(&signing_ctx, &signer); - - contributor.contribute_protected_headers(&mut headers, &ctx); - - let x5t_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5T_LABEL); - let x5chain_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5CHAIN_LABEL); - - // Both headers should be present. - assert!(headers.get(&x5t_label).is_some(), "x5t missing"); - assert!(headers.get(&x5chain_label).is_some(), "x5chain missing"); - - // Validate x5t is CBOR-encoded [alg_id, thumbprint]. - if let Some(CoseHeaderValue::Raw(x5t_bytes)) = headers.get(&x5t_label) { - let mut dec = cose_sign1_primitives::provider::decoder(x5t_bytes); - let arr_len = dec.decode_array_len().unwrap(); - assert_eq!(arr_len, Some(2), "x5t should be 2-element array"); - let alg = dec.decode_i64().unwrap(); - assert_eq!(alg, -16, "x5t alg should be SHA-256 = -16"); - let thumb = dec.decode_bstr().unwrap(); - assert_eq!(thumb.len(), 32, "SHA-256 thumbprint should be 32 bytes"); - } else { - panic!("x5t should be Raw CBOR"); - } - - // Validate x5chain is CBOR array of 3 bstr. - if let Some(CoseHeaderValue::Raw(x5c_bytes)) = headers.get(&x5chain_label) { - let mut dec = cose_sign1_primitives::provider::decoder(x5c_bytes); - let arr_len = dec.decode_array_len().unwrap(); - assert_eq!(arr_len, Some(3), "x5chain should have 3 certs"); - for _i in 0..3 { - let cert_bytes = dec.decode_bstr().unwrap(); - assert!(!cert_bytes.is_empty()); - } - } else { - panic!("x5chain should be Raw CBOR"); - } -} - -#[test] -fn header_contributor_unprotected_is_noop() { - let cert = generate_test_cert(); - let chain: Vec<&[u8]> = vec![&cert]; - let contributor = CertificateHeaderContributor::new(&cert, &chain).unwrap(); - let mut headers = CoseHeaderMap::new(); - let signing_ctx = SigningContext::from_bytes(vec![]); - let signer = MockSigner; - let ctx = HeaderContributorContext::new(&signing_ctx, &signer); - - contributor.contribute_unprotected_headers(&mut headers, &ctx); - assert!( - headers.is_empty(), - "unprotected headers should remain empty" - ); -} - -#[test] -fn header_contributor_empty_chain() { - let cert = generate_test_cert(); - let chain: Vec<&[u8]> = vec![]; - let contributor = CertificateHeaderContributor::new(&cert, &chain).unwrap(); - let mut headers = CoseHeaderMap::new(); - let signing_ctx = SigningContext::from_bytes(vec![]); - let signer = MockSigner; - let ctx = HeaderContributorContext::new(&signing_ctx, &signer); - - contributor.contribute_protected_headers(&mut headers, &ctx); - - // x5chain should still be present as an empty CBOR array. - let x5chain_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5CHAIN_LABEL); - if let Some(CoseHeaderValue::Raw(x5c_bytes)) = headers.get(&x5chain_label) { - let mut dec = cose_sign1_primitives::provider::decoder(x5c_bytes); - let arr_len = dec.decode_array_len().unwrap(); - assert_eq!( - arr_len, - Some(0), - "empty chain should produce 0-element array" - ); - } else { - panic!("x5chain should be Raw CBOR"); - } -} - -use cbor_primitives::CborDecoder; - -#[test] -fn header_contributor_merge_strategy_is_replace() { - let cert = generate_test_cert(); - let contributor = CertificateHeaderContributor::new(&cert, &[cert.as_slice()]).unwrap(); - assert!(matches!( - contributor.merge_strategy(), - cose_sign1_signing::HeaderMergeStrategy::Replace - )); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Deep coverage tests for certificates pack.rs and certificate_header_contributor.rs. +//! +//! Targets uncovered lines in: +//! - validation/pack.rs: counter-signature paths, chain identity/validity iteration, +//! chain trust well-formed logic, EKU extraction paths, key usage bit scanning, +//! basic constraints, identity pinning denied path, produce() dispatch branches, +//! and chain-trust summary fields. +//! - signing/certificate_header_contributor.rs: build_x5t / build_x5chain encoding +//! and contribute_protected_headers / contribute_unprotected_headers via +//! HeaderContributor trait. + +use std::sync::Arc; + +use cbor_primitives::{CborEncoder, CborProvider}; +use cbor_primitives_everparse::EverParseCborProvider; +use cose_sign1_certificates::validation::facts::*; +use cose_sign1_certificates::validation::pack::{ + CertificateTrustOptions, X509CertificateTrustPack, +}; +use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message}; +use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, SigningContext}; +use cose_sign1_validation_primitives::facts::{TrustFactEngine, TrustFactSet}; +use cose_sign1_validation_primitives::subject::TrustSubject; +use crypto_primitives::{CryptoError, CryptoSigner}; +use rcgen::{ + CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, + PKCS_ECDSA_P256_SHA256, +}; + +// --------------------------------------------------------------------------- +// Helper: generate a self-signed cert with specific extensions +// --------------------------------------------------------------------------- + +/// Generate a real DER certificate with the requested extensions. +fn generate_cert_with_extensions( + cn: &str, + is_ca: Option, + key_usages: &[KeyUsagePurpose], + ekus: &[ExtendedKeyUsagePurpose], +) -> (Vec, KeyPair) { + let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let mut params = CertificateParams::new(vec![format!("{}.example", cn)]).unwrap(); + params.distinguished_name.push(DnType::CommonName, cn); + + if let Some(path_len) = is_ca { + params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Constrained(path_len)); + } else { + params.is_ca = IsCa::NoCa; + } + + params.key_usages = key_usages.to_vec(); + params.extended_key_usages = ekus.to_vec(); + + let cert = params.self_signed(&kp).unwrap(); + (cert.der().to_vec(), kp) +} + +/// Generate a simple self-signed leaf certificate. +fn generate_leaf(cn: &str) -> (Vec, KeyPair) { + generate_cert_with_extensions(cn, None, &[], &[]) +} + +/// Generate a CA cert with optional path length. +fn generate_ca(cn: &str, path_len: u8) -> (Vec, KeyPair) { + generate_cert_with_extensions( + cn, + Some(path_len), + &[KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign], + &[], + ) +} + +// --------------------------------------------------------------------------- +// Helper: build a COSE_Sign1 message with an x5chain in the protected header +// --------------------------------------------------------------------------- + +fn protected_map_with_x5chain(certs: &[&[u8]]) -> Vec { + let p = EverParseCborProvider; + let mut enc = p.encoder(); + enc.encode_map(2).unwrap(); + // alg: ES256 + enc.encode_i64(1).unwrap(); + enc.encode_i64(-7).unwrap(); + // x5chain + enc.encode_i64(33).unwrap(); + enc.encode_array(certs.len()).unwrap(); + for c in certs { + enc.encode_bstr(c).unwrap(); + } + enc.into_bytes() +} + +fn cose_sign1_from_protected(protected_map: &[u8]) -> Vec { + let p = EverParseCborProvider; + let mut enc = p.encoder(); + enc.encode_array(4).unwrap(); + enc.encode_bstr(protected_map).unwrap(); + enc.encode_map(0).unwrap(); + enc.encode_null().unwrap(); + enc.encode_bstr(b"sig").unwrap(); + enc.into_bytes() +} + +/// Build a COSE_Sign1 with DER certs in x5chain. +fn build_cose_with_chain(chain: &[&[u8]]) -> Vec { + let pm = protected_map_with_x5chain(chain); + cose_sign1_from_protected(&pm) +} + +/// Create engine from pack + COSE bytes (also parses message). +fn engine_from(pack: X509CertificateTrustPack, cose: &[u8]) -> TrustFactEngine { + let msg = CoseSign1Message::parse(cose).unwrap(); + TrustFactEngine::new(vec![Arc::new(pack)]) + .with_cose_sign1_bytes(Arc::from(cose.to_vec().into_boxed_slice())) + .with_cose_sign1_message(Arc::new(msg)) +} + +/// Shorthand: primary signing key subject from cose bytes. +fn signing_key(cose: &[u8]) -> TrustSubject { + let msg = TrustSubject::message(cose); + TrustSubject::primary_signing_key(&msg) +} + +// ========================================================================= +// pack.rs — EKU extraction paths (lines 457-482) +// ========================================================================= + +#[test] +fn produce_eku_facts_with_code_signing() { + let (cert, _kp) = generate_cert_with_extensions( + "code-signer", + None, + &[KeyUsagePurpose::DigitalSignature], + &[ExtendedKeyUsagePurpose::CodeSigning], + ); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let eku = eng + .get_fact_set::(&sk) + .unwrap(); + match eku { + TrustFactSet::Available(v) => { + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.3"), + "expected code_signing OID, got {:?}", + oids + ); + } + _ => panic!("expected Available EKU facts"), + } +} + +#[test] +fn produce_eku_facts_with_server_and_client_auth() { + let (cert, _kp) = generate_cert_with_extensions( + "auth-cert", + None, + &[KeyUsagePurpose::DigitalSignature], + &[ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ], + ); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let eku = eng + .get_fact_set::(&sk) + .unwrap(); + match eku { + TrustFactSet::Available(v) => { + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.1"), + "expected server_auth OID" + ); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.2"), + "expected client_auth OID" + ); + } + _ => panic!("expected Available EKU facts"), + } +} + +#[test] +fn produce_eku_facts_with_email_protection() { + let (cert, _kp) = generate_cert_with_extensions( + "email-cert", + None, + &[KeyUsagePurpose::DigitalSignature], + &[ExtendedKeyUsagePurpose::EmailProtection], + ); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let eku = eng + .get_fact_set::(&sk) + .unwrap(); + match eku { + TrustFactSet::Available(v) => { + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.4"), + "expected email_protection OID, got {:?}", + oids + ); + } + _ => panic!("expected Available EKU facts"), + } +} + +#[test] +fn produce_eku_facts_with_time_stamping() { + let (cert, _kp) = generate_cert_with_extensions( + "ts-cert", + None, + &[KeyUsagePurpose::DigitalSignature], + &[ExtendedKeyUsagePurpose::TimeStamping], + ); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let eku = eng + .get_fact_set::(&sk) + .unwrap(); + match eku { + TrustFactSet::Available(v) => { + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.8"), + "expected time_stamping OID, got {:?}", + oids + ); + } + _ => panic!("expected Available EKU facts"), + } +} + +#[test] +fn produce_eku_facts_with_ocsp_signing() { + let (cert, _kp) = generate_cert_with_extensions( + "ocsp-cert", + None, + &[KeyUsagePurpose::DigitalSignature], + &[ExtendedKeyUsagePurpose::OcspSigning], + ); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let eku = eng + .get_fact_set::(&sk) + .unwrap(); + match eku { + TrustFactSet::Available(v) => { + let oids: Vec<&str> = v.iter().map(|f| &*f.oid_value).collect(); + assert!( + oids.contains(&"1.3.6.1.5.5.7.3.9"), + "expected ocsp_signing OID, got {:?}", + oids + ); + } + _ => panic!("expected Available EKU facts"), + } +} + +// ========================================================================= +// pack.rs — Key usage bit scanning (lines 491-517) +// ========================================================================= + +#[test] +fn produce_key_usage_digital_signature() { + let (cert, _kp) = + generate_cert_with_extensions("ds-cert", None, &[KeyUsagePurpose::DigitalSignature], &[]); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ku = eng + .get_fact_set::(&sk) + .unwrap(); + match ku { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].usages.contains(&"DigitalSignature".to_string())); + } + _ => panic!("expected Available key usage facts"), + } +} + +#[test] +fn produce_key_usage_key_cert_sign_and_crl_sign() { + let (cert, _kp) = generate_ca("ca-ku", 0); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ku = eng + .get_fact_set::(&sk) + .unwrap(); + match ku { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!( + v[0].usages.contains(&"KeyCertSign".to_string()), + "got {:?}", + v[0].usages + ); + assert!( + v[0].usages.contains(&"CrlSign".to_string()), + "got {:?}", + v[0].usages + ); + } + _ => panic!("expected Available key usage facts"), + } +} + +#[test] +fn produce_key_usage_key_encipherment() { + let (cert, _kp) = + generate_cert_with_extensions("ke-cert", None, &[KeyUsagePurpose::KeyEncipherment], &[]); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ku = eng + .get_fact_set::(&sk) + .unwrap(); + match ku { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!( + v[0].usages.contains(&"KeyEncipherment".to_string()), + "got {:?}", + v[0].usages + ); + } + _ => panic!("expected Available key usage facts"), + } +} + +#[test] +fn produce_key_usage_content_commitment() { + let (cert, _kp) = + generate_cert_with_extensions("cc-cert", None, &[KeyUsagePurpose::ContentCommitment], &[]); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ku = eng + .get_fact_set::(&sk) + .unwrap(); + match ku { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + // ContentCommitment maps to NonRepudiation in RFC 5280. + assert!( + v[0].usages.contains(&"NonRepudiation".to_string()), + "got {:?}", + v[0].usages + ); + } + _ => panic!("expected Available key usage facts"), + } +} + +#[test] +fn produce_key_usage_key_agreement() { + let (cert, _kp) = + generate_cert_with_extensions("ka-cert", None, &[KeyUsagePurpose::KeyAgreement], &[]); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ku = eng + .get_fact_set::(&sk) + .unwrap(); + match ku { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!( + v[0].usages.contains(&"KeyAgreement".to_string()), + "got {:?}", + v[0].usages + ); + } + _ => panic!("expected Available key usage facts"), + } +} + +// ========================================================================= +// pack.rs — Basic constraints facts (lines 526-540) +// ========================================================================= + +#[test] +fn produce_basic_constraints_ca_with_path_length() { + let (cert, _kp) = generate_ca("ca-bc", 3); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let bc = eng + .get_fact_set::(&sk) + .unwrap(); + match bc { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].is_ca); + assert_eq!(v[0].path_len_constraint, Some(3)); + } + _ => panic!("expected Available basic constraints facts"), + } +} + +#[test] +fn produce_basic_constraints_not_ca() { + let (cert, _kp) = generate_leaf("leaf-bc"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let bc = eng + .get_fact_set::(&sk) + .unwrap(); + match bc { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(!v[0].is_ca); + } + _ => panic!("expected Available basic constraints facts"), + } +} + +// ========================================================================= +// pack.rs — Chain identity facts with multi-element chain (lines 575-595) +// ========================================================================= + +#[test] +fn produce_chain_element_identity_and_validity_for_multi_cert_chain() { + let (leaf, _) = generate_leaf("leaf.multi"); + let (root, _) = generate_ca("root.multi", 0); + let cose = build_cose_with_chain(&[&leaf, &root]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let elems = eng + .get_fact_set::(&sk) + .unwrap(); + match elems { + TrustFactSet::Available(mut v) => { + v.sort_by_key(|e| e.index); + assert_eq!(v.len(), 2); + assert_eq!(v[0].index, 0); + assert_eq!(v[1].index, 1); + assert!(v[0].subject.contains("leaf.multi")); + assert!(v[1].subject.contains("root.multi")); + } + _ => panic!("expected Available chain element identity facts"), + } + + let validity = eng + .get_fact_set::(&sk) + .unwrap(); + match validity { + TrustFactSet::Available(mut v) => { + v.sort_by_key(|e| e.index); + assert_eq!(v.len(), 2); + assert!(v[0].not_before_unix_seconds <= v[0].not_after_unix_seconds); + assert!(v[1].not_before_unix_seconds <= v[1].not_after_unix_seconds); + } + _ => panic!("expected Available chain element validity facts"), + } +} + +// ========================================================================= +// pack.rs — Chain identity missing when no cose_sign1_bytes (lines 554-562) +// ========================================================================= + +#[test] +fn chain_identity_missing_when_no_cose_bytes() { + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let engine = TrustFactEngine::new(vec![Arc::new(pack)]); + let subject = TrustSubject::root("PrimarySigningKey", b"seed-no-bytes"); + + let x5 = engine + .get_fact_set::(&subject) + .unwrap(); + assert!( + x5.is_missing(), + "expected Missing for chain identity without cose bytes" + ); + + let elems = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(elems.is_missing()); + + let validity = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(validity.is_missing()); +} + +// ========================================================================= +// pack.rs — Chain identity missing when no x5chain in headers (lines 565-573) +// ========================================================================= + +#[test] +fn chain_identity_missing_when_no_x5chain_header() { + // Build a COSE message with only an alg header, no x5chain. + let p = EverParseCborProvider; + let mut hdr_enc = p.encoder(); + hdr_enc.encode_map(1).unwrap(); + hdr_enc.encode_i64(1).unwrap(); + hdr_enc.encode_i64(-7).unwrap(); + let pm = hdr_enc.into_bytes(); + + let cose = cose_sign1_from_protected(&pm); + let msg = CoseSign1Message::parse(&cose).unwrap(); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let engine = TrustFactEngine::new(vec![Arc::new(pack)]) + .with_cose_sign1_bytes(Arc::from(cose.clone().into_boxed_slice())) + .with_cose_sign1_message(Arc::new(msg)); + + let sk = signing_key(&cose); + + let x5 = engine + .get_fact_set::(&sk) + .unwrap(); + assert!(x5.is_missing(), "expected Missing when no x5chain"); +} + +// ========================================================================= +// pack.rs — Chain trust well-formed logic (lines 630-672) +// ========================================================================= + +#[test] +fn chain_trust_trusted_when_well_formed_and_trust_embedded_enabled() { + // A single self-signed cert: issuer == subject (well-formed root). + let (cert, _) = generate_leaf("self-signed-trusted"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::trust_embedded_chain_as_trusted(); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ct = eng.get_fact_set::(&sk).unwrap(); + match ct { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].chain_built); + assert!(v[0].is_trusted, "self-signed cert should be trusted"); + assert_eq!(v[0].status_flags, 0); + assert!(v[0].status_summary.is_none()); + assert_eq!(v[0].element_count, 1); + } + _ => panic!("expected Available chain trust"), + } + + let skt = eng + .get_fact_set::(&sk) + .unwrap(); + match skt { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].chain_built); + assert!(v[0].chain_trusted); + assert_eq!(v[0].chain_status_flags, 0); + assert!(v[0].chain_status_summary.is_none()); + } + _ => panic!("expected Available signing key trust"), + } +} + +#[test] +fn chain_trust_not_well_formed_when_issuer_mismatch() { + // Two self-signed certs that do NOT chain: issuer(0) != subject(1) + let (c1, _) = generate_leaf("leaf-one"); + let (c2, _) = generate_leaf("leaf-two"); + let cose = build_cose_with_chain(&[&c1, &c2]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions { + trust_embedded_chain_as_trusted: true, + ..Default::default() + }); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ct = eng.get_fact_set::(&sk).unwrap(); + match ct { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].chain_built); + assert!(!v[0].is_trusted); + assert_eq!(v[0].status_flags, 1); + assert_eq!( + v[0].status_summary.as_deref(), + Some("EmbeddedChainNotWellFormed") + ); + } + _ => panic!("expected Available chain trust"), + } +} + +#[test] +fn chain_trust_disabled_when_not_trusting_embedded() { + let (cert, _) = generate_leaf("disabled-trust"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions { + trust_embedded_chain_as_trusted: false, + ..Default::default() + }); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let ct = eng.get_fact_set::(&sk).unwrap(); + match ct { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(!v[0].is_trusted); + assert_eq!(v[0].status_flags, 1); + assert_eq!( + v[0].status_summary.as_deref(), + Some("TrustEvaluationDisabled") + ); + } + _ => panic!("expected Available chain trust"), + } +} + +// ========================================================================= +// pack.rs — Chain trust missing when no chain present (lines 621-628) +// ========================================================================= + +#[test] +fn chain_trust_missing_when_chain_empty() { + let p = EverParseCborProvider; + let mut hdr_enc = p.encoder(); + hdr_enc.encode_map(1).unwrap(); + hdr_enc.encode_i64(1).unwrap(); + hdr_enc.encode_i64(-7).unwrap(); + let pm = hdr_enc.into_bytes(); + let cose = cose_sign1_from_protected(&pm); + let msg = CoseSign1Message::parse(&cose).unwrap(); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let engine = TrustFactEngine::new(vec![Arc::new(pack)]) + .with_cose_sign1_bytes(Arc::from(cose.clone().into_boxed_slice())) + .with_cose_sign1_message(Arc::new(msg)); + let sk = signing_key(&cose); + + let ct = engine.get_fact_set::(&sk).unwrap(); + assert!(ct.is_missing(), "expected Missing when no x5chain"); + + let skt = engine + .get_fact_set::(&sk) + .unwrap(); + assert!(skt.is_missing()); +} + +// ========================================================================= +// pack.rs — Signing cert facts missing without cose bytes (lines 393-397) +// ========================================================================= + +#[test] +fn signing_cert_facts_missing_without_cose_bytes() { + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let engine = TrustFactEngine::new(vec![Arc::new(pack)]); + let subject = TrustSubject::root("PrimarySigningKey", b"no-cose"); + + let id = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(id.is_missing()); + + let allowed = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(allowed.is_missing()); + + let eku = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(eku.is_missing()); + + let ku = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(ku.is_missing()); + + let bc = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(bc.is_missing()); + + let alg = engine + .get_fact_set::(&subject) + .unwrap(); + assert!(alg.is_missing()); +} + +// ========================================================================= +// pack.rs — Identity pinning denied (lines 413-423 allowed=false path) +// ========================================================================= + +#[test] +fn identity_pinning_denies_non_matching_thumbprint() { + let (cert, _) = generate_leaf("deny-me"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions { + identity_pinning_enabled: true, + allowed_thumbprints: vec![ + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + ], + ..Default::default() + }); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let allowed = eng + .get_fact_set::(&sk) + .unwrap(); + match allowed { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(!v[0].is_allowed, "thumbprint should be denied"); + } + _ => panic!("expected Available identity allowed fact"), + } +} + +// ========================================================================= +// pack.rs — Public key algorithm + PQC OID matching (lines 430-442) +// ========================================================================= + +#[test] +fn public_key_algorithm_fact_produced() { + let (cert, _) = generate_leaf("alg-check"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let alg = eng.get_fact_set::(&sk).unwrap(); + match alg { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + // EC key OID should contain 1.2.840.10045 + assert!( + v[0].algorithm_oid.contains("1.2.840.10045"), + "got OID: {}", + v[0].algorithm_oid + ); + assert!(!v[0].is_pqc); + } + _ => panic!("expected Available public key algorithm fact"), + } +} + +#[test] +fn pqc_oid_flag_set_when_matching() { + let (cert, _) = generate_leaf("pqc-check"); + let cose = build_cose_with_chain(&[&cert]); + + // First discover the real OID. + let pack1 = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng1 = engine_from(pack1, &cose); + let sk = signing_key(&cose); + let real_oid = match eng1 + .get_fact_set::(&sk) + .unwrap() + { + TrustFactSet::Available(v) => v[0].algorithm_oid.clone(), + _ => panic!("need real OID"), + }; + + // Now pretend it's PQC by adding its OID to the list. + let pack2 = X509CertificateTrustPack::new(CertificateTrustOptions { + pqc_algorithm_oids: vec![real_oid.to_string()], + ..Default::default() + }); + let eng2 = engine_from(pack2, &cose); + let alg = eng2 + .get_fact_set::(&sk) + .unwrap(); + match alg { + TrustFactSet::Available(v) => { + assert!(v[0].is_pqc, "expected PQC flag set for OID {}", real_oid); + } + _ => panic!("expected Available"), + } +} + +// ========================================================================= +// pack.rs — produce() dispatch for chain identity fact request (line 721) +// ========================================================================= + +#[test] +fn produce_dispatches_to_chain_identity_group_via_chain_element_identity_request() { + let (cert, _) = generate_leaf("dispatch-chain-elem"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + // Requesting X509ChainElementIdentityFact triggers the chain identity group. + let elems = eng + .get_fact_set::(&sk) + .unwrap(); + match elems { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert_eq!(v[0].index, 0); + } + _ => panic!("expected Available chain element identity facts"), + } +} + +// ========================================================================= +// pack.rs — chain trust facts via CertificateSigningKeyTrustFact dispatch (line 728) +// ========================================================================= + +#[test] +fn produce_dispatches_to_chain_trust_via_signing_key_trust_request() { + let (cert, _) = generate_leaf("dispatch-skt"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::trust_embedded_chain_as_trusted(); + let eng = engine_from(pack, &cose); + let sk = signing_key(&cose); + + let skt = eng + .get_fact_set::(&sk) + .unwrap(); + match skt { + TrustFactSet::Available(v) => { + assert_eq!(v.len(), 1); + assert!(v[0].chain_built); + assert!(v[0].chain_trusted); + } + _ => panic!("expected Available signing key trust"), + } +} + +// ========================================================================= +// pack.rs — non-signing-key subjects produce Available(empty) (line 387-390) +// ========================================================================= + +#[test] +fn non_signing_key_subject_produces_empty_for_all_cert_facts() { + let (cert, _) = generate_leaf("non-sk"); + let cose = build_cose_with_chain(&[&cert]); + let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + let eng = engine_from(pack, &cose); + let msg_subject = TrustSubject::message(&cose); + + // Message subject is NOT a signing-key subject. + let id = eng + .get_fact_set::(&msg_subject) + .unwrap(); + match id { + TrustFactSet::Available(v) => assert!(v.is_empty()), + _ => panic!("expected Available(empty)"), + } + + let x5 = eng + .get_fact_set::(&msg_subject) + .unwrap(); + match x5 { + TrustFactSet::Available(v) => assert!(v.is_empty()), + _ => panic!("expected Available(empty)"), + } + + let ct = eng + .get_fact_set::(&msg_subject) + .unwrap(); + match ct { + TrustFactSet::Available(v) => assert!(v.is_empty()), + _ => panic!("expected Available(empty)"), + } +} + +// ========================================================================= +// certificate_header_contributor.rs — build_x5t / build_x5chain encoding +// and contribute_protected_headers / contribute_unprotected_headers +// (lines 54-58, 77-86, 95-104) +// ========================================================================= + +fn generate_test_cert() -> Vec { + let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let params = CertificateParams::new(vec!["test.example.com".to_string()]).unwrap(); + let cert = params.self_signed(&kp).unwrap(); + cert.der().to_vec() +} + +struct MockSigner; +impl CryptoSigner for MockSigner { + fn sign(&self, _data: &[u8]) -> Result, CryptoError> { + Ok(vec![1, 2, 3]) + } + fn algorithm(&self) -> i64 { + -7 + } + fn key_id(&self) -> Option<&[u8]> { + None + } + fn key_type(&self) -> &str { + "EC" + } +} + +use cose_sign1_certificates::signing::certificate_header_contributor::CertificateHeaderContributor; + +#[test] +fn header_contributor_builds_x5t_and_x5chain_for_multi_cert_chain() { + let leaf = generate_test_cert(); + let intermediate = generate_test_cert(); + let root = generate_test_cert(); + let chain: Vec<&[u8]> = vec![&leaf, &intermediate, &root]; + + let contributor = CertificateHeaderContributor::new(&leaf, &chain).unwrap(); + let mut headers = CoseHeaderMap::new(); + let signing_ctx = SigningContext::from_bytes(vec![]); + let signer = MockSigner; + let ctx = HeaderContributorContext::new(&signing_ctx, &signer); + + contributor.contribute_protected_headers(&mut headers, &ctx); + + let x5t_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5T_LABEL); + let x5chain_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5CHAIN_LABEL); + + // Both headers should be present. + assert!(headers.get(&x5t_label).is_some(), "x5t missing"); + assert!(headers.get(&x5chain_label).is_some(), "x5chain missing"); + + // Validate x5t is CBOR-encoded [alg_id, thumbprint]. + if let Some(CoseHeaderValue::Raw(x5t_bytes)) = headers.get(&x5t_label) { + let mut dec = cose_sign1_primitives::provider::decoder(x5t_bytes); + let arr_len = dec.decode_array_len().unwrap(); + assert_eq!(arr_len, Some(2), "x5t should be 2-element array"); + let alg = dec.decode_i64().unwrap(); + assert_eq!(alg, -16, "x5t alg should be SHA-256 = -16"); + let thumb = dec.decode_bstr().unwrap(); + assert_eq!(thumb.len(), 32, "SHA-256 thumbprint should be 32 bytes"); + } else { + panic!("x5t should be Raw CBOR"); + } + + // Validate x5chain is CBOR array of 3 bstr. + if let Some(CoseHeaderValue::Raw(x5c_bytes)) = headers.get(&x5chain_label) { + let mut dec = cose_sign1_primitives::provider::decoder(x5c_bytes); + let arr_len = dec.decode_array_len().unwrap(); + assert_eq!(arr_len, Some(3), "x5chain should have 3 certs"); + for _i in 0..3 { + let cert_bytes = dec.decode_bstr().unwrap(); + assert!(!cert_bytes.is_empty()); + } + } else { + panic!("x5chain should be Raw CBOR"); + } +} + +#[test] +fn header_contributor_unprotected_is_noop() { + let cert = generate_test_cert(); + let chain: Vec<&[u8]> = vec![&cert]; + let contributor = CertificateHeaderContributor::new(&cert, &chain).unwrap(); + let mut headers = CoseHeaderMap::new(); + let signing_ctx = SigningContext::from_bytes(vec![]); + let signer = MockSigner; + let ctx = HeaderContributorContext::new(&signing_ctx, &signer); + + contributor.contribute_unprotected_headers(&mut headers, &ctx); + assert!( + headers.is_empty(), + "unprotected headers should remain empty" + ); +} + +#[test] +fn header_contributor_empty_chain() { + let cert = generate_test_cert(); + let chain: Vec<&[u8]> = vec![]; + let contributor = CertificateHeaderContributor::new(&cert, &chain).unwrap(); + let mut headers = CoseHeaderMap::new(); + let signing_ctx = SigningContext::from_bytes(vec![]); + let signer = MockSigner; + let ctx = HeaderContributorContext::new(&signing_ctx, &signer); + + contributor.contribute_protected_headers(&mut headers, &ctx); + + // x5chain should still be present as an empty CBOR array. + let x5chain_label = CoseHeaderLabel::Int(CertificateHeaderContributor::X5CHAIN_LABEL); + if let Some(CoseHeaderValue::Raw(x5c_bytes)) = headers.get(&x5chain_label) { + let mut dec = cose_sign1_primitives::provider::decoder(x5c_bytes); + let arr_len = dec.decode_array_len().unwrap(); + assert_eq!( + arr_len, + Some(0), + "empty chain should produce 0-element array" + ); + } else { + panic!("x5chain should be Raw CBOR"); + } +} + +use cbor_primitives::CborDecoder; + +#[test] +fn header_contributor_merge_strategy_is_replace() { + let cert = generate_test_cert(); + let contributor = CertificateHeaderContributor::new(&cert, &[cert.as_slice()]).unwrap(); + assert!(matches!( + contributor.merge_strategy(), + cose_sign1_signing::HeaderMergeStrategy::Replace + )); +} diff --git a/native/rust/extension_packs/certificates/tests/fact_properties_coverage.rs b/native/rust/extension_packs/certificates/tests/fact_properties_coverage.rs index 86818efa..d878f711 100644 --- a/native/rust/extension_packs/certificates/tests/fact_properties_coverage.rs +++ b/native/rust/extension_packs/certificates/tests/fact_properties_coverage.rs @@ -6,14 +6,15 @@ use cose_sign1_certificates::validation::facts::{ X509PublicKeyAlgorithmFact, X509SigningCertificateIdentityFact, }; use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; +use std::sync::Arc; #[test] fn certificate_fact_properties_expose_expected_fields() { let signing = X509SigningCertificateIdentityFact { - certificate_thumbprint: "thumb".to_string(), - subject: "subj".to_string(), - issuer: "iss".to_string(), - serial_number: "serial".to_string(), + certificate_thumbprint: Arc::from("thumb"), + subject: Arc::from("subj"), + issuer: Arc::from("iss"), + serial_number: Arc::from("serial"), not_before_unix_seconds: 1, not_after_unix_seconds: 2, }; @@ -46,9 +47,9 @@ fn certificate_fact_properties_expose_expected_fields() { let chain_id = X509ChainElementIdentityFact { index: 3, - certificate_thumbprint: "t".to_string(), - subject: "s".to_string(), - issuer: "i".to_string(), + certificate_thumbprint: Arc::from("t"), + subject: Arc::from("s"), + issuer: Arc::from("i"), }; assert_eq!( @@ -75,7 +76,7 @@ fn certificate_fact_properties_expose_expected_fields() { chain_built: true, is_trusted: false, status_flags: 123, - status_summary: Some("ok".to_string()), + status_summary: Some(Arc::from("ok")), element_count: 2, }; @@ -101,8 +102,8 @@ fn certificate_fact_properties_expose_expected_fields() { )); let alg = X509PublicKeyAlgorithmFact { - certificate_thumbprint: "t".to_string(), - algorithm_oid: "1.2.3".to_string(), + certificate_thumbprint: Arc::from("t"), + algorithm_oid: Arc::from("1.2.3"), algorithm_name: None, is_pqc: true, }; diff --git a/native/rust/extension_packs/certificates/tests/fact_properties_more.rs b/native/rust/extension_packs/certificates/tests/fact_properties_more.rs index 0b391e73..da101cad 100644 --- a/native/rust/extension_packs/certificates/tests/fact_properties_more.rs +++ b/native/rust/extension_packs/certificates/tests/fact_properties_more.rs @@ -6,6 +6,7 @@ use cose_sign1_certificates::validation::facts::{ X509PublicKeyAlgorithmFact, X509SigningCertificateIdentityFact, }; use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; +use std::sync::Arc; // --------------------------------------------------------------------------- // X509ChainTrustedFact – status_summary None branch @@ -34,9 +35,9 @@ fn chain_trusted_status_summary_none_returns_none() { #[test] fn public_key_algorithm_name_some_returns_value() { let fact = X509PublicKeyAlgorithmFact { - certificate_thumbprint: "abc".to_string(), - algorithm_oid: "1.2.840.113549.1.1.11".to_string(), - algorithm_name: Some("RSA-SHA256".to_string()), + certificate_thumbprint: Arc::from("abc"), + algorithm_oid: Arc::from("1.2.840.113549.1.1.11"), + algorithm_name: Some(Arc::from("RSA-SHA256")), is_pqc: false, }; @@ -49,8 +50,8 @@ fn public_key_algorithm_name_some_returns_value() { #[test] fn public_key_algorithm_name_none_returns_none() { let fact = X509PublicKeyAlgorithmFact { - certificate_thumbprint: "abc".to_string(), - algorithm_oid: "1.2.3".to_string(), + certificate_thumbprint: Arc::from("abc"), + algorithm_oid: Arc::from("1.2.3"), algorithm_name: None, is_pqc: false, }; @@ -68,10 +69,10 @@ fn public_key_algorithm_name_none_returns_none() { #[test] fn signing_cert_identity_unknown_property_returns_none() { let fact = X509SigningCertificateIdentityFact { - certificate_thumbprint: "t".to_string(), - subject: "s".to_string(), - issuer: "i".to_string(), - serial_number: "sn".to_string(), + certificate_thumbprint: Arc::from("t"), + subject: Arc::from("s"), + issuer: Arc::from("i"), + serial_number: Arc::from("sn"), not_before_unix_seconds: 0, not_after_unix_seconds: 0, }; @@ -85,9 +86,9 @@ fn signing_cert_identity_unknown_property_returns_none() { fn chain_element_identity_unknown_property_returns_none() { let fact = X509ChainElementIdentityFact { index: 0, - certificate_thumbprint: "t".to_string(), - subject: "s".to_string(), - issuer: "i".to_string(), + certificate_thumbprint: Arc::from("t"), + subject: Arc::from("s"), + issuer: Arc::from("i"), }; assert_eq!(fact.get_property("nonexistent"), None); @@ -112,7 +113,7 @@ fn chain_trusted_unknown_property_returns_none() { chain_built: false, is_trusted: false, status_flags: 0, - status_summary: Some("summary".to_string()), + status_summary: Some(Arc::from("summary")), element_count: 0, }; @@ -123,9 +124,9 @@ fn chain_trusted_unknown_property_returns_none() { #[test] fn public_key_algorithm_unknown_property_returns_none() { let fact = X509PublicKeyAlgorithmFact { - certificate_thumbprint: "t".to_string(), - algorithm_oid: "1.2.3".to_string(), - algorithm_name: Some("name".to_string()), + certificate_thumbprint: Arc::from("t"), + algorithm_oid: Arc::from("1.2.3"), + algorithm_name: Some(Arc::from("name")), is_pqc: false, }; @@ -141,9 +142,9 @@ fn public_key_algorithm_unknown_property_returns_none() { fn chain_element_identity_all_valid_properties() { let fact = X509ChainElementIdentityFact { index: 7, - certificate_thumbprint: "thumb123".to_string(), - subject: "CN=Test".to_string(), - issuer: "CN=Issuer".to_string(), + certificate_thumbprint: Arc::from("thumb123"), + subject: Arc::from("CN=Test"), + issuer: Arc::from("CN=Issuer"), }; assert_eq!( @@ -200,7 +201,7 @@ fn chain_trusted_all_valid_properties_with_summary() { chain_built: false, is_trusted: true, status_flags: 42, - status_summary: Some("all good".to_string()), + status_summary: Some(Arc::from("all good")), element_count: 5, }; @@ -233,9 +234,9 @@ fn chain_trusted_all_valid_properties_with_summary() { #[test] fn public_key_algorithm_all_valid_properties() { let fact = X509PublicKeyAlgorithmFact { - certificate_thumbprint: "tp".to_string(), - algorithm_oid: "1.3.6.1.4.1.2.267.7.6.5".to_string(), - algorithm_name: Some("ML-DSA-65".to_string()), + certificate_thumbprint: Arc::from("tp"), + algorithm_oid: Arc::from("1.3.6.1.4.1.2.267.7.6.5"), + algorithm_name: Some(Arc::from("ML-DSA-65")), is_pqc: true, }; diff --git a/native/rust/extension_packs/mst/src/validation/jwks_cache.rs b/native/rust/extension_packs/mst/src/validation/jwks_cache.rs index 206b09af..21d9826f 100644 --- a/native/rust/extension_packs/mst/src/validation/jwks_cache.rs +++ b/native/rust/extension_packs/mst/src/validation/jwks_cache.rs @@ -23,7 +23,7 @@ use code_transparency_client::JwksDocument; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::RwLock; +use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; /// Default TTL for cached JWKS entries (1 hour). @@ -38,8 +38,8 @@ pub const DEFAULT_VERIFICATION_WINDOW: usize = 20; /// A cached JWKS entry with metadata. #[derive(Debug, Clone)] struct CacheEntry { - /// The cached JWKS document. - jwks: JwksDocument, + /// The cached JWKS document, wrapped in Arc for zero-copy sharing. + jwks: Arc, /// When this entry was last fetched/refreshed. fetched_at: Instant, /// Count of consecutive key-lookup misses against this entry. @@ -163,7 +163,7 @@ impl JwksCache { ( issuer, CacheEntry { - jwks, + jwks: Arc::new(jwks), fetched_at: now, consecutive_misses: 0, }, @@ -184,9 +184,12 @@ impl JwksCache { /// Look up a cached JWKS for an issuer. Returns `None` if not cached or stale. /// + /// Returns an `Arc` — callers get a refcount bump instead of + /// a deep clone (5-50 KB saved per lookup). + /// /// A stale entry (older than `refresh_interval`) returns `None` so the /// caller fetches fresh data and calls [`insert`](Self::insert). - pub fn get(&self, issuer: &str) -> Option { + pub fn get(&self, issuer: &str) -> Option> { let inner = self.inner.read().ok()?; let entry = inner.entries.get(issuer)?; @@ -230,7 +233,7 @@ impl JwksCache { inner.entries.insert( issuer.to_string(), CacheEntry { - jwks, + jwks: Arc::new(jwks), fetched_at: Instant::now(), consecutive_misses: 0, }, @@ -325,7 +328,7 @@ impl JwksCache { let serializable: HashMap<&str, &JwksDocument> = inner .entries .iter() - .map(|(k, v)| (k.as_str(), &v.jwks)) + .map(|(k, v)| (k.as_str(), v.jwks.as_ref())) .collect(); if let Ok(json) = serde_json::to_string_pretty(&serializable) { let _ = std::fs::write(path, json); diff --git a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs index cbe61c82..d638e520 100644 --- a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs +++ b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs @@ -2,7 +2,9 @@ // Licensed under the MIT License. use cbor_primitives::{CborDecoder, CborEncoder}; -use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message}; +use cose_sign1_primitives::{ + ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message, +}; use crypto_primitives::{EcJwk, JwkVerifierFactory}; use serde::Deserialize; use sha2::{Digest, Sha256}; @@ -229,7 +231,7 @@ pub fn verify_mst_receipt( let mut any_matching_data_hash = false; for proof_blob in proof_blobs { - let proof = MstCcfInclusionProof::parse(proof_blob.as_slice())?; + let proof = MstCcfInclusionProof::parse(&proof_blob)?; // Compute CCF accumulator (leaf hash) and fold proof path. // If the proof doesn't match this statement, try the next blob. @@ -242,14 +244,10 @@ pub fn verify_mst_receipt( Err(e) => return Err(e), }; for (is_left, sibling) in proof.path.iter() { - let sibling: [u8; 32] = sibling.as_slice().try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode("unexpected_path_hash_len".to_string()) - })?; - acc = if *is_left { - sha256_concat_slices(&sibling, &acc) + sha256_concat_slices(sibling, &acc) } else { - sha256_concat_slices(&acc, &sibling) + sha256_concat_slices(&acc, sibling) }; } @@ -412,10 +410,10 @@ pub(crate) fn fetch_jwks_for_issuer( #[derive(Clone, Debug)] pub struct MstCcfInclusionProof { - pub internal_txn_hash: Vec, + pub internal_txn_hash: [u8; 32], pub internal_evidence: String, - pub data_hash: Vec, - pub path: Vec<(bool, Vec)>, + pub data_hash: [u8; 32], + pub path: Vec<(bool, [u8; 32])>, } impl MstCcfInclusionProof { @@ -431,7 +429,7 @@ impl MstCcfInclusionProof { .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; let mut leaf_raw: Option> = None; - let mut path: Option)>> = None; + let mut path: Option> = None; for _ in 0..map_len.unwrap_or(usize::MAX) { let k = d @@ -469,18 +467,23 @@ impl MstCcfInclusionProof { } /// Parse a CCF proof leaf (array) into its components. -pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<(Vec, String, Vec), ReceiptVerifyError> { +pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), ReceiptVerifyError> { let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); let _arr_len = d .decode_array_len() .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - let internal_txn_hash = d + let internal_txn_hash_slice = d .decode_bstr() .map_err(|e| { ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) - })? - .to_vec(); + })?; + let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_internal_txn_hash_len: {}", + internal_txn_hash_slice.len() + )) + })?; let internal_evidence = d .decode_tstr() @@ -489,16 +492,21 @@ pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<(Vec, String, Vec), Recei })? .to_string(); - let data_hash = d + let data_hash_slice = d .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))? - .to_vec(); + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; + let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_data_hash_len: {}", + data_hash_slice.len() + )) + })?; Ok((internal_txn_hash, internal_evidence, data_hash)) } /// Parse a CCF proof path value into a sequence of (direction, sibling_hash) pairs. -pub fn parse_path(bytes: &[u8]) -> Result)>, ReceiptVerifyError> { +pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyError> { let mut d = cose_sign1_primitives::provider::decoder(bytes); let arr_len = d .decode_array_len() @@ -521,10 +529,15 @@ pub fn parse_path(bytes: &[u8]) -> Result)>, ReceiptVerifyErr let bytes_item = vd .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))? - .to_vec(); + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; + let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_path_hash_len: {}", + bytes_item.len() + )) + })?; - out.push((is_left, bytes_item)); + out.push((is_left, hash)); } Ok(out) @@ -533,9 +546,10 @@ pub fn parse_path(bytes: &[u8]) -> Result)>, ReceiptVerifyErr /// Extract proof blobs from the parsed VDP header value (unprotected header 396). /// /// The MST receipt places an array of proof blobs under label `-1` in the VDP map. +/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. pub fn extract_proof_blobs( vdp_value: &CoseHeaderValue, -) -> Result>, ReceiptVerifyError> { +) -> Result, ReceiptVerifyError> { let pairs = match vdp_value { CoseHeaderValue::Map(pairs) => pairs, _ => { @@ -562,7 +576,7 @@ pub fn extract_proof_blobs( let mut out = Vec::new(); for item in arr { match item { - CoseHeaderValue::Bytes(b) => out.push(b.to_vec()), + CoseHeaderValue::Bytes(b) => out.push(b.clone()), _ => { return Err(ReceiptVerifyError::ReceiptDecode( "proof_item_not_bstr".to_string(), @@ -610,32 +624,21 @@ pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), Recei /// Compute the CCF accumulator (leaf hash) for an inclusion proof. /// -/// This validates expected field sizes, checks that the proof's `data_hash` matches the statement -/// digest, and then hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. +/// Checks that the proof's `data_hash` matches the statement digest, and then +/// hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. +/// Hash field sizes are guaranteed at parse time via `[u8; 32]` types. pub fn ccf_accumulator_sha256( proof: &MstCcfInclusionProof, expected_data_hash: [u8; 32], ) -> Result<[u8; 32], ReceiptVerifyError> { - if proof.internal_txn_hash.len() != 32 { - return Err(ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_internal_txn_hash_len: {}", - proof.internal_txn_hash.len() - ))); - } - if proof.data_hash.len() != 32 { - return Err(ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_data_hash_len: {}", - proof.data_hash.len() - ))); - } - if proof.data_hash.as_slice() != expected_data_hash.as_slice() { + if proof.data_hash != expected_data_hash { return Err(ReceiptVerifyError::DataHashMismatch); } let internal_evidence_hash = sha256(proof.internal_evidence.as_bytes()); let mut h = Sha256::new(); - h.update(proof.internal_txn_hash.as_slice()); + h.update(proof.internal_txn_hash); h.update(internal_evidence_hash); h.update(expected_data_hash); let out = h.finalize(); diff --git a/native/rust/extension_packs/mst/src/validation/verify.rs b/native/rust/extension_packs/mst/src/validation/verify.rs index cf37cb18..92976f10 100644 --- a/native/rust/extension_packs/mst/src/validation/verify.rs +++ b/native/rust/extension_packs/mst/src/validation/verify.rs @@ -326,7 +326,7 @@ fn resolve_jwks_for_issuer( ) -> Option { if let Some(ref cache) = options.jwks_cache { if let Some(doc) = cache.get(issuer) { - return serde_json::to_string(&doc).ok(); + return serde_json::to_string(&*doc).ok(); } } if options.allow_network_fetch { diff --git a/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs b/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs index 76d399ae..5822be43 100644 --- a/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs @@ -258,8 +258,8 @@ fn extract_proof_blobs_valid() { let value = CoseHeaderValue::Map(pairs); let result = extract_proof_blobs(&value).unwrap(); assert_eq!(result.len(), 2); - assert_eq!(result[0], blob1); - assert_eq!(result[1], blob2); + assert_eq!(&*result[0], &blob1[..]); + assert_eq!(&*result[1], &blob2[..]); } // ========================================================================= @@ -526,40 +526,15 @@ fn find_jwk_invalid_json() { // ccf_accumulator_sha256 // ========================================================================= -#[test] -fn ccf_accumulator_bad_txn_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 16], // wrong length (not 32) - internal_evidence: "evidence".to_string(), - data_hash: vec![0u8; 32], - path: vec![], - }; - let result = ccf_accumulator_sha256(&proof, [0u8; 32]); - assert!(result.is_err()); - let msg = format!("{}", result.unwrap_err()); - assert!(msg.contains("unexpected_internal_txn_hash_len")); -} - -#[test] -fn ccf_accumulator_bad_data_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 32], - internal_evidence: "evidence".to_string(), - data_hash: vec![0u8; 16], // wrong length (not 32) - path: vec![], - }; - let result = ccf_accumulator_sha256(&proof, [0u8; 32]); - assert!(result.is_err()); - let msg = format!("{}", result.unwrap_err()); - assert!(msg.contains("unexpected_data_hash_len")); -} +// Wrong-length hash tests have been removed because MstCcfInclusionProof now +// uses [u8; 32] fixed arrays — invalid lengths are caught at parse time. #[test] fn ccf_accumulator_data_hash_mismatch() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 32], + internal_txn_hash: [0u8; 32], internal_evidence: "evidence".to_string(), - data_hash: vec![1u8; 32], // different from expected + data_hash: [1u8; 32], // different from expected path: vec![], }; let result = ccf_accumulator_sha256(&proof, [0u8; 32]); @@ -572,9 +547,9 @@ fn ccf_accumulator_data_hash_mismatch() { fn ccf_accumulator_valid() { let data_hash = [0xABu8; 32]; let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 32], + internal_txn_hash: [0u8; 32], internal_evidence: "some evidence".to_string(), - data_hash: data_hash.to_vec(), + data_hash, path: vec![], }; let result = ccf_accumulator_sha256(&proof, data_hash); diff --git a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs index 1e991488..cb4a4e09 100644 --- a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs @@ -564,9 +564,9 @@ fn test_ccf_accumulator_matching_hash() { let data_hash = sha256(b"statement bytes"); let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0xAA; 32], + internal_txn_hash: [0xAA; 32], internal_evidence: "evidence".to_string(), - data_hash: data_hash.to_vec(), + data_hash, path: vec![], }; @@ -579,9 +579,9 @@ fn test_ccf_accumulator_matching_hash() { #[test] fn test_ccf_accumulator_mismatched_hash() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0xAA; 32], + internal_txn_hash: [0xAA; 32], internal_evidence: "evidence".to_string(), - data_hash: vec![0xBB; 32], + data_hash: [0xBB; 32], path: vec![], }; @@ -593,43 +593,8 @@ fn test_ccf_accumulator_mismatched_hash() { } } -#[test] -fn test_ccf_accumulator_wrong_txn_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0xAA; 16], // Wrong length - internal_evidence: "ev".to_string(), - data_hash: vec![0xBB; 32], - path: vec![], - }; - - let result = ccf_accumulator_sha256(&proof, [0xBB; 32]); - assert!(result.is_err()); - match result { - Err(ReceiptVerifyError::ReceiptDecode(msg)) => { - assert!(msg.contains("unexpected_internal_txn_hash_len")); - } - other => panic!("Expected ReceiptDecode, got: {:?}", other), - } -} - -#[test] -fn test_ccf_accumulator_wrong_data_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0xAA; 32], - internal_evidence: "ev".to_string(), - data_hash: vec![0xBB; 16], // Wrong length - path: vec![], - }; - - let result = ccf_accumulator_sha256(&proof, [0xBB; 32]); - assert!(result.is_err()); - match result { - Err(ReceiptVerifyError::ReceiptDecode(msg)) => { - assert!(msg.contains("unexpected_data_hash_len")); - } - other => panic!("Expected ReceiptDecode, got: {:?}", other), - } -} +// Wrong-length hash tests have been removed because MstCcfInclusionProof now +// uses [u8; 32] fixed arrays — invalid lengths are caught at parse time. // ============================================================================ // Target: lines 533-574 — extract_proof_blobs @@ -653,8 +618,8 @@ fn test_extract_proof_blobs_valid() { assert!(result.is_ok()); let blobs = result.unwrap(); assert_eq!(blobs.len(), 2); - assert_eq!(blobs[0], blob1); - assert_eq!(blobs[1], blob2); + assert_eq!(&*blobs[0], &blob1[..]); + assert_eq!(&*blobs[1], &blob2[..]); } #[test] diff --git a/native/rust/extension_packs/mst/tests/internal_helper_coverage.rs b/native/rust/extension_packs/mst/tests/internal_helper_coverage.rs index 23e45d1c..76cd8d09 100644 --- a/native/rust/extension_packs/mst/tests/internal_helper_coverage.rs +++ b/native/rust/extension_packs/mst/tests/internal_helper_coverage.rs @@ -51,10 +51,10 @@ fn test_validate_cose_alg_supported_rs256() { #[test] fn test_ccf_accumulator_sha256_valid() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0x42; 32], // 32 bytes + internal_txn_hash: [0x42; 32], // 32 bytes internal_evidence: "test evidence".to_string(), - data_hash: vec![0x01; 32], // 32 bytes - path: vec![(true, vec![0x02; 32])], + data_hash: [0x01; 32], // 32 bytes + path: vec![(true, [0x02; 32])], }; let expected_data_hash = [0x01; 32]; @@ -66,52 +66,15 @@ fn test_ccf_accumulator_sha256_valid() { assert_eq!(result.unwrap(), result2.unwrap()); } -#[test] -fn test_ccf_accumulator_sha256_wrong_internal_txn_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0x42; 31], // Wrong length - internal_evidence: "test evidence".to_string(), - data_hash: vec![0x01; 32], - path: vec![], - }; - - let expected_data_hash = [0x01; 32]; - let result = ccf_accumulator_sha256(&proof, expected_data_hash); - assert!(result.is_err()); - match result.unwrap_err() { - ReceiptVerifyError::ReceiptDecode(msg) => { - assert!(msg.contains("unexpected_internal_txn_hash_len: 31")); - } - _ => panic!("Wrong error type"), - } -} - -#[test] -fn test_ccf_accumulator_sha256_wrong_data_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0x42; 32], - internal_evidence: "test evidence".to_string(), - data_hash: vec![0x01; 31], // Wrong length - path: vec![], - }; - - let expected_data_hash = [0x01; 32]; - let result = ccf_accumulator_sha256(&proof, expected_data_hash); - assert!(result.is_err()); - match result.unwrap_err() { - ReceiptVerifyError::ReceiptDecode(msg) => { - assert!(msg.contains("unexpected_data_hash_len: 31")); - } - _ => panic!("Wrong error type"), - } -} +// Wrong-length hash tests have been removed because MstCcfInclusionProof now +// uses [u8; 32] fixed arrays — invalid lengths are caught at parse time. #[test] fn test_ccf_accumulator_sha256_data_hash_mismatch() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0x42; 32], + internal_txn_hash: [0x42; 32], internal_evidence: "test evidence".to_string(), - data_hash: vec![0x01; 32], + data_hash: [0x01; 32], path: vec![], }; @@ -141,8 +104,8 @@ fn test_extract_proof_blobs_valid_map() { let result = extract_proof_blobs(&vdp_value).unwrap(); assert_eq!(result.len(), 2); - assert_eq!(result[0], vec![0x01, 0x02, 0x03]); - assert_eq!(result[1], vec![0x04, 0x05, 0x06]); + assert_eq!(&*result[0], &[0x01, 0x02, 0x03]); + assert_eq!(&*result[1], &[0x04, 0x05, 0x06]); } #[test] @@ -352,12 +315,12 @@ fn test_mst_ccf_inclusion_proof_parse_valid() { let proof_blob = enc.into_bytes(); let result = MstCcfInclusionProof::parse(&proof_blob).unwrap(); - assert_eq!(result.internal_txn_hash, vec![0x42; 32]); + assert_eq!(result.internal_txn_hash, [0x42; 32]); assert_eq!(result.internal_evidence, "test evidence"); - assert_eq!(result.data_hash, vec![0x01; 32]); + assert_eq!(result.data_hash, [0x01; 32]); assert_eq!(result.path.len(), 1); assert_eq!(result.path[0].0, true); - assert_eq!(result.path[0].1, vec![0x02; 32]); + assert_eq!(result.path[0].1, [0x02; 32]); } #[test] @@ -425,9 +388,9 @@ fn test_parse_leaf_valid() { let leaf_bytes = enc.into_bytes(); let result = parse_leaf(&leaf_bytes).unwrap(); - assert_eq!(result.0, vec![0x42; 32]); // internal_txn_hash + assert_eq!(result.0, [0x42; 32]); // internal_txn_hash assert_eq!(result.1, "test evidence"); // internal_evidence - assert_eq!(result.2, vec![0x01; 32]); // data_hash + assert_eq!(result.2, [0x01; 32]); // data_hash } #[test] @@ -470,9 +433,9 @@ fn test_parse_path_valid() { assert_eq!(result.len(), 2); assert_eq!(result[0].0, true); - assert_eq!(result[0].1, vec![0x01; 32]); + assert_eq!(result[0].1, [0x01; 32]); assert_eq!(result[1].0, false); - assert_eq!(result[1].1, vec![0x02; 32]); + assert_eq!(result[1].1, [0x02; 32]); } #[test] diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs b/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs index 63ed95b2..db5a834e 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs @@ -65,10 +65,10 @@ fn test_validate_cose_alg_supported_common_unsupported() { #[test] fn test_ccf_accumulator_sha256_valid() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![1u8; 32], // 32 bytes + internal_txn_hash: [1u8; 32], // 32 bytes internal_evidence: "test_evidence".to_string(), - data_hash: vec![2u8; 32], // 32 bytes - path: vec![], // Not used in accumulator calculation + data_hash: [2u8; 32], // 32 bytes + path: vec![], // Not used in accumulator calculation }; let expected_data_hash = [2u8; 32]; @@ -96,56 +96,15 @@ fn test_ccf_accumulator_sha256_valid() { assert_eq!(&accumulator[..], &expected_accumulator[..]); } -#[test] -fn test_ccf_accumulator_sha256_wrong_internal_txn_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![1u8; 31], // Wrong length (should be 32) - internal_evidence: "test_evidence".to_string(), - data_hash: vec![2u8; 32], - path: vec![], - }; - - let expected_data_hash = [2u8; 32]; - let result = ccf_accumulator_sha256(&proof, expected_data_hash); - - assert!(result.is_err()); - match result.unwrap_err() { - ReceiptVerifyError::ReceiptDecode(msg) => { - assert!(msg.contains("unexpected_internal_txn_hash_len")); - assert!(msg.contains("31")); - } - _ => panic!("Expected ReceiptDecode error"), - } -} - -#[test] -fn test_ccf_accumulator_sha256_wrong_data_hash_len() { - let proof = MstCcfInclusionProof { - internal_txn_hash: vec![1u8; 32], - internal_evidence: "test_evidence".to_string(), - data_hash: vec![2u8; 31], // Wrong length (should be 32) - path: vec![], - }; - - let expected_data_hash = [2u8; 32]; - let result = ccf_accumulator_sha256(&proof, expected_data_hash); - - assert!(result.is_err()); - match result.unwrap_err() { - ReceiptVerifyError::ReceiptDecode(msg) => { - assert!(msg.contains("unexpected_data_hash_len")); - assert!(msg.contains("31")); - } - _ => panic!("Expected ReceiptDecode error"), - } -} +// Wrong-length hash tests have been removed because MstCcfInclusionProof now +// uses [u8; 32] fixed arrays — invalid lengths are caught at parse time. #[test] fn test_ccf_accumulator_sha256_data_hash_mismatch() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![1u8; 32], + internal_txn_hash: [1u8; 32], internal_evidence: "test_evidence".to_string(), - data_hash: vec![2u8; 32], // Different from expected + data_hash: [2u8; 32], // Different from expected path: vec![], }; @@ -163,9 +122,9 @@ fn test_ccf_accumulator_sha256_data_hash_mismatch() { fn test_ccf_accumulator_sha256_edge_cases() { // Test with empty internal evidence let proof = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 32], + internal_txn_hash: [0u8; 32], internal_evidence: "".to_string(), // Empty - data_hash: vec![0u8; 32], + data_hash: [0u8; 32], path: vec![], }; @@ -175,9 +134,9 @@ fn test_ccf_accumulator_sha256_edge_cases() { // Test with very long internal evidence let proof2 = MstCcfInclusionProof { - internal_txn_hash: vec![0u8; 32], + internal_txn_hash: [0u8; 32], internal_evidence: "x".repeat(10000), // Very long - data_hash: vec![0u8; 32], + data_hash: [0u8; 32], path: vec![], }; @@ -207,8 +166,8 @@ fn test_extract_proof_blobs_valid() { assert!(result.is_ok()); let blobs = result.unwrap(); assert_eq!(blobs.len(), 2); - assert_eq!(blobs[0], proof_blob1); - assert_eq!(blobs[1], proof_blob2); + assert_eq!(&*blobs[0], &proof_blob1[..]); + assert_eq!(&*blobs[1], &proof_blob2[..]); } #[test] @@ -334,7 +293,7 @@ fn test_extract_proof_blobs_multiple_labels() { assert!(result.is_ok()); let blobs = result.unwrap(); assert_eq!(blobs.len(), 1); - assert_eq!(blobs[0], proof_blob); + assert_eq!(&*blobs[0], &proof_blob[..]); } // Test error types for comprehensive coverage @@ -451,10 +410,10 @@ fn test_validate_receipt_alg_against_jwk() { #[test] fn test_mst_ccf_inclusion_proof_traits() { let proof = MstCcfInclusionProof { - internal_txn_hash: vec![1, 2, 3], + internal_txn_hash: [1; 32], internal_evidence: "test".to_string(), - data_hash: vec![4, 5, 6], - path: vec![(true, vec![7, 8]), (false, vec![9, 10])], + data_hash: [4; 32], + path: vec![(true, [7; 32]), (false, [9; 32])], }; // Test Clone diff --git a/native/rust/signing/core/src/context.rs b/native/rust/signing/core/src/context.rs index ec3f5dc6..d60f2575 100644 --- a/native/rust/signing/core/src/context.rs +++ b/native/rust/signing/core/src/context.rs @@ -8,9 +8,11 @@ use cose_sign1_primitives::SizedRead; /// Payload to be signed. /// /// Maps V2 payload handling in `ISigningService`. -pub enum SigningPayload { +pub enum SigningPayload<'a> { /// In-memory payload bytes. Bytes(Vec), + /// Borrowed payload bytes (zero-copy from caller). + Borrowed(&'a [u8]), /// Streaming payload with known length. Stream(Box), } @@ -18,16 +20,16 @@ pub enum SigningPayload { /// Context for a signing operation. /// /// Maps V2 signing context passed to `ISigningService.GetSignerAsync()`. -pub struct SigningContext { +pub struct SigningContext<'a> { /// The payload to be signed. - pub payload: SigningPayload, + pub payload: SigningPayload<'a>, /// Content type of the payload (COSE header 3). pub content_type: Option, /// Additional header contributors for this signing operation. pub additional_header_contributors: Vec>, } -impl SigningContext { +impl<'a> SigningContext<'a> { /// Creates a signing context from in-memory bytes. pub fn from_bytes(payload: Vec) -> Self { Self { @@ -37,6 +39,15 @@ impl SigningContext { } } + /// Creates a signing context from a borrowed byte slice (zero-copy). + pub fn from_slice(payload: &'a [u8]) -> Self { + Self { + payload: SigningPayload::Borrowed(payload), + content_type: None, + additional_header_contributors: Vec::new(), + } + } + /// Creates a signing context from a streaming payload. pub fn from_stream(stream: Box) -> Self { Self { @@ -52,6 +63,7 @@ impl SigningContext { pub fn payload_bytes(&self) -> Option<&[u8]> { match &self.payload { SigningPayload::Bytes(b) => Some(b), + SigningPayload::Borrowed(b) => Some(b), SigningPayload::Stream(_) => None, } } diff --git a/native/rust/signing/core/src/signer.rs b/native/rust/signing/core/src/signer.rs index a99cb947..e181e790 100644 --- a/native/rust/signing/core/src/signer.rs +++ b/native/rust/signing/core/src/signer.rs @@ -28,14 +28,14 @@ pub enum HeaderMergeStrategy { /// Provides access to signing context and key metadata during header contribution. pub struct HeaderContributorContext<'a> { /// Reference to the signing context. - pub signing_context: &'a SigningContext, + pub signing_context: &'a SigningContext<'a>, /// Reference to the signing key. pub signing_key: &'a dyn CryptoSigner, } impl<'a> HeaderContributorContext<'a> { /// Creates a new header contributor context. - pub fn new(signing_context: &'a SigningContext, signing_key: &'a dyn CryptoSigner) -> Self { + pub fn new(signing_context: &'a SigningContext<'a>, signing_key: &'a dyn CryptoSigner) -> Self { Self { signing_context, signing_key, diff --git a/native/rust/signing/core/src/traits.rs b/native/rust/signing/core/src/traits.rs index 720475b4..b223dd7f 100644 --- a/native/rust/signing/core/src/traits.rs +++ b/native/rust/signing/core/src/traits.rs @@ -18,7 +18,7 @@ pub trait SigningService: Send + Sync { /// Gets a signer for the given signing context. /// /// Maps V2 `GetSignerAsync()`. - fn get_cose_signer(&self, context: &SigningContext) -> Result; + fn get_cose_signer(&self, context: &SigningContext<'_>) -> Result; /// Returns whether this is a remote signing service. fn is_remote(&self) -> bool; @@ -37,7 +37,7 @@ pub trait SigningService: Send + Sync { fn verify_signature( &self, message_bytes: &[u8], - context: &SigningContext, + context: &SigningContext<'_>, ) -> Result; } diff --git a/native/rust/signing/core/tests/context_tests.rs b/native/rust/signing/core/tests/context_tests.rs index cd7924e3..8f3434dd 100644 --- a/native/rust/signing/core/tests/context_tests.rs +++ b/native/rust/signing/core/tests/context_tests.rs @@ -37,9 +37,19 @@ fn test_signing_payload_bytes() { match payload_enum { SigningPayload::Bytes(ref b) => assert_eq!(b, &payload), SigningPayload::Stream(_) => panic!("Expected Bytes variant"), + SigningPayload::Borrowed(_) => panic!("Expected Bytes variant"), } } +#[test] +fn test_signing_payload_borrowed() { + let data = vec![4, 5, 6]; + let context = SigningContext::from_slice(&data); + + assert_eq!(context.payload_bytes(), Some(data.as_slice())); + assert!(!context.has_stream()); +} + #[test] fn test_context_payload_bytes_returns_none_for_stream() { use cose_sign1_primitives::SizedReader; diff --git a/native/rust/signing/factories/src/direct/factory.rs b/native/rust/signing/factories/src/direct/factory.rs index a656720b..41462c42 100644 --- a/native/rust/signing/factories/src/direct/factory.rs +++ b/native/rust/signing/factories/src/direct/factory.rs @@ -71,8 +71,8 @@ impl DirectSignatureFactory { info!(method = "sign_direct", payload_len = payload.len(), content_type = %content_type, "Signing payload"); let options = options.unwrap_or_default(); - // Create signing context (payload copy required by SigningContext ownership model) - let mut context = SigningContext::from_bytes(payload.to_vec()); + // Create signing context (zero-copy borrow of caller's payload) + let mut context = SigningContext::from_slice(payload); context.content_type = Some(content_type.to_string()); // Add content type contributor (always first) diff --git a/native/rust/signing/headers/src/cwt_claims_contributor.rs b/native/rust/signing/headers/src/cwt_claims_contributor.rs index f5732430..94c5162b 100644 --- a/native/rust/signing/headers/src/cwt_claims_contributor.rs +++ b/native/rust/signing/headers/src/cwt_claims_contributor.rs @@ -5,7 +5,7 @@ //! //! Maps V2 `CWTClaimsHeaderExtender` class (note: different name in V2). -use cose_sign1_primitives::{CoseHeaderMap, CoseHeaderValue}; +use cose_sign1_primitives::{ArcSlice, CoseHeaderMap, CoseHeaderValue}; use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, HeaderMergeStrategy}; use crate::cwt_claims::CwtClaims; @@ -16,7 +16,7 @@ use crate::cwt_claims::CwtClaims; /// Always adds to PROTECTED headers (label 15) for SCITT compliance. #[derive(Debug)] pub struct CwtClaimsHeaderContributor { - claims_bytes: Vec, + claims_bytes: ArcSlice, } impl CwtClaimsHeaderContributor { @@ -27,9 +27,10 @@ impl CwtClaimsHeaderContributor { /// * `claims` - The CWT claims /// * `provider` - CBOR provider for encoding claims pub fn new(claims: &CwtClaims) -> Result { - let claims_bytes = claims + let claims_bytes: ArcSlice = claims .to_cbor_bytes() - .map_err(|e| format!("Failed to encode CWT claims: {}", e))?; + .map_err(|e| format!("Failed to encode CWT claims: {}", e))? + .into(); Ok(Self { claims_bytes }) } @@ -49,7 +50,7 @@ impl HeaderContributor for CwtClaimsHeaderContributor { ) { headers.insert( cose_sign1_primitives::CoseHeaderLabel::Int(Self::CWT_CLAIMS_LABEL), - CoseHeaderValue::Bytes(self.claims_bytes.clone().into()), + CoseHeaderValue::Bytes(self.claims_bytes.clone()), ); } diff --git a/native/rust/signing/headers/src/cwt_claims_header_contributor.rs b/native/rust/signing/headers/src/cwt_claims_header_contributor.rs index 6e9a3e6f..d7648f1f 100644 --- a/native/rust/signing/headers/src/cwt_claims_header_contributor.rs +++ b/native/rust/signing/headers/src/cwt_claims_header_contributor.rs @@ -5,7 +5,7 @@ //! //! Maps V2 `CWTClaimsHeaderExtender` class (note: different name in V2). -use cose_sign1_primitives::{CoseHeaderMap, CoseHeaderValue}; +use cose_sign1_primitives::{ArcSlice, CoseHeaderMap, CoseHeaderValue}; use cose_sign1_signing::{HeaderContributor, HeaderContributorContext, HeaderMergeStrategy}; use crate::cwt_claims::CwtClaims; @@ -16,7 +16,7 @@ use crate::cwt_claims::CwtClaims; /// Always adds to PROTECTED headers (label 15) for SCITT compliance. #[derive(Debug)] pub struct CwtClaimsHeaderContributor { - claims_bytes: Vec, + claims_bytes: ArcSlice, } impl CwtClaimsHeaderContributor { @@ -27,8 +27,9 @@ impl CwtClaimsHeaderContributor { /// * `claims` - The CWT claims /// * `provider` - CBOR provider for encoding claims pub fn new(claims: &CwtClaims) -> Result { - let claims_bytes = claims.to_cbor_bytes() - .map_err(|e| format!("Failed to encode CWT claims: {}", e))?; + let claims_bytes: ArcSlice = claims.to_cbor_bytes() + .map_err(|e| format!("Failed to encode CWT claims: {}", e))? + .into(); Ok(Self { claims_bytes }) } From 30a27f171366adb755f1da3decc4c21c5d172061 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Mon, 6 Apr 2026 16:28:54 -0700 Subject: [PATCH 4/8] World-class zero-allocation optimizations: AKV stack digests, certificates static key usage strings, CWT Arc claims, MST fact Arc/Cow, DID Cow policies, factory ArcStr fields, CounterSignature Cow details, validation message_arc() zero-copy - AKV: Inline digest computation, eliminate hash_sig_structure method and 3x .to_vec() - Certificates: Key usage Vec -> Vec<&'static str> for 10 static strings - CWT: Fact fields String -> Arc, use cose_sign1_message_arc() for zero-copy - MST: sha256_hex->Arc, coverage->&'static str, details->Option> - DID: DidX509Policy::Eku uses Cow::Borrowed for OID string literals - Factory: Hash envelope contributor fields -> ArcStr, stack GenericArray digests - Validation: CounterSignatureEnvelopeIntegrityFact.details -> Cow<'static, str> - All 7,886 tests pass, clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- native/rust/did/x509/ffi/src/lib.rs | 2 +- .../x509/ffi/tests/additional_ffi_coverage.rs | 13 +- .../did/x509/ffi/tests/ffi_rsa_coverage.rs | 1937 ++++++------ .../ffi/tests/resolve_validate_coverage.rs | 613 ++-- native/rust/did/x509/src/did_document.rs | 3 +- .../did/x509/src/models/parsed_identifier.rs | 3 +- native/rust/did/x509/src/models/policy.rs | 3 +- native/rust/did/x509/src/parsing/parser.rs | 3 +- native/rust/did/x509/src/policy_validators.rs | 3 +- native/rust/did/x509/src/resolver.rs | 25 +- native/rust/did/x509/src/x509_extensions.rs | 19 +- .../x509/tests/additional_coverage_tests.rs | 699 ++--- native/rust/did/x509/tests/builder_tests.rs | 709 ++--- .../rust/did/x509/tests/did_document_tests.rs | 171 +- .../did/x509/tests/policy_validator_tests.rs | 750 ++--- .../x509/tests/policy_validators_coverage.rs | 738 ++--- .../rust/did/x509/tests/resolver_coverage.rs | 353 +-- .../did/x509/tests/resolver_rsa_coverage.rs | 525 ++-- .../did/x509/tests/surgical_did_coverage.rs | 2789 +++++++++-------- .../did/x509/tests/targeted_95_coverage.rs | 557 ++-- .../did/x509/tests/validator_comprehensive.rs | 749 ++--- .../did/x509/tests/x509_extensions_rcgen.rs | 473 +-- .../did/x509/tests/x509_extensions_tests.rs | 303 +- .../src/signing/did_x509_helper.rs | 2 +- .../mst/src/validation/facts.rs | 19 +- .../mst/src/validation/pack.rs | 744 ++--- .../mst/src/validation/receipt_verify.rs | 1468 ++++----- .../mst/tests/facts_properties.rs | 9 +- .../mst/tests/final_targeted_mst_coverage.rs | 3 +- .../mst/tests/fluent_ext_coverage.rs | 10 +- .../mst/tests/receipt_verify_coverage.rs | 13 +- .../mst/tests/receipt_verify_extended.rs | 4 +- .../mst/tests/receipt_verify_helpers.rs | 5 +- .../primitives/crypto/openssl/ffi/src/lib.rs | 10 +- .../crypto/openssl/src/jwk_verifier.rs | 4 +- .../openssl/tests/jwk_verifier_tests.rs | 754 ++--- native/rust/primitives/crypto/src/jwk.rs | 21 +- .../crypto/tests/comprehensive_trait_tests.rs | 848 ++--- .../core/src/message_fact_producer.rs | 102 +- .../rust/validation/core/src/message_facts.rs | 18 +- .../core/tests/final_targeted_coverage.rs | 6 +- .../core/tests/message_fact_coverage.rs | 14 +- .../message_fact_producer_counter_sig.rs | 14 +- .../tests/message_fact_producer_raw_cwt.rs | 4 +- .../tests/message_facts_claim_properties.rs | 12 +- .../core/tests/message_facts_more_coverage.rs | 7 +- .../core/tests/message_facts_properties.rs | 8 +- .../core/tests/message_fluent_ext_more.rs | 6 +- .../core/tests/message_parts_accessors.rs | 2 +- .../core/tests/targeted_coverage_gaps.rs | 21 +- .../core/tests/validator_deep_coverage.rs | 13 +- .../rust/validation/primitives/src/facts.rs | 6 + 52 files changed, 7821 insertions(+), 7766 deletions(-) diff --git a/native/rust/did/x509/ffi/src/lib.rs b/native/rust/did/x509/ffi/src/lib.rs index c48afacf..4c954c57 100644 --- a/native/rust/did/x509/ffi/src/lib.rs +++ b/native/rust/did/x509/ffi/src/lib.rs @@ -381,7 +381,7 @@ pub fn impl_build_with_eku_inner( } let c_str = unsafe { std::ffi::CStr::from_ptr(oid_ptr) }; match c_str.to_str() { - Ok(s) => oids.push(s.to_string()), + Ok(s) => oids.push(std::borrow::Cow::Owned(s.to_string())), Err(_) => { set_error( out_error, diff --git a/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs b/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs index 108d31cd..97d97d44 100644 --- a/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs +++ b/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs @@ -5,6 +5,7 @@ //! //! These tests focus on uncovered paths in the FFI layer. +use std::borrow::Cow; use did_x509::builder::DidX509Builder; use did_x509::models::policy::DidX509Policy; use did_x509_ffi::*; @@ -116,7 +117,7 @@ fn test_parse_null_out_handle() { #[test] fn test_parse_valid_did() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); @@ -227,7 +228,7 @@ fn test_validate_null_chain() { #[test] fn test_validate_null_out_valid() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); @@ -296,7 +297,7 @@ fn test_resolve_null_did() { #[test] fn test_resolve_null_out_json() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); @@ -474,7 +475,7 @@ fn test_error_free_null() { #[test] fn test_parsed_get_fingerprint() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); @@ -514,7 +515,7 @@ fn test_parsed_get_fingerprint() { #[test] fn test_parsed_get_hash_algorithm() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); @@ -553,7 +554,7 @@ fn test_parsed_get_hash_algorithm() { #[test] fn test_parsed_get_policy_count() { let cert_der = generate_test_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); let did_cstring = CString::new(did_string.as_str()).unwrap(); diff --git a/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs b/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs index dca2b420..1e2647ca 100644 --- a/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs +++ b/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs @@ -1,968 +1,969 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional FFI coverage tests to improve coverage on resolve, validate, and build paths. - -use did_x509::builder::DidX509Builder; -use did_x509::models::policy::DidX509Policy; -use did_x509_ffi::*; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::hash::MessageDigest; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::{X509Builder, X509NameBuilder}; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; -use std::ffi::{CStr, CString}; -use std::ptr; - -/// Helper to get error message from an error handle. -fn error_message(err: *const DidX509ErrorHandle) -> Option { - if err.is_null() { - return None; - } - let msg = unsafe { did_x509_error_message(err) }; - if msg.is_null() { - return None; - } - let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); - unsafe { did_x509_string_free(msg) }; - Some(s) -} - -/// Generate an RSA certificate using openssl. -fn generate_rsa_cert() -> Vec { - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - - let serial = BigNum::from_u32(1).unwrap(); - builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "RSA Test Certificate") - .unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - - let eku = openssl::x509::extension::ExtendedKeyUsage::new() - .code_signing() - .build() - .unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Generate an EC certificate using rcgen. -fn generate_ec_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "EC Test Certificate"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -#[test] -fn test_ffi_resolve_rsa_certificate() { - let cert_der = generate_rsa_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_json.is_null()); - - // Verify RSA key type in result - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - assert!(json_str.contains("RSA"), "Should contain RSA key type"); - - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_rsa_certificate() { - let cert_der = generate_rsa_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert_eq!(is_valid, 1, "RSA certificate should be valid"); - - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_ec_certificate() { - let cert_der = generate_ec_cert(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_build_from_chain( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_did.is_null()); - - let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); - assert!( - did_str.starts_with("did:x509:"), - "Should be a valid DID:x509" - ); - - unsafe { - did_x509_string_free(result_did); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_ec_certificate() { - let cert_der = generate_ec_cert(); - - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_build_with_eku( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_did.is_null()); - - let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); - assert!( - did_str.starts_with("did:x509:"), - "Should be a valid DID:x509" - ); - assert!(did_str.contains("eku"), "Should contain EKU policy"); - - unsafe { - did_x509_string_free(result_did); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parse_and_get_fields() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Parse - let status = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - assert_eq!(status, DID_X509_OK, "Parse should succeed"); - assert!(!handle.is_null()); - - // Get fingerprint - let mut fingerprint: *mut libc::c_char = ptr::null_mut(); - let status = impl_parsed_get_fingerprint_inner(handle, &mut fingerprint, &mut error); - assert_eq!(status, DID_X509_OK, "Get fingerprint should succeed"); - assert!(!fingerprint.is_null()); - - let fp_str = unsafe { CStr::from_ptr(fingerprint) }.to_str().unwrap(); - assert_eq!( - fp_str.len(), - 64, - "SHA256 fingerprint should be 64 hex chars" - ); - - // Get hash algorithm - let mut algorithm: *mut libc::c_char = ptr::null_mut(); - let status = impl_parsed_get_hash_algorithm_inner(handle, &mut algorithm, &mut error); - assert_eq!(status, DID_X509_OK, "Get algorithm should succeed"); - assert!(!algorithm.is_null()); - - let alg_str = unsafe { CStr::from_ptr(algorithm) }.to_str().unwrap(); - assert_eq!(alg_str, "sha256", "Should be sha256"); - - // Get policy count - let mut count: u32 = 0; - let status = impl_parsed_get_policy_count_inner(handle, &mut count); - assert_eq!(status, DID_X509_OK, "Get policy count should succeed"); - assert_eq!(count, 1, "Should have 1 policy"); - - // Clean up - unsafe { - did_x509_string_free(fingerprint); - did_x509_string_free(algorithm); - did_x509_parsed_free(handle); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_ec_verify_document_structure() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - assert_eq!(status, DID_X509_OK); - assert!(!result_json.is_null()); - - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - - // Verify EC key in result - assert!(json_str.contains("EC"), "Should contain EC key type"); - assert!(json_str.contains("P-256"), "Should contain P-256 curve"); - assert!( - json_str.contains("JsonWebKey2020"), - "Should contain JsonWebKey2020" - ); - - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_error_code_accessor() { - // Create an error by passing invalid arguments - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Parse with null string should create an error - let status = impl_parse_inner(ptr::null(), &mut handle, &mut error); - - assert_ne!(status, DID_X509_OK); - assert!(!error.is_null()); - - // Test error code accessor - let code = unsafe { did_x509_error_code(error) }; - assert!(code != 0, "Error code should be non-zero"); - - // Clean up - unsafe { - did_x509_error_free(error); - } -} - -#[test] -fn test_ffi_build_with_eku_null_output_pointer() { - let cert_der = generate_ec_cert(); - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null for out_did_string - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - ptr::null_mut(), - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_cert() { - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null cert with non-zero len - let status = impl_build_with_eku_inner( - ptr::null(), - 10, // non-zero length but null pointer - eku_oids.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_oids() { - let cert_der = generate_ec_cert(); - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null eku_oids with non-zero count - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - ptr::null(), - 1, // non-zero count but null pointer - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_oid_entry() { - let cert_der = generate_ec_cert(); - let eku_oids: [*const libc::c_char; 1] = [ptr::null()]; // Null entry - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_output() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_certs() { - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - ptr::null(), // null certs - ptr::null(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_zero_count() { - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - - let status = impl_build_from_chain_inner( - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_cert_entry() { - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; // non-zero len but null pointer - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_is_valid() { - let cert_der = generate_ec_cert(); - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null out_is_valid - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_did() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - ptr::null(), // null DID - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_chain() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - ptr::null(), // null chain - ptr::null(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_zero_chain_count() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_chain_entry() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_output() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_did() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - ptr::null(), // null DID - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_chain() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - ptr::null(), // null chain - ptr::null(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_zero_chain_count() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_chain_entry() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_fingerprint_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - // Test null output - let status = impl_parsed_get_fingerprint_inner(handle, ptr::null_mut(), &mut error); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_fingerprint_null_handle() { - let mut fingerprint: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parsed_get_fingerprint_inner( - ptr::null(), // null handle - &mut fingerprint, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_algorithm_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - let status = impl_parsed_get_hash_algorithm_inner( - handle, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_algorithm_null_handle() { - let mut algorithm: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parsed_get_hash_algorithm_inner( - ptr::null(), // null handle - &mut algorithm, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_policy_count_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - let status = impl_parsed_get_policy_count_inner( - handle, - ptr::null_mut(), // null output - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_policy_count_null_handle() { - let mut count: u32 = 0; - - let status = impl_parsed_get_policy_count_inner( - ptr::null(), // null handle - &mut count, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); -} - -#[test] -fn test_ffi_parse_null_output_handle() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parse_inner( - did_cstring.as_ptr(), - ptr::null_mut(), // null output handle - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional FFI coverage tests to improve coverage on resolve, validate, and build paths. + +use std::borrow::Cow; +use did_x509::builder::DidX509Builder; +use did_x509::models::policy::DidX509Policy; +use did_x509_ffi::*; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::{X509Builder, X509NameBuilder}; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; +use std::ffi::{CStr, CString}; +use std::ptr; + +/// Helper to get error message from an error handle. +fn error_message(err: *const DidX509ErrorHandle) -> Option { + if err.is_null() { + return None; + } + let msg = unsafe { did_x509_error_message(err) }; + if msg.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); + unsafe { did_x509_string_free(msg) }; + Some(s) +} + +/// Generate an RSA certificate using openssl. +fn generate_rsa_cert() -> Vec { + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + + let serial = BigNum::from_u32(1).unwrap(); + builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "RSA Test Certificate") + .unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + + let eku = openssl::x509::extension::ExtendedKeyUsage::new() + .code_signing() + .build() + .unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Generate an EC certificate using rcgen. +fn generate_ec_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "EC Test Certificate"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +#[test] +fn test_ffi_resolve_rsa_certificate() { + let cert_der = generate_rsa_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_json.is_null()); + + // Verify RSA key type in result + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + assert!(json_str.contains("RSA"), "Should contain RSA key type"); + + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_rsa_certificate() { + let cert_der = generate_rsa_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert_eq!(is_valid, 1, "RSA certificate should be valid"); + + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_ec_certificate() { + let cert_der = generate_ec_cert(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_build_from_chain( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_did.is_null()); + + let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); + assert!( + did_str.starts_with("did:x509:"), + "Should be a valid DID:x509" + ); + + unsafe { + did_x509_string_free(result_did); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_ec_certificate() { + let cert_der = generate_ec_cert(); + + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_build_with_eku( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_did.is_null()); + + let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); + assert!( + did_str.starts_with("did:x509:"), + "Should be a valid DID:x509" + ); + assert!(did_str.contains("eku"), "Should contain EKU policy"); + + unsafe { + did_x509_string_free(result_did); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parse_and_get_fields() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Parse + let status = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + assert_eq!(status, DID_X509_OK, "Parse should succeed"); + assert!(!handle.is_null()); + + // Get fingerprint + let mut fingerprint: *mut libc::c_char = ptr::null_mut(); + let status = impl_parsed_get_fingerprint_inner(handle, &mut fingerprint, &mut error); + assert_eq!(status, DID_X509_OK, "Get fingerprint should succeed"); + assert!(!fingerprint.is_null()); + + let fp_str = unsafe { CStr::from_ptr(fingerprint) }.to_str().unwrap(); + assert_eq!( + fp_str.len(), + 64, + "SHA256 fingerprint should be 64 hex chars" + ); + + // Get hash algorithm + let mut algorithm: *mut libc::c_char = ptr::null_mut(); + let status = impl_parsed_get_hash_algorithm_inner(handle, &mut algorithm, &mut error); + assert_eq!(status, DID_X509_OK, "Get algorithm should succeed"); + assert!(!algorithm.is_null()); + + let alg_str = unsafe { CStr::from_ptr(algorithm) }.to_str().unwrap(); + assert_eq!(alg_str, "sha256", "Should be sha256"); + + // Get policy count + let mut count: u32 = 0; + let status = impl_parsed_get_policy_count_inner(handle, &mut count); + assert_eq!(status, DID_X509_OK, "Get policy count should succeed"); + assert_eq!(count, 1, "Should have 1 policy"); + + // Clean up + unsafe { + did_x509_string_free(fingerprint); + did_x509_string_free(algorithm); + did_x509_parsed_free(handle); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_ec_verify_document_structure() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + assert_eq!(status, DID_X509_OK); + assert!(!result_json.is_null()); + + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + + // Verify EC key in result + assert!(json_str.contains("EC"), "Should contain EC key type"); + assert!(json_str.contains("P-256"), "Should contain P-256 curve"); + assert!( + json_str.contains("JsonWebKey2020"), + "Should contain JsonWebKey2020" + ); + + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_error_code_accessor() { + // Create an error by passing invalid arguments + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Parse with null string should create an error + let status = impl_parse_inner(ptr::null(), &mut handle, &mut error); + + assert_ne!(status, DID_X509_OK); + assert!(!error.is_null()); + + // Test error code accessor + let code = unsafe { did_x509_error_code(error) }; + assert!(code != 0, "Error code should be non-zero"); + + // Clean up + unsafe { + did_x509_error_free(error); + } +} + +#[test] +fn test_ffi_build_with_eku_null_output_pointer() { + let cert_der = generate_ec_cert(); + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null for out_did_string + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + ptr::null_mut(), + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_cert() { + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null cert with non-zero len + let status = impl_build_with_eku_inner( + ptr::null(), + 10, // non-zero length but null pointer + eku_oids.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_oids() { + let cert_der = generate_ec_cert(); + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null eku_oids with non-zero count + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + ptr::null(), + 1, // non-zero count but null pointer + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_oid_entry() { + let cert_der = generate_ec_cert(); + let eku_oids: [*const libc::c_char; 1] = [ptr::null()]; // Null entry + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_output() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_certs() { + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + ptr::null(), // null certs + ptr::null(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_zero_count() { + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + + let status = impl_build_from_chain_inner( + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_cert_entry() { + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; // non-zero len but null pointer + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_is_valid() { + let cert_der = generate_ec_cert(); + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null out_is_valid + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_did() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + ptr::null(), // null DID + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_chain() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + ptr::null(), // null chain + ptr::null(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_zero_chain_count() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_chain_entry() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_output() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_did() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + ptr::null(), // null DID + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_chain() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + ptr::null(), // null chain + ptr::null(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_zero_chain_count() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_chain_entry() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_fingerprint_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + // Test null output + let status = impl_parsed_get_fingerprint_inner(handle, ptr::null_mut(), &mut error); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_fingerprint_null_handle() { + let mut fingerprint: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parsed_get_fingerprint_inner( + ptr::null(), // null handle + &mut fingerprint, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_algorithm_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + let status = impl_parsed_get_hash_algorithm_inner( + handle, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_algorithm_null_handle() { + let mut algorithm: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parsed_get_hash_algorithm_inner( + ptr::null(), // null handle + &mut algorithm, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_policy_count_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + let status = impl_parsed_get_policy_count_inner( + handle, + ptr::null_mut(), // null output + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_policy_count_null_handle() { + let mut count: u32 = 0; + + let status = impl_parsed_get_policy_count_inner( + ptr::null(), // null handle + &mut count, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); +} + +#[test] +fn test_ffi_parse_null_output_handle() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parse_inner( + did_cstring.as_ptr(), + ptr::null_mut(), // null output handle + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} \ No newline at end of file diff --git a/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs b/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs index 0ca46755..2a3a7ea7 100644 --- a/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs +++ b/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs @@ -1,306 +1,307 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive coverage tests for DID:x509 FFI resolve, validate, and build functions. -//! -//! These tests target uncovered paths in impl_*_inner functions to achieve full coverage. - -use did_x509::builder::DidX509Builder; -use did_x509::models::policy::DidX509Policy; -use did_x509_ffi::*; -use rcgen::string::Ia5String; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; -use serde_json::Value; -use std::ffi::{CStr, CString}; -use std::ptr; - -/// Helper to get error message from an error handle. -fn error_message(err: *const DidX509ErrorHandle) -> Option { - if err.is_null() { - return None; - } - let msg = unsafe { did_x509_error_message(err) }; - if msg.is_null() { - return None; - } - let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); - unsafe { did_x509_string_free(msg) }; - Some(s) -} - -/// Generate a self-signed X.509 certificate with code signing EKU using rcgen. -fn generate_code_signing_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Certificate"); - - // Add Extended Key Usage for Code Signing - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - // Add Subject Alternative Name - params.subject_alt_names = vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate invalid certificate data (garbage bytes). -fn generate_invalid_cert() -> Vec { - vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER -} - -#[test] -fn test_resolve_inner_happy_path() { - // Generate a valid certificate and build proper DID - let cert_der = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the resolve function - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - // Verify success - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_json.is_null()); - - // Parse the JSON result - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - let doc: Value = serde_json::from_str(json_str).unwrap(); - - // Verify the DID document structure - assert_eq!(doc["id"], did_string); - assert!(doc["verificationMethod"].is_array()); - assert_eq!(doc["verificationMethod"][0]["type"], "JsonWebKey2020"); - - // Clean up - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_resolve_inner_invalid_did() { - // Generate a valid certificate - let cert_der = generate_code_signing_cert(); - - // Use an invalid DID string (completely malformed) - let invalid_did = CString::new("not-a-did-at-all").unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the resolve function - let status = unsafe { - did_x509_resolve( - invalid_did.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - // Verify failure - assert_ne!(status, DID_X509_OK); - assert!(result_json.is_null()); - assert!(!error.is_null()); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("must start with 'did:x509'"), - "Error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_validate_inner_matching_chain() { - // Generate a valid certificate and build proper DID - let cert_der = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the validate function - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - // Verify success and validity - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert_eq!(is_valid, 1, "Certificate should be valid for the DID"); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_validate_inner_wrong_chain() { - // Generate one certificate - let cert_der1 = generate_code_signing_cert(); - - // Calculate fingerprint for a different certificate - let cert_der2 = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der2, &[policy]).expect("Should build DID"); - - // Build DID for cert2 but validate against cert1 - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain with cert1 (doesn't match DID fingerprint) - let cert_ptr = cert_der1.as_ptr(); - let cert_len = cert_der1.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = -1; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the validate function - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - // Verify the operation should fail because the fingerprint doesn't match - assert_ne!(status, DID_X509_OK); - assert_ne!( - is_valid, 1, - "Certificate should not be valid for the mismatched DID" - ); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("fingerprint"), - "Should be a fingerprint mismatch error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_build_from_chain_invalid_cert() { - // Use invalid certificate data (garbage bytes) - let invalid_cert = generate_invalid_cert(); - - // Prepare certificate chain with invalid cert - let cert_ptr = invalid_cert.as_ptr(); - let cert_len = invalid_cert.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the build_from_chain function - let status = unsafe { - did_x509_build_from_chain( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - // Verify failure - assert_ne!(status, DID_X509_OK); - assert!(result_did.is_null()); - assert!(!error.is_null()); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("parse") || err_msg.contains("build") || err_msg.contains("invalid"), - "Error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive coverage tests for DID:x509 FFI resolve, validate, and build functions. +//! +//! These tests target uncovered paths in impl_*_inner functions to achieve full coverage. + +use std::borrow::Cow; +use did_x509::builder::DidX509Builder; +use did_x509::models::policy::DidX509Policy; +use did_x509_ffi::*; +use rcgen::string::Ia5String; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; +use serde_json::Value; +use std::ffi::{CStr, CString}; +use std::ptr; + +/// Helper to get error message from an error handle. +fn error_message(err: *const DidX509ErrorHandle) -> Option { + if err.is_null() { + return None; + } + let msg = unsafe { did_x509_error_message(err) }; + if msg.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); + unsafe { did_x509_string_free(msg) }; + Some(s) +} + +/// Generate a self-signed X.509 certificate with code signing EKU using rcgen. +fn generate_code_signing_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Certificate"); + + // Add Extended Key Usage for Code Signing + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + // Add Subject Alternative Name + params.subject_alt_names = vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate invalid certificate data (garbage bytes). +fn generate_invalid_cert() -> Vec { + vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER +} + +#[test] +fn test_resolve_inner_happy_path() { + // Generate a valid certificate and build proper DID + let cert_der = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the resolve function + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + // Verify success + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_json.is_null()); + + // Parse the JSON result + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + let doc: Value = serde_json::from_str(json_str).unwrap(); + + // Verify the DID document structure + assert_eq!(doc["id"], did_string); + assert!(doc["verificationMethod"].is_array()); + assert_eq!(doc["verificationMethod"][0]["type"], "JsonWebKey2020"); + + // Clean up + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_resolve_inner_invalid_did() { + // Generate a valid certificate + let cert_der = generate_code_signing_cert(); + + // Use an invalid DID string (completely malformed) + let invalid_did = CString::new("not-a-did-at-all").unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the resolve function + let status = unsafe { + did_x509_resolve( + invalid_did.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + // Verify failure + assert_ne!(status, DID_X509_OK); + assert!(result_json.is_null()); + assert!(!error.is_null()); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("must start with 'did:x509'"), + "Error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_validate_inner_matching_chain() { + // Generate a valid certificate and build proper DID + let cert_der = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the validate function + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + // Verify success and validity + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert_eq!(is_valid, 1, "Certificate should be valid for the DID"); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_validate_inner_wrong_chain() { + // Generate one certificate + let cert_der1 = generate_code_signing_cert(); + + // Calculate fingerprint for a different certificate + let cert_der2 = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der2, &[policy]).expect("Should build DID"); + + // Build DID for cert2 but validate against cert1 + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain with cert1 (doesn't match DID fingerprint) + let cert_ptr = cert_der1.as_ptr(); + let cert_len = cert_der1.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = -1; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the validate function + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + // Verify the operation should fail because the fingerprint doesn't match + assert_ne!(status, DID_X509_OK); + assert_ne!( + is_valid, 1, + "Certificate should not be valid for the mismatched DID" + ); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("fingerprint"), + "Should be a fingerprint mismatch error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_build_from_chain_invalid_cert() { + // Use invalid certificate data (garbage bytes) + let invalid_cert = generate_invalid_cert(); + + // Prepare certificate chain with invalid cert + let cert_ptr = invalid_cert.as_ptr(); + let cert_len = invalid_cert.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the build_from_chain function + let status = unsafe { + did_x509_build_from_chain( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + // Verify failure + assert_ne!(status, DID_X509_OK); + assert!(result_did.is_null()); + assert!(!error.is_null()); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("parse") || err_msg.contains("build") || err_msg.contains("invalid"), + "Error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} \ No newline at end of file diff --git a/native/rust/did/x509/src/did_document.rs b/native/rust/did/x509/src/did_document.rs index 12474989..25443403 100644 --- a/native/rust/did/x509/src/did_document.rs +++ b/native/rust/did/x509/src/did_document.rs @@ -3,6 +3,7 @@ use crate::error::DidX509Error; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::collections::HashMap; /// W3C DID Document according to DID Core specification @@ -39,7 +40,7 @@ pub struct VerificationMethod { /// Public key in JWK format #[serde(rename = "publicKeyJwk")] - pub public_key_jwk: HashMap, + pub public_key_jwk: HashMap, String>, } impl DidDocument { diff --git a/native/rust/did/x509/src/models/parsed_identifier.rs b/native/rust/did/x509/src/models/parsed_identifier.rs index 7c8a5e1e..5a560bba 100644 --- a/native/rust/did/x509/src/models/parsed_identifier.rs +++ b/native/rust/did/x509/src/models/parsed_identifier.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::models::DidX509Policy; +use std::borrow::Cow; /// A parsed DID:x509 identifier with all its components #[derive(Debug, Clone, PartialEq)] @@ -64,7 +65,7 @@ impl DidX509ParsedIdentifier { } /// Get the EKU policy if it exists - pub fn get_eku_policy(&self) -> Option<&Vec> { + pub fn get_eku_policy(&self) -> Option<&Vec>> { self.policies.iter().find_map(|p| { if let DidX509Policy::Eku(oids) = p { Some(oids) diff --git a/native/rust/did/x509/src/models/policy.rs b/native/rust/did/x509/src/models/policy.rs index d02e6b1d..6600bbf3 100644 --- a/native/rust/did/x509/src/models/policy.rs +++ b/native/rust/did/x509/src/models/policy.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::constants::{SAN_TYPE_DN, SAN_TYPE_DNS, SAN_TYPE_EMAIL, SAN_TYPE_URI}; +use std::borrow::Cow; /// Type of Subject Alternative Name #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -44,7 +45,7 @@ impl SanType { #[derive(Debug, Clone, PartialEq)] pub enum DidX509Policy { /// Extended Key Usage policy with list of OIDs - Eku(Vec), + Eku(Vec>), /// Subject Distinguished Name policy with key-value pairs /// Each tuple is (attribute_label, value), e.g., ("CN", "example.com") diff --git a/native/rust/did/x509/src/parsing/parser.rs b/native/rust/did/x509/src/parsing/parser.rs index fdd0eb7f..acb999e8 100644 --- a/native/rust/did/x509/src/parsing/parser.rs +++ b/native/rust/did/x509/src/parsing/parser.rs @@ -5,6 +5,7 @@ use crate::constants::*; use crate::error::DidX509Error; use crate::models::{DidX509ParsedIdentifier, DidX509Policy, SanType}; use crate::parsing::percent_encoding::percent_decode; +use std::borrow::Cow; /// Encode bytes as lowercase hex string. fn hex_encode(bytes: &[u8]) -> String { @@ -272,7 +273,7 @@ fn parse_eku_policy(value: &str) -> Result { if !is_valid_oid(oid) { return Err(DidX509Error::InvalidEkuOid); } - valid_oids.push(oid.into()); + valid_oids.push(Cow::Owned(oid.to_string())); } Ok(DidX509Policy::Eku(valid_oids)) diff --git a/native/rust/did/x509/src/policy_validators.rs b/native/rust/did/x509/src/policy_validators.rs index 4ae59f9c..e69d848b 100644 --- a/native/rust/did/x509/src/policy_validators.rs +++ b/native/rust/did/x509/src/policy_validators.rs @@ -6,10 +6,11 @@ use crate::error::DidX509Error; use crate::models::SanType; use crate::san_parser; use crate::x509_extensions; +use std::borrow::Cow; use x509_parser::prelude::*; /// Validate Extended Key Usage (EKU) policy -pub fn validate_eku(cert: &X509Certificate, expected_oids: &[String]) -> Result<(), DidX509Error> { +pub fn validate_eku(cert: &X509Certificate, expected_oids: &[Cow<'static, str>]) -> Result<(), DidX509Error> { let ekus = x509_extensions::extract_extended_key_usage(cert); if ekus.is_empty() { diff --git a/native/rust/did/x509/src/resolver.rs b/native/rust/did/x509/src/resolver.rs index 9a51ef25..05c7f90a 100644 --- a/native/rust/did/x509/src/resolver.rs +++ b/native/rust/did/x509/src/resolver.rs @@ -4,6 +4,7 @@ use crate::did_document::{DidDocument, VerificationMethod}; use crate::error::DidX509Error; use crate::validator::DidX509Validator; +use std::borrow::Cow; use std::collections::HashMap; use x509_parser::oid_registry::Oid; use x509_parser::prelude::*; @@ -106,7 +107,7 @@ impl DidX509Resolver { } /// Convert X.509 certificate public key to JWK format - fn public_key_to_jwk(cert: &X509Certificate) -> Result, DidX509Error> { + fn public_key_to_jwk(cert: &X509Certificate) -> Result, String>, DidX509Error> { let public_key = cert.public_key(); match public_key.parsed() { @@ -120,17 +121,17 @@ impl DidX509Resolver { } /// Convert RSA public key to JWK - fn rsa_to_jwk(rsa: &RSAPublicKey) -> Result, DidX509Error> { + fn rsa_to_jwk(rsa: &RSAPublicKey) -> Result, String>, DidX509Error> { let mut jwk = HashMap::new(); - jwk.insert("kty".to_string(), "RSA".to_string()); + jwk.insert(Cow::Borrowed("kty"), "RSA".to_string()); // Encode modulus (n) as base64url let n_base64 = base64url_encode(rsa.modulus); - jwk.insert("n".to_string(), n_base64); + jwk.insert(Cow::Borrowed("n"), n_base64); // Encode exponent (e) as base64url let e_base64 = base64url_encode(rsa.exponent); - jwk.insert("e".to_string(), e_base64); + jwk.insert(Cow::Borrowed("e"), e_base64); Ok(jwk) } @@ -139,14 +140,14 @@ impl DidX509Resolver { fn ec_to_jwk( cert: &X509Certificate, ec_point: &ECPoint, - ) -> Result, DidX509Error> { + ) -> Result, String>, DidX509Error> { let mut jwk = HashMap::new(); - jwk.insert("kty".to_string(), "EC".to_string()); + jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); // Determine the curve from the algorithm OID let alg_oid = &cert.public_key().algorithm.algorithm; let curve = Self::determine_ec_curve(alg_oid, ec_point.data())?; - jwk.insert("crv".to_string(), curve); + jwk.insert(Cow::Borrowed("crv"), curve.to_string()); // Extract x and y coordinates from the EC point // EC points are typically encoded as 0x04 || x || y for uncompressed points @@ -169,8 +170,8 @@ impl DidX509Resolver { let x = &point_data[1..1 + coord_len]; let y = &point_data[1 + coord_len..]; - jwk.insert("x".to_string(), base64url_encode(x)); - jwk.insert("y".to_string(), base64url_encode(y)); + jwk.insert(Cow::Borrowed("x"), base64url_encode(x)); + jwk.insert(Cow::Borrowed("y"), base64url_encode(y)); } else { return Err(DidX509Error::InvalidChain( "Compressed EC point format not supported".to_string(), @@ -181,7 +182,7 @@ impl DidX509Resolver { } /// Determine EC curve name from algorithm parameters - fn determine_ec_curve(alg_oid: &Oid, point_data: &[u8]) -> Result { + fn determine_ec_curve(alg_oid: &Oid, point_data: &[u8]) -> Result<&'static str, DidX509Error> { // Common EC curve OIDs const P256_OID: &str = "1.2.840.10045.3.1.7"; // secp256r1 / prime256v1 const P384_OID: &str = "1.3.132.0.34"; // secp384r1 @@ -212,6 +213,6 @@ impl DidX509Resolver { } }; - Ok(curve.to_string()) + Ok(curve) } } diff --git a/native/rust/did/x509/src/x509_extensions.rs b/native/rust/did/x509/src/x509_extensions.rs index a42a94dc..dfae6e7e 100644 --- a/native/rust/did/x509/src/x509_extensions.rs +++ b/native/rust/did/x509/src/x509_extensions.rs @@ -3,10 +3,11 @@ use crate::constants::*; use crate::error::DidX509Error; +use std::borrow::Cow; use x509_parser::prelude::*; /// Extract Extended Key Usage OIDs from a certificate -pub fn extract_extended_key_usage(cert: &X509Certificate) -> Vec { +pub fn extract_extended_key_usage(cert: &X509Certificate) -> Vec> { let mut ekus = Vec::new(); for ext in cert.extensions() { @@ -14,27 +15,27 @@ pub fn extract_extended_key_usage(cert: &X509Certificate) -> Vec { if let ParsedExtension::ExtendedKeyUsage(eku) = ext.parsed_extension() { // Add standard EKU OIDs if eku.server_auth { - ekus.push("1.3.6.1.5.5.7.3.1".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.1")); } if eku.client_auth { - ekus.push("1.3.6.1.5.5.7.3.2".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.2")); } if eku.code_signing { - ekus.push("1.3.6.1.5.5.7.3.3".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.3")); } if eku.email_protection { - ekus.push("1.3.6.1.5.5.7.3.4".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.4")); } if eku.time_stamping { - ekus.push("1.3.6.1.5.5.7.3.8".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.8")); } if eku.ocsp_signing { - ekus.push("1.3.6.1.5.5.7.3.9".to_string()); + ekus.push(Cow::Borrowed("1.3.6.1.5.5.7.3.9")); } // Add other/custom OIDs for oid in &eku.other { - ekus.push(oid.to_id_string()); + ekus.push(Cow::Owned(oid.to_id_string())); } } } @@ -44,7 +45,7 @@ pub fn extract_extended_key_usage(cert: &X509Certificate) -> Vec { } /// Extract EKU OIDs from a certificate (alias for builder convenience) -pub fn extract_eku_oids(cert: &X509Certificate) -> Result, DidX509Error> { +pub fn extract_eku_oids(cert: &X509Certificate) -> Result>, DidX509Error> { let oids = extract_extended_key_usage(cert); Ok(oids) } diff --git a/native/rust/did/x509/tests/additional_coverage_tests.rs b/native/rust/did/x509/tests/additional_coverage_tests.rs index 7b7ade6d..00ce54d3 100644 --- a/native/rust/did/x509/tests/additional_coverage_tests.rs +++ b/native/rust/did/x509/tests/additional_coverage_tests.rs @@ -1,349 +1,350 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional coverage tests for DID:x509 library to achieve 90% line coverage. -//! -//! These tests focus on: -//! 1. resolver.rs - EC JWK conversion paths, edge cases -//! 2. x509_extensions.rs - EKU extraction, CA detection -//! 3. Base64 encoding edge cases - -use did_x509::builder::DidX509Builder; -use did_x509::error::DidX509Error; -use did_x509::models::policy::DidX509Policy; -use did_x509::resolver::DidX509Resolver; -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use rcgen::string::Ia5String; -use rcgen::{ - BasicConstraints as RcgenBasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, - IsCa, KeyPair, SanType as RcgenSanType, -}; -use x509_parser::prelude::*; - -/// Generate an EC certificate with code signing EKU -fn generate_ec_cert_with_eku(ekus: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Certificate"); - params.extended_key_usages = ekus; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a CA certificate with BasicConstraints(CA:true) -fn generate_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test CA Certificate"); - params.is_ca = IsCa::Ca(RcgenBasicConstraints::Unconstrained); - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a non-CA certificate -fn generate_non_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Non-CA Certificate"); - params.is_ca = IsCa::NoCa; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a certificate with multiple EKU extensions -fn generate_multi_eku_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Multi EKU Certificate"); - params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::EmailProtection, - ExtendedKeyUsagePurpose::TimeStamping, - ExtendedKeyUsagePurpose::OcspSigning, - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate certificate with no extensions -fn generate_plain_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Plain Certificate"); - // No extended_key_usages, no is_ca, no SAN - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -// ============================================================================ -// Resolver tests - covering EC JWK conversion and base64url encoding -// ============================================================================ - -#[test] -fn test_resolver_ec_p256_jwk() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]); - assert!( - result.is_ok(), - "Should resolve EC P-256 cert: {:?}", - result.err() - ); - - let doc = result.unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - - // Verify EC JWK structure - assert_eq!(jwk.get("kty").unwrap(), "EC"); - assert_eq!(jwk.get("crv").unwrap(), "P-256"); - assert!(jwk.contains_key("x")); - assert!(jwk.contains_key("y")); -} - -#[test] -fn test_resolver_did_document_structure() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); - - // Verify DID Document structure - assert_eq!(result.id, did); - assert!(!result.context.is_empty()); - assert!(result - .context - .contains(&"https://www.w3.org/ns/did/v1".to_string())); - assert_eq!(result.verification_method.len(), 1); - assert_eq!(result.assertion_method.len(), 1); - - // Verify verification method structure - let vm = &result.verification_method[0]; - assert!(vm.id.starts_with(&did)); - assert!(vm.id.ends_with("#key-1")); - assert_eq!(vm.type_, "JsonWebKey2020"); - assert_eq!(vm.controller, did); -} - -#[test] -fn test_resolver_validation_failure() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - // Create DID requiring Code Signing EKU, but cert only has Server Auth - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); // Code Signing - - // Use a correct fingerprint but wrong policy - use sha2::{Digest, Sha256}; - let fingerprint = Sha256::digest(&cert_der); - let fingerprint_hex = hex::encode(fingerprint); - let did = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - fingerprint_hex - ); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]); - assert!( - result.is_err(), - "Should fail - cert doesn't have required EKU" - ); -} - -// ============================================================================ -// x509_extensions tests - covering all standard EKU OIDs -// ============================================================================ - -#[test] -fn test_extract_all_standard_ekus() { - let cert_der = generate_multi_eku_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - - // Should contain all 6 standard EKU OIDs - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.1".to_string()), - "Missing ServerAuth" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.2".to_string()), - "Missing ClientAuth" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.3".to_string()), - "Missing CodeSigning" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.4".to_string()), - "Missing EmailProtection" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.8".to_string()), - "Missing TimeStamping" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.9".to_string()), - "Missing OcspSigning" - ); -} - -#[test] -fn test_extract_single_eku_code_signing() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert_eq!(ekus.len(), 1); - assert_eq!(ekus[0], "1.3.6.1.5.5.7.3.3"); -} - -#[test] -fn test_extract_eku_oids_wrapper_success() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - - let oids = result.unwrap(); - assert!(oids.contains(&"1.3.6.1.5.5.7.3.1".to_string())); -} - -#[test] -fn test_extract_eku_no_extension() { - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.is_empty(), - "Cert without EKU extension should return empty vec" - ); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - assert!(result.unwrap().is_empty()); -} - -// ============================================================================ -// CA certificate detection tests -// ============================================================================ - -#[test] -fn test_is_ca_certificate_true() { - let cert_der = generate_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(is_ca, "CA certificate should be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_false() { - let cert_der = generate_non_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Non-CA certificate should not be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_no_basic_constraints() { - // Plain cert has no basic constraints extension at all - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Cert without BasicConstraints should not be CA"); -} - -// ============================================================================ -// Fulcio issuer extraction tests -// ============================================================================ - -#[test] -fn test_extract_fulcio_issuer_none() { - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!( - issuer.is_none(), - "Regular cert should not have Fulcio issuer" - ); -} - -#[test] -fn test_extract_fulcio_issuer_not_present() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!(issuer.is_none()); -} - -// ============================================================================ -// Base64url encoding edge cases (via resolver) -// ============================================================================ - -#[test] -fn test_base64url_no_padding() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let doc = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - - // base64url encoding should NOT have padding characters - let x = jwk.get("x").unwrap(); - let y = jwk.get("y").unwrap(); - - assert!(!x.contains('='), "x should not have padding"); - assert!(!y.contains('='), "y should not have padding"); - assert!(!x.contains('+'), "x should use URL-safe alphabet"); - assert!(!y.contains('+'), "y should use URL-safe alphabet"); - assert!(!x.contains('/'), "x should use URL-safe alphabet"); - assert!(!y.contains('/'), "y should use URL-safe alphabet"); -} - -// ============================================================================ -// Error path coverage -// ============================================================================ - -#[test] -fn test_resolver_empty_chain() { - let did = - "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.3.6.1.5.5.7.3.3"; - - let result = DidX509Resolver::resolve(did, &[]); - assert!(result.is_err(), "Should fail with empty chain"); -} - -#[test] -fn test_resolver_invalid_did_format() { - let cert_der = generate_plain_cert(); - let invalid_did = "not:a:valid:did"; - - let result = DidX509Resolver::resolve(invalid_did, &[&cert_der]); - assert!(result.is_err(), "Should fail with invalid DID format"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional coverage tests for DID:x509 library to achieve 90% line coverage. +//! +//! These tests focus on: +//! 1. resolver.rs - EC JWK conversion paths, edge cases +//! 2. x509_extensions.rs - EKU extraction, CA detection +//! 3. Base64 encoding edge cases + +use did_x509::builder::DidX509Builder; +use did_x509::error::DidX509Error; +use did_x509::models::policy::DidX509Policy; +use did_x509::resolver::DidX509Resolver; +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use rcgen::string::Ia5String; +use rcgen::{ + BasicConstraints as RcgenBasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, + IsCa, KeyPair, SanType as RcgenSanType, +}; +use x509_parser::prelude::*; +use std::borrow::Cow; + +/// Generate an EC certificate with code signing EKU +fn generate_ec_cert_with_eku(ekus: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Certificate"); + params.extended_key_usages = ekus; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a CA certificate with BasicConstraints(CA:true) +fn generate_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test CA Certificate"); + params.is_ca = IsCa::Ca(RcgenBasicConstraints::Unconstrained); + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a non-CA certificate +fn generate_non_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Non-CA Certificate"); + params.is_ca = IsCa::NoCa; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a certificate with multiple EKU extensions +fn generate_multi_eku_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Multi EKU Certificate"); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ExtendedKeyUsagePurpose::TimeStamping, + ExtendedKeyUsagePurpose::OcspSigning, + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate certificate with no extensions +fn generate_plain_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Plain Certificate"); + // No extended_key_usages, no is_ca, no SAN + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +// ============================================================================ +// Resolver tests - covering EC JWK conversion and base64url encoding +// ============================================================================ + +#[test] +fn test_resolver_ec_p256_jwk() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]); + assert!( + result.is_ok(), + "Should resolve EC P-256 cert: {:?}", + result.err() + ); + + let doc = result.unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + + // Verify EC JWK structure + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-256"); + assert!(jwk.contains_key("x")); + assert!(jwk.contains_key("y")); +} + +#[test] +fn test_resolver_did_document_structure() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); + + // Verify DID Document structure + assert_eq!(result.id, did); + assert!(!result.context.is_empty()); + assert!(result + .context + .contains(&"https://www.w3.org/ns/did/v1".to_string())); + assert_eq!(result.verification_method.len(), 1); + assert_eq!(result.assertion_method.len(), 1); + + // Verify verification method structure + let vm = &result.verification_method[0]; + assert!(vm.id.starts_with(&did)); + assert!(vm.id.ends_with("#key-1")); + assert_eq!(vm.type_, "JsonWebKey2020"); + assert_eq!(vm.controller, did); +} + +#[test] +fn test_resolver_validation_failure() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + // Create DID requiring Code Signing EKU, but cert only has Server Auth + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); // Code Signing + + // Use a correct fingerprint but wrong policy + use sha2::{Digest, Sha256}; + let fingerprint = Sha256::digest(&cert_der); + let fingerprint_hex = hex::encode(fingerprint); + let did = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + fingerprint_hex + ); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]); + assert!( + result.is_err(), + "Should fail - cert doesn't have required EKU" + ); +} + +// ============================================================================ +// x509_extensions tests - covering all standard EKU OIDs +// ============================================================================ + +#[test] +fn test_extract_all_standard_ekus() { + let cert_der = generate_multi_eku_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + + // Should contain all 6 standard EKU OIDs + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Missing ServerAuth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Missing ClientAuth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Missing CodeSigning" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Missing EmailProtection" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Missing TimeStamping" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Missing OcspSigning" + ); +} + +#[test] +fn test_extract_single_eku_code_signing() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert_eq!(ekus.len(), 1); + assert_eq!(ekus[0], "1.3.6.1.5.5.7.3.3"); +} + +#[test] +fn test_extract_eku_oids_wrapper_success() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + + let oids = result.unwrap(); + assert!(oids.iter().any(|x| x == "1.3.6.1.5.5.7.3.1")); +} + +#[test] +fn test_extract_eku_no_extension() { + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.is_empty(), + "Cert without EKU extension should return empty vec" + ); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); +} + +// ============================================================================ +// CA certificate detection tests +// ============================================================================ + +#[test] +fn test_is_ca_certificate_true() { + let cert_der = generate_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(is_ca, "CA certificate should be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_false() { + let cert_der = generate_non_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Non-CA certificate should not be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_no_basic_constraints() { + // Plain cert has no basic constraints extension at all + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Cert without BasicConstraints should not be CA"); +} + +// ============================================================================ +// Fulcio issuer extraction tests +// ============================================================================ + +#[test] +fn test_extract_fulcio_issuer_none() { + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!( + issuer.is_none(), + "Regular cert should not have Fulcio issuer" + ); +} + +#[test] +fn test_extract_fulcio_issuer_not_present() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!(issuer.is_none()); +} + +// ============================================================================ +// Base64url encoding edge cases (via resolver) +// ============================================================================ + +#[test] +fn test_base64url_no_padding() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let doc = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + + // base64url encoding should NOT have padding characters + let x = jwk.get("x").unwrap(); + let y = jwk.get("y").unwrap(); + + assert!(!x.contains('='), "x should not have padding"); + assert!(!y.contains('='), "y should not have padding"); + assert!(!x.contains('+'), "x should use URL-safe alphabet"); + assert!(!y.contains('+'), "y should use URL-safe alphabet"); + assert!(!x.contains('/'), "x should use URL-safe alphabet"); + assert!(!y.contains('/'), "y should use URL-safe alphabet"); +} + +// ============================================================================ +// Error path coverage +// ============================================================================ + +#[test] +fn test_resolver_empty_chain() { + let did = + "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.3.6.1.5.5.7.3.3"; + + let result = DidX509Resolver::resolve(did, &[]); + assert!(result.is_err(), "Should fail with empty chain"); +} + +#[test] +fn test_resolver_invalid_did_format() { + let cert_der = generate_plain_cert(); + let invalid_did = "not:a:valid:did"; + + let result = DidX509Resolver::resolve(invalid_did, &[&cert_der]); + assert!(result.is_err(), "Should fail with invalid DID format"); +} diff --git a/native/rust/did/x509/tests/builder_tests.rs b/native/rust/did/x509/tests/builder_tests.rs index b5037cdf..aa77f0fe 100644 --- a/native/rust/did/x509/tests/builder_tests.rs +++ b/native/rust/did/x509/tests/builder_tests.rs @@ -1,354 +1,355 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use did_x509::{ - builder::DidX509Builder, - constants::*, - models::policy::{DidX509Policy, SanType}, - parsing::DidX509Parser, - DidX509Error, -}; - -// Inline base64 utilities for tests -const BASE64_STANDARD: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { - let mut lookup = [0xFFu8; 256]; - for (i, &c) in alphabet.iter().enumerate() { - lookup[c as usize] = i as u8; - } - - let input = input.trim_end_matches('='); - let mut out = Vec::with_capacity(input.len() * 3 / 4); - let mut buf: u32 = 0; - let mut bits: u32 = 0; - - for &b in input.as_bytes() { - let val = lookup[b as usize]; - if val == 0xFF { - return Err(format!("invalid base64 byte: 0x{:02x}", b)); - } - buf = (buf << 6) | val as u32; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((buf >> bits) as u8); - buf &= (1 << bits) - 1; - } - } - Ok(out) -} - -fn base64_standard_decode(input: &str) -> Result, String> { - base64_decode(input, BASE64_STANDARD) -} - -/// Create a simple self-signed test certificate in DER format -/// This is a minimal test certificate for unit testing purposes -fn create_test_cert_der() -> Vec { - // This is a minimal self-signed certificate encoded in DER format - // Subject: CN=Test CA, O=Test Org - // Validity: Not critical for fingerprint testing - // This is a real DER-encoded certificate for testing - let cert_pem = r#"-----BEGIN CERTIFICATE----- -MIICpDCCAYwCCQDU7T7JbtQhxTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU -ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD -VQQDDAlUZXN0IFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO -8vH0PqH3m3KkjvFnqvqp8aIJYVIqW+aTvnW5VNvz6rQkX8d8VnNqPfGYQxJjMzTl -xJ3FxU7dI5C5PbF8qQqOkZ7lNxL+XH5LPnvZdF3zV8lJxVR5J3LWnE5eQqYHqOkT -yJNlM6xvF8kPqOB7hH5vFXrXxqPvLlQqQqZPvGqHqKFLvLZqQqPvKqQqPvLqQqPv -LqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQq -PvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLq -QqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqAgMBAAEwDQYJKoZIhvcNAQELBQADggEB -AKT3qxYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYq -KYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqK -YqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKY -qLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYq -LVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqL -VYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLV -YqKYqA== ------END CERTIFICATE-----"#; - - // Parse PEM and extract DER - let cert_lines: Vec<&str> = cert_pem - .lines() - .filter(|line| !line.contains("BEGIN") && !line.contains("END")) - .collect(); - let cert_base64 = cert_lines.join(""); - - // Decode base64 to DER - base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") -} - -/// Create a test leaf certificate with EKU extension -fn create_test_leaf_cert_with_eku() -> Vec { - // A test certificate with EKU extension - let cert_pem = r#"-----BEGIN CERTIFICATE----- -MIICrjCCAZYCCQCxvF8bFxMqFjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU -ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD -VQQDDAlUZXN0IExlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP -HqYxNKj5J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxAgMBAAGj -PDBOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH -AwEwDQYJKoZIhvcNAQELBQADggEBAA== ------END CERTIFICATE-----"#; - - let cert_lines: Vec<&str> = cert_pem - .lines() - .filter(|line| !line.contains("BEGIN") && !line.contains("END")) - .collect(); - let cert_base64 = cert_lines.join(""); - base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") -} - -#[test] -fn test_build_with_eku_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string()]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2")); -} - -#[test] -fn test_build_with_multiple_eku_oids() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec![ - "1.3.6.1.5.5.7.3.2".to_string(), - "1.3.6.1.5.5.7.3.3".to_string(), - ]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2:1.3.6.1.5.5.7.3.3")); -} - -#[test] -fn test_build_with_subject_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Subject(vec![ - ("CN".to_string(), "example.com".to_string()), - ("O".to_string(), "Example Org".to_string()), - ]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::subject:CN:example.com:O:Example%20Org")); -} - -#[test] -fn test_build_with_san_email_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Email, "test@example.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:email:test%40example.com")); -} - -#[test] -fn test_build_with_san_dns_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:dns:example.com")); -} - -#[test] -fn test_build_with_san_uri_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Uri, "https://example.com/path".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:uri:https%3A%2F%2Fexample.com%2Fpath")); -} - -#[test] -fn test_build_with_fulcio_issuer_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::fulcio-issuer:accounts.google.com")); -} - -#[test] -fn test_build_with_multiple_policies() { - let ca_cert = create_test_cert_der(); - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string()]), - DidX509Policy::Subject(vec![("CN".to_string(), "test".to_string())]), - ]; - - let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); - - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2::subject:CN:test")); -} - -#[test] -fn test_build_with_sha256() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA256).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - // SHA-256 produces 32 bytes = 43 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 43); -} - -#[test] -fn test_build_with_sha384() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA384).unwrap(); - - assert!(did.starts_with("did:x509:0:sha384:")); - // SHA-384 produces 48 bytes = 64 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 64); -} - -#[test] -fn test_build_with_sha512() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA512).unwrap(); - - assert!(did.starts_with("did:x509:0:sha512:")); - // SHA-512 produces 64 bytes = 86 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 86); -} - -#[test] -fn test_build_with_invalid_hash_algorithm() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - - let result = DidX509Builder::build(&ca_cert, &[policy], "sha1"); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - DidX509Error::UnsupportedHashAlgorithm("sha1".to_string()) - ); -} - -#[test] -fn test_build_from_chain() { - let leaf_cert = create_test_leaf_cert_with_eku(); - let ca_cert = create_test_cert_der(); - let chain: Vec<&[u8]> = vec![&leaf_cert, &ca_cert]; - - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); - - // Should use the last cert (CA) for fingerprint - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::eku:1.2.3.4")); -} - -#[test] -fn test_build_from_chain_empty() { - let chain: Vec<&[u8]> = vec![]; - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - - let result = DidX509Builder::build_from_chain(&chain, &[policy]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - DidX509Error::InvalidChain("Empty chain".to_string()) - ); -} - -#[test] -fn test_build_from_chain_single_cert() { - let ca_cert = create_test_cert_der(); - let chain: Vec<&[u8]> = vec![&ca_cert]; - - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string()]); - let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); -} - -#[test] -fn test_roundtrip_build_and_parse() { - let ca_cert = create_test_cert_der(); - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string()]), - DidX509Policy::Subject(vec![ - ("CN".to_string(), "test.example.com".to_string()), - ("O".to_string(), "Test Org".to_string()), - ]), - DidX509Policy::San(SanType::Dns, "example.com".to_string()), - ]; - - let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); - - // Parse the built DID - let parsed = DidX509Parser::parse(&did).unwrap(); - - // Verify structure - assert_eq!(parsed.hash_algorithm, HASH_ALGORITHM_SHA256); - assert_eq!(parsed.policies.len(), 3); - - // Verify EKU policy - if let DidX509Policy::Eku(oids) = &parsed.policies[0] { - assert_eq!(oids, &vec!["1.3.6.1.5.5.7.3.2".to_string()]); - } else { - panic!("Expected EKU policy"); - } - - // Verify Subject policy - if let DidX509Policy::Subject(attrs) = &parsed.policies[1] { - assert_eq!(attrs.len(), 2); - assert_eq!(attrs[0], ("CN".to_string(), "test.example.com".to_string())); - assert_eq!(attrs[1], ("O".to_string(), "Test Org".to_string())); - } else { - panic!("Expected Subject policy"); - } - - // Verify SAN policy - if let DidX509Policy::San(san_type, value) = &parsed.policies[2] { - assert_eq!(*san_type, SanType::Dns); - assert_eq!(value, "example.com"); - } else { - panic!("Expected SAN policy"); - } -} - -#[test] -fn test_encode_policy_with_special_characters() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Subject(vec![( - "CN".to_string(), - "Test: Value, With Special/Chars".to_string(), - )]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - // Special characters should be percent-encoded - assert!(did.contains("%3A")); // colon - assert!(did.contains("%2C")); // comma - assert!(did.contains("%2F")); // slash -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; +use did_x509::{ + builder::DidX509Builder, + constants::*, + models::policy::{DidX509Policy, SanType}, + parsing::DidX509Parser, + DidX509Error, +}; + +// Inline base64 utilities for tests +const BASE64_STANDARD: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { + let mut lookup = [0xFFu8; 256]; + for (i, &c) in alphabet.iter().enumerate() { + lookup[c as usize] = i as u8; + } + + let input = input.trim_end_matches('='); + let mut out = Vec::with_capacity(input.len() * 3 / 4); + let mut buf: u32 = 0; + let mut bits: u32 = 0; + + for &b in input.as_bytes() { + let val = lookup[b as usize]; + if val == 0xFF { + return Err(format!("invalid base64 byte: 0x{:02x}", b)); + } + buf = (buf << 6) | val as u32; + bits += 6; + if bits >= 8 { + bits -= 8; + out.push((buf >> bits) as u8); + buf &= (1 << bits) - 1; + } + } + Ok(out) +} + +fn base64_standard_decode(input: &str) -> Result, String> { + base64_decode(input, BASE64_STANDARD) +} + +/// Create a simple self-signed test certificate in DER format +/// This is a minimal test certificate for unit testing purposes +fn create_test_cert_der() -> Vec { + // This is a minimal self-signed certificate encoded in DER format + // Subject: CN=Test CA, O=Test Org + // Validity: Not critical for fingerprint testing + // This is a real DER-encoded certificate for testing + let cert_pem = r#"-----BEGIN CERTIFICATE----- +MIICpDCCAYwCCQDU7T7JbtQhxTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU +ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD +VQQDDAlUZXN0IFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO +8vH0PqH3m3KkjvFnqvqp8aIJYVIqW+aTvnW5VNvz6rQkX8d8VnNqPfGYQxJjMzTl +xJ3FxU7dI5C5PbF8qQqOkZ7lNxL+XH5LPnvZdF3zV8lJxVR5J3LWnE5eQqYHqOkT +yJNlM6xvF8kPqOB7hH5vFXrXxqPvLlQqQqZPvGqHqKFLvLZqQqPvKqQqPvLqQqPv +LqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQq +PvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLq +QqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqAgMBAAEwDQYJKoZIhvcNAQELBQADggEB +AKT3qxYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYq +KYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqK +YqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKY +qLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYq +LVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqL +VYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLV +YqKYqA== +-----END CERTIFICATE-----"#; + + // Parse PEM and extract DER + let cert_lines: Vec<&str> = cert_pem + .lines() + .filter(|line| !line.contains("BEGIN") && !line.contains("END")) + .collect(); + let cert_base64 = cert_lines.join(""); + + // Decode base64 to DER + base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") +} + +/// Create a test leaf certificate with EKU extension +fn create_test_leaf_cert_with_eku() -> Vec { + // A test certificate with EKU extension + let cert_pem = r#"-----BEGIN CERTIFICATE----- +MIICrjCCAZYCCQCxvF8bFxMqFjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU +ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD +VQQDDAlUZXN0IExlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP +HqYxNKj5J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxAgMBAAGj +PDBOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwEwDQYJKoZIhvcNAQELBQADggEBAA== +-----END CERTIFICATE-----"#; + + let cert_lines: Vec<&str> = cert_pem + .lines() + .filter(|line| !line.contains("BEGIN") && !line.contains("END")) + .collect(); + let cert_base64 = cert_lines.join(""); + base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") +} + +#[test] +fn test_build_with_eku_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2")); +} + +#[test] +fn test_build_with_multiple_eku_oids() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec![ + "1.3.6.1.5.5.7.3.2".to_string().into(), + "1.3.6.1.5.5.7.3.3".to_string().into(), + ]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2:1.3.6.1.5.5.7.3.3")); +} + +#[test] +fn test_build_with_subject_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Subject(vec![ + ("CN".to_string(), "example.com".to_string()), + ("O".to_string(), "Example Org".to_string()), + ]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::subject:CN:example.com:O:Example%20Org")); +} + +#[test] +fn test_build_with_san_email_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Email, "test@example.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:email:test%40example.com")); +} + +#[test] +fn test_build_with_san_dns_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:dns:example.com")); +} + +#[test] +fn test_build_with_san_uri_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Uri, "https://example.com/path".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:uri:https%3A%2F%2Fexample.com%2Fpath")); +} + +#[test] +fn test_build_with_fulcio_issuer_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::fulcio-issuer:accounts.google.com")); +} + +#[test] +fn test_build_with_multiple_policies() { + let ca_cert = create_test_cert_der(); + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), + DidX509Policy::Subject(vec![("CN".to_string(), "test".to_string())]), + ]; + + let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); + + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2::subject:CN:test")); +} + +#[test] +fn test_build_with_sha256() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA256).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + // SHA-256 produces 32 bytes = 43 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 43); +} + +#[test] +fn test_build_with_sha384() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA384).unwrap(); + + assert!(did.starts_with("did:x509:0:sha384:")); + // SHA-384 produces 48 bytes = 64 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 64); +} + +#[test] +fn test_build_with_sha512() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA512).unwrap(); + + assert!(did.starts_with("did:x509:0:sha512:")); + // SHA-512 produces 64 bytes = 86 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 86); +} + +#[test] +fn test_build_with_invalid_hash_algorithm() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let result = DidX509Builder::build(&ca_cert, &[policy], "sha1"); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + DidX509Error::UnsupportedHashAlgorithm("sha1".to_string()) + ); +} + +#[test] +fn test_build_from_chain() { + let leaf_cert = create_test_leaf_cert_with_eku(); + let ca_cert = create_test_cert_der(); + let chain: Vec<&[u8]> = vec![&leaf_cert, &ca_cert]; + + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); + + // Should use the last cert (CA) for fingerprint + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::eku:1.2.3.4")); +} + +#[test] +fn test_build_from_chain_empty() { + let chain: Vec<&[u8]> = vec![]; + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let result = DidX509Builder::build_from_chain(&chain, &[policy]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + DidX509Error::InvalidChain("Empty chain".to_string()) + ); +} + +#[test] +fn test_build_from_chain_single_cert() { + let ca_cert = create_test_cert_der(); + let chain: Vec<&[u8]> = vec![&ca_cert]; + + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); +} + +#[test] +fn test_roundtrip_build_and_parse() { + let ca_cert = create_test_cert_der(); + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), + DidX509Policy::Subject(vec![ + ("CN".to_string(), "test.example.com".to_string()), + ("O".to_string(), "Test Org".to_string()), + ]), + DidX509Policy::San(SanType::Dns, "example.com".to_string()), + ]; + + let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); + + // Parse the built DID + let parsed = DidX509Parser::parse(&did).unwrap(); + + // Verify structure + assert_eq!(parsed.hash_algorithm, HASH_ALGORITHM_SHA256); + assert_eq!(parsed.policies.len(), 3); + + // Verify EKU policy + if let DidX509Policy::Eku(oids) = &parsed.policies[0] { + assert_eq!(oids, &vec!["1.3.6.1.5.5.7.3.2".to_string()]); + } else { + panic!("Expected EKU policy"); + } + + // Verify Subject policy + if let DidX509Policy::Subject(attrs) = &parsed.policies[1] { + assert_eq!(attrs.len(), 2); + assert_eq!(attrs[0], ("CN".to_string(), "test.example.com".to_string())); + assert_eq!(attrs[1], ("O".to_string(), "Test Org".to_string())); + } else { + panic!("Expected Subject policy"); + } + + // Verify SAN policy + if let DidX509Policy::San(san_type, value) = &parsed.policies[2] { + assert_eq!(*san_type, SanType::Dns); + assert_eq!(value, "example.com"); + } else { + panic!("Expected SAN policy"); + } +} + +#[test] +fn test_encode_policy_with_special_characters() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Subject(vec![( + "CN".to_string(), + "Test: Value, With Special/Chars".to_string(), + )]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + // Special characters should be percent-encoded + assert!(did.contains("%3A")); // colon + assert!(did.contains("%2C")); // comma + assert!(did.contains("%2F")); // slash +} diff --git a/native/rust/did/x509/tests/did_document_tests.rs b/native/rust/did/x509/tests/did_document_tests.rs index ee824a61..52c3c3ec 100644 --- a/native/rust/did/x509/tests/did_document_tests.rs +++ b/native/rust/did/x509/tests/did_document_tests.rs @@ -1,85 +1,86 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use did_x509::{DidDocument, VerificationMethod}; -use std::collections::HashMap; - -#[test] -fn test_did_document_to_json() { - let mut jwk = HashMap::new(); - jwk.insert("kty".to_string(), "RSA".to_string()); - jwk.insert("n".to_string(), "test".to_string()); - jwk.insert("e".to_string(), "AQAB".to_string()); - - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - public_key_jwk: jwk, - }], - assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], - }; - - let json = doc.to_json(false).unwrap(); - assert!(json.contains("@context")); - assert!(json.contains("did:x509:0:sha256:test::eku:1.2.3")); - assert!(json.contains("verificationMethod")); - assert!(json.contains("assertionMethod")); -} - -#[test] -fn test_did_document_to_json_indented() { - let mut jwk = HashMap::new(); - jwk.insert("kty".to_string(), "EC".to_string()); - - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - public_key_jwk: jwk, - }], - assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], - }; - - // Test indented output - let json_indented = doc.to_json(true).unwrap(); - assert!(json_indented.contains('\n')); // Should have newlines - assert!(json_indented.contains("@context")); -} - -#[test] -fn test_did_document_clone_partial_eq() { - let mut jwk = HashMap::new(); - jwk.insert("kty".to_string(), "EC".to_string()); - - let doc1 = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), - public_key_jwk: jwk.clone(), - }], - assertion_method: vec!["did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string()], - }; - - // Clone and test equality - let doc2 = doc1.clone(); - assert_eq!(doc1, doc2); - - // Test inequality with different doc - let doc3 = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test2::eku:1.2.3".to_string(), - verification_method: vec![], - assertion_method: vec![], - }; - assert_ne!(doc1, doc3); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use did_x509::{DidDocument, VerificationMethod}; +use std::collections::HashMap; +use std::borrow::Cow; + +#[test] +fn test_did_document_to_json() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "RSA".to_string()); + jwk.insert(Cow::Borrowed("n"), "test".to_string()); + jwk.insert(Cow::Borrowed("e"), "AQAB".to_string()); + + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + public_key_jwk: jwk, + }], + assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], + }; + + let json = doc.to_json(false).unwrap(); + assert!(json.contains("@context")); + assert!(json.contains("did:x509:0:sha256:test::eku:1.2.3")); + assert!(json.contains("verificationMethod")); + assert!(json.contains("assertionMethod")); +} + +#[test] +fn test_did_document_to_json_indented() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); + + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + public_key_jwk: jwk, + }], + assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], + }; + + // Test indented output + let json_indented = doc.to_json(true).unwrap(); + assert!(json_indented.contains('\n')); // Should have newlines + assert!(json_indented.contains("@context")); +} + +#[test] +fn test_did_document_clone_partial_eq() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); + + let doc1 = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), + public_key_jwk: jwk.clone(), + }], + assertion_method: vec!["did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string()], + }; + + // Clone and test equality + let doc2 = doc1.clone(); + assert_eq!(doc1, doc2); + + // Test inequality with different doc + let doc3 = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test2::eku:1.2.3".to_string(), + verification_method: vec![], + assertion_method: vec![], + }; + assert_ne!(doc1, doc3); +} diff --git a/native/rust/did/x509/tests/policy_validator_tests.rs b/native/rust/did/x509/tests/policy_validator_tests.rs index ee5a33ea..799bc66c 100644 --- a/native/rust/did/x509/tests/policy_validator_tests.rs +++ b/native/rust/did/x509/tests/policy_validator_tests.rs @@ -1,375 +1,375 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive tests for policy validators with real X.509 certificates. -//! -//! Tests the policy_validators.rs functions with actual certificate generation -//! to ensure proper validation behavior for various policy types. - -use did_x509::error::DidX509Error; -use did_x509::models::SanType; -use did_x509::policy_validators::{ - validate_eku, validate_fulcio_issuer, validate_san, validate_subject, -}; -use rcgen::string::Ia5String; -use rcgen::ExtendedKeyUsagePurpose; -use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; -use x509_parser::prelude::*; - -/// Helper to generate a certificate with specific EKU OIDs. -fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test EKU Certificate"); - - if !eku_purposes.is_empty() { - params.extended_key_usages = eku_purposes; - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific subject attributes. -fn generate_cert_with_subject(attributes: Vec<(DnType, String)>) -> Vec { - let mut params = CertificateParams::default(); - - for (dn_type, value) in attributes { - params.distinguished_name.push(dn_type, value); - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific SAN entries. -fn generate_cert_with_san(san_entries: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test SAN Certificate"); - params.subject_alt_names = san_entries; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_eku_success_single_oid() { - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string()]); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_eku_success_multiple_oids() { - let cert_der = generate_cert_with_eku(vec![ - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::ClientAuth, - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string(), // Code Signing - "1.3.6.1.5.5.7.3.2".to_string(), // Client Auth - ], - ); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_eku_failure_missing_extension() { - let cert_der = generate_cert_with_eku(vec![]); // No EKU extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string()]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Extended Key Usage extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_eku_failure_wrong_oid() { - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string()]); // Expect Code Signing - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required EKU OID '1.3.6.1.5.5.7.3.3' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_success_single_attribute() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("CN".to_string(), "Test Subject".to_string())]); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_subject_success_multiple_attributes() { - let cert_der = generate_cert_with_subject(vec![ - (DnType::CommonName, "Test Subject".to_string()), - (DnType::OrganizationName, "Test Org".to_string()), - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject( - &cert, - &[ - ("CN".to_string(), "Test Subject".to_string()), - ("O".to_string(), "Test Org".to_string()), - ], - ); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_subject_failure_empty_attributes() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Must contain at least one attribute")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_attribute_not_found() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("O".to_string(), "Missing Org".to_string())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required attribute 'O' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_attribute_value_mismatch() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("CN".to_string(), "Wrong Subject".to_string())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("value mismatch")); - assert!(msg.contains("expected 'Wrong Subject', got 'Test Subject'")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_unknown_attribute() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("UNKNOWN".to_string(), "value".to_string())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Unknown attribute 'UNKNOWN'")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_success_dns() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( - Ia5String::try_from("example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_success_email() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Email, "test@example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_success_uri() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::URI( - Ia5String::try_from("https://example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Uri, "https://example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_failure_no_extension() { - let cert_der = generate_cert_with_san(vec![]); // No SAN extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Subject Alternative Names")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_failure_wrong_value() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( - Ia5String::try_from("wrong.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required SAN 'dns:example.com' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_failure_wrong_type() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "test@example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required SAN 'dns:test@example.com' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_fulcio_issuer_success() { - // Generate a basic certificate - Fulcio issuer extension testing would - // require more complex certificate generation with custom extensions - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Fulcio Test".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // This test will fail since the certificate doesn't have Fulcio extension - let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Fulcio issuer extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_fulcio_issuer_failure_missing_extension() { - let cert_der = generate_cert_with_subject(vec![(DnType::CommonName, "Test Cert".to_string())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Fulcio issuer extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_error_display_coverage() { - // Test additional error paths to improve coverage - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test with multiple missing EKU OIDs - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string(), // Code Signing - "1.3.6.1.5.5.7.3.4".to_string(), // Email Protection - ], - ); - assert!(result.is_err()); - - // Test subject validation with duplicate checks - let result2 = validate_subject( - &cert, - &[ - ("CN".to_string(), "Test".to_string()), - ("O".to_string(), "Missing".to_string()), - ], - ); - assert!(result2.is_err()); -} - -#[test] -fn test_policy_validation_edge_cases() { - let cert_der = generate_cert_with_subject(vec![ - (DnType::CommonName, "Edge Case Test".to_string()), - (DnType::OrganizationName, "Test Corp".to_string()), - (DnType::CountryName, "US".to_string()), - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test with less common DN attributes - let result = validate_subject(&cert, &[("C".to_string(), "US".to_string())]); - assert!(result.is_ok()); - - // Test with case sensitivity - let result2 = validate_subject( - &cert, - &[ - ("CN".to_string(), "edge case test".to_string()), // Different case - ], - ); - assert!(result2.is_err()); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive tests for policy validators with real X.509 certificates. +//! +//! Tests the policy_validators.rs functions with actual certificate generation +//! to ensure proper validation behavior for various policy types. + +use did_x509::error::DidX509Error; +use did_x509::models::SanType; +use did_x509::policy_validators::{ + validate_eku, validate_fulcio_issuer, validate_san, validate_subject, +}; +use rcgen::string::Ia5String; +use rcgen::ExtendedKeyUsagePurpose; +use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; +use x509_parser::prelude::*; + +/// Helper to generate a certificate with specific EKU OIDs. +fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test EKU Certificate"); + + if !eku_purposes.is_empty() { + params.extended_key_usages = eku_purposes; + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific subject attributes. +fn generate_cert_with_subject(attributes: Vec<(DnType, String)>) -> Vec { + let mut params = CertificateParams::default(); + + for (dn_type, value) in attributes { + params.distinguished_name.push(dn_type, value); + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific SAN entries. +fn generate_cert_with_san(san_entries: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test SAN Certificate"); + params.subject_alt_names = san_entries; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_eku_success_single_oid() { + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_eku_success_multiple_oids() { + let cert_der = generate_cert_with_eku(vec![ + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::ClientAuth, + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing + "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth + ], + ); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_eku_failure_missing_extension() { + let cert_der = generate_cert_with_eku(vec![]); // No EKU extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Extended Key Usage extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_eku_failure_wrong_oid() { + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); // Expect Code Signing + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required EKU OID '1.3.6.1.5.5.7.3.3' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_success_single_attribute() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[("CN".to_string().into(), "Test Subject".to_string().into())]); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_subject_success_multiple_attributes() { + let cert_der = generate_cert_with_subject(vec![ + (DnType::CommonName, "Test Subject".to_string().into()), + (DnType::OrganizationName, "Test Org".to_string().into()), + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test Subject".to_string().into()), + ("O".to_string().into(), "Test Org".to_string().into()), + ], + ); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_subject_failure_empty_attributes() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Must contain at least one attribute")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_attribute_not_found() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[("O".to_string().into(), "Missing Org".to_string().into())]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required attribute 'O' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_attribute_value_mismatch() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[("CN".to_string().into(), "Wrong Subject".to_string().into())]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("value mismatch")); + assert!(msg.contains("expected 'Wrong Subject', got 'Test Subject'")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_unknown_attribute() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[("UNKNOWN".to_string().into(), "value".to_string().into())]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Unknown attribute 'UNKNOWN'")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_success_dns() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( + Ia5String::try_from("example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_success_email() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Email, "test@example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_success_uri() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::URI( + Ia5String::try_from("https://example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Uri, "https://example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_failure_no_extension() { + let cert_der = generate_cert_with_san(vec![]); // No SAN extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Subject Alternative Names")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_failure_wrong_value() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( + Ia5String::try_from("wrong.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required SAN 'dns:example.com' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_failure_wrong_type() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "test@example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required SAN 'dns:test@example.com' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_fulcio_issuer_success() { + // Generate a basic certificate - Fulcio issuer extension testing would + // require more complex certificate generation with custom extensions + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Fulcio Test".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // This test will fail since the certificate doesn't have Fulcio extension + let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Fulcio issuer extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_fulcio_issuer_failure_missing_extension() { + let cert_der = generate_cert_with_subject(vec![(DnType::CommonName, "Test Cert".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Fulcio issuer extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_error_display_coverage() { + // Test additional error paths to improve coverage + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test with multiple missing EKU OIDs + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing + "1.3.6.1.5.5.7.3.4".to_string().into(), // Email Protection + ], + ); + assert!(result.is_err()); + + // Test subject validation with duplicate checks + let result2 = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test".to_string().into()), + ("O".to_string().into(), "Missing".to_string().into()), + ], + ); + assert!(result2.is_err()); +} + +#[test] +fn test_policy_validation_edge_cases() { + let cert_der = generate_cert_with_subject(vec![ + (DnType::CommonName, "Edge Case Test".to_string().into()), + (DnType::OrganizationName, "Test Corp".to_string().into()), + (DnType::CountryName, "US".to_string().into()), + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test with less common DN attributes + let result = validate_subject(&cert, &[("C".to_string().into(), "US".to_string().into())]); + assert!(result.is_ok()); + + // Test with case sensitivity + let result2 = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "edge case test".to_string().into()), // Different case + ], + ); + assert!(result2.is_err()); +} diff --git a/native/rust/did/x509/tests/policy_validators_coverage.rs b/native/rust/did/x509/tests/policy_validators_coverage.rs index 187e0746..6c5b5d8a 100644 --- a/native/rust/did/x509/tests/policy_validators_coverage.rs +++ b/native/rust/did/x509/tests/policy_validators_coverage.rs @@ -1,369 +1,369 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional coverage tests for policy validators to cover uncovered lines in policy_validators.rs. -//! -//! These tests target specific edge cases and error paths not covered by existing tests. - -use did_x509::error::DidX509Error; -use did_x509::models::SanType; -use did_x509::policy_validators::{ - validate_eku, validate_fulcio_issuer, validate_san, validate_subject, -}; -use rcgen::string::Ia5String; -use rcgen::ExtendedKeyUsagePurpose; -use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; -use x509_parser::prelude::*; - -/// Helper to generate a certificate with no EKU extension. -fn generate_cert_without_eku() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test No EKU Certificate"); - // Explicitly don't add extended_key_usages - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific subject attributes, including parsing edge cases. -fn generate_cert_with_subject_edge_cases() -> Vec { - let mut params = CertificateParams::default(); - // Add multiple types of subject attributes to test parsing - params - .distinguished_name - .push(DnType::CommonName, "Test Subject"); - params - .distinguished_name - .push(DnType::OrganizationName, "Test Org"); - params - .distinguished_name - .push(DnType::OrganizationalUnitName, "Test Unit"); - params.distinguished_name.push(DnType::CountryName, "US"); - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with no SAN extension. -fn generate_cert_without_san() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test No SAN Certificate"); - // Explicitly don't add subject_alt_names - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific SAN entries for edge case testing. -fn generate_cert_with_multiple_sans() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Multi SAN Certificate"); - - // Add multiple types of SANs - params.subject_alt_names = vec![ - RcgenSanType::DnsName(Ia5String::try_from("test1.example.com").unwrap()), - RcgenSanType::DnsName(Ia5String::try_from("test2.example.com").unwrap()), - RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), - RcgenSanType::IpAddress("192.168.1.1".parse().unwrap()), - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_eku_no_extension() { - let cert_der = generate_cert_without_eku(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string()]); - - // Should fail because certificate has no EKU extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("no Extended Key Usage"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_eku_missing_required_oid() { - // Generate cert with only code signing, but require both code signing and client auth - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string(), // Code Signing (present) - "1.3.6.1.5.5.7.3.2".to_string(), // Client Auth (missing) - ], - ); - - // Should fail because Client Auth EKU is missing - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("1.3.6.1.5.5.7.3.2") && msg.contains("not found"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -/// Helper to generate a certificate with specific EKU OIDs. -fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test EKU Certificate"); - - if !eku_purposes.is_empty() { - params.extended_key_usages = eku_purposes; - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_subject_empty_attributes() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Empty expected attributes should fail - let result = validate_subject(&cert, &[]); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("at least one attribute"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_unknown_attribute() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Use an unknown attribute label - let result = validate_subject( - &cert, - &[("UnknownAttribute".to_string(), "SomeValue".to_string())], - ); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("Unknown attribute") && msg.contains("UnknownAttribute"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_missing_attribute() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request an attribute that doesn't exist in the certificate - let result = validate_subject( - &cert, - &[ - ("L".to_string(), "NonExistent".to_string()), // Locality - ], - ); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("L"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_value_mismatch() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request CommonName with wrong value - let result = validate_subject(&cert, &[("CN".to_string(), "Wrong Name".to_string())]); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("value mismatch") && msg.contains("CN"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_success_multiple_attributes() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request multiple attributes that exist with correct values - let result = validate_subject( - &cert, - &[ - ("CN".to_string(), "Test Subject".to_string()), - ("O".to_string(), "Test Org".to_string()), - ("C".to_string(), "US".to_string()), - ], - ); - - assert!( - result.is_ok(), - "Multiple attribute validation should succeed" - ); -} - -#[test] -fn test_validate_san_no_extension() { - let cert_der = generate_cert_without_san(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "test.example.com"); - - // Should fail because certificate has no SAN extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("no Subject Alternative Names"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_not_found() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "nonexistent.example.com"); - - // Should fail because requested SAN doesn't exist - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("nonexistent.example.com"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_wrong_type() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Look for "test1.example.com" as an email instead of DNS name - let result = validate_san(&cert, &SanType::Email, "test1.example.com"); - - // Should fail because type doesn't match (it's a DNS name, not email) - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("email"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_success_multiple_types() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test each SAN type we added - assert!(validate_san(&cert, &SanType::Dns, "test1.example.com").is_ok()); - assert!(validate_san(&cert, &SanType::Dns, "test2.example.com").is_ok()); - assert!(validate_san(&cert, &SanType::Email, "test@example.com").is_ok()); -} - -#[test] -fn test_validate_fulcio_issuer_no_extension() { - let cert_der = generate_cert_without_san(); // Regular cert without Fulcio extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_fulcio_issuer(&cert, "github.com"); - - // Should fail because certificate has no Fulcio issuer extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("no Fulcio issuer extension"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -// Note: Testing successful Fulcio validation is difficult without creating certificates -// with the specific Fulcio extension, which would require more complex certificate creation. -// The main coverage goal is to test the error paths which we've done above. - -#[test] -fn test_validate_fulcio_issuer_url_normalization() { - // This test would ideally check the URL normalization logic in validate_fulcio_issuer, - // but since we can't easily create certificates with Fulcio extensions using rcgen, - // we've focused on the error path testing above. - - // The URL normalization logic (adding https:// prefix) is covered when the extension - // exists but doesn't match, which we can't easily test without the extension. - - // Test case showing the expected behavior: - // If we had a cert with Fulcio issuer "https://github.com" and expected "github.com", - // it should normalize to "https://github.com" and match. - - let cert_der = generate_cert_without_san(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // This will fail with "no extension" but shows the expected interface - let result = validate_fulcio_issuer(&cert, "github.com"); - assert!(result.is_err()); // Expected due to no extension -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional coverage tests for policy validators to cover uncovered lines in policy_validators.rs. +//! +//! These tests target specific edge cases and error paths not covered by existing tests. + +use did_x509::error::DidX509Error; +use did_x509::models::SanType; +use did_x509::policy_validators::{ + validate_eku, validate_fulcio_issuer, validate_san, validate_subject, +}; +use rcgen::string::Ia5String; +use rcgen::ExtendedKeyUsagePurpose; +use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; +use x509_parser::prelude::*; + +/// Helper to generate a certificate with no EKU extension. +fn generate_cert_without_eku() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test No EKU Certificate"); + // Explicitly don't add extended_key_usages + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific subject attributes, including parsing edge cases. +fn generate_cert_with_subject_edge_cases() -> Vec { + let mut params = CertificateParams::default(); + // Add multiple types of subject attributes to test parsing + params + .distinguished_name + .push(DnType::CommonName, "Test Subject"); + params + .distinguished_name + .push(DnType::OrganizationName, "Test Org"); + params + .distinguished_name + .push(DnType::OrganizationalUnitName, "Test Unit"); + params.distinguished_name.push(DnType::CountryName, "US"); + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with no SAN extension. +fn generate_cert_without_san() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test No SAN Certificate"); + // Explicitly don't add subject_alt_names + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific SAN entries for edge case testing. +fn generate_cert_with_multiple_sans() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Multi SAN Certificate"); + + // Add multiple types of SANs + params.subject_alt_names = vec![ + RcgenSanType::DnsName(Ia5String::try_from("test1.example.com").unwrap()), + RcgenSanType::DnsName(Ia5String::try_from("test2.example.com").unwrap()), + RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), + RcgenSanType::IpAddress("192.168.1.1".parse().unwrap()), + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_eku_no_extension() { + let cert_der = generate_cert_without_eku(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + + // Should fail because certificate has no EKU extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("no Extended Key Usage"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_eku_missing_required_oid() { + // Generate cert with only code signing, but require both code signing and client auth + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing (present) + "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth (missing) + ], + ); + + // Should fail because Client Auth EKU is missing + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("1.3.6.1.5.5.7.3.2") && msg.contains("not found"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +/// Helper to generate a certificate with specific EKU OIDs. +fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test EKU Certificate"); + + if !eku_purposes.is_empty() { + params.extended_key_usages = eku_purposes; + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_subject_empty_attributes() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Empty expected attributes should fail + let result = validate_subject(&cert, &[]); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("at least one attribute"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_unknown_attribute() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Use an unknown attribute label + let result = validate_subject( + &cert, + &[("UnknownAttribute".to_string().into(), "SomeValue".to_string().into())], + ); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("Unknown attribute") && msg.contains("UnknownAttribute"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_missing_attribute() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request an attribute that doesn't exist in the certificate + let result = validate_subject( + &cert, + &[ + ("L".to_string().into(), "NonExistent".to_string().into()), // Locality + ], + ); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("L"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_value_mismatch() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request CommonName with wrong value + let result = validate_subject(&cert, &[("CN".to_string().into(), "Wrong Name".to_string().into())]); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("value mismatch") && msg.contains("CN"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_success_multiple_attributes() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request multiple attributes that exist with correct values + let result = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test Subject".to_string().into()), + ("O".to_string().into(), "Test Org".to_string().into()), + ("C".to_string().into(), "US".to_string().into()), + ], + ); + + assert!( + result.is_ok(), + "Multiple attribute validation should succeed" + ); +} + +#[test] +fn test_validate_san_no_extension() { + let cert_der = generate_cert_without_san(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "test.example.com"); + + // Should fail because certificate has no SAN extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("no Subject Alternative Names"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_not_found() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "nonexistent.example.com"); + + // Should fail because requested SAN doesn't exist + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("nonexistent.example.com"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_wrong_type() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Look for "test1.example.com" as an email instead of DNS name + let result = validate_san(&cert, &SanType::Email, "test1.example.com"); + + // Should fail because type doesn't match (it's a DNS name, not email) + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("email"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_success_multiple_types() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test each SAN type we added + assert!(validate_san(&cert, &SanType::Dns, "test1.example.com").is_ok()); + assert!(validate_san(&cert, &SanType::Dns, "test2.example.com").is_ok()); + assert!(validate_san(&cert, &SanType::Email, "test@example.com").is_ok()); +} + +#[test] +fn test_validate_fulcio_issuer_no_extension() { + let cert_der = generate_cert_without_san(); // Regular cert without Fulcio extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_fulcio_issuer(&cert, "github.com"); + + // Should fail because certificate has no Fulcio issuer extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("no Fulcio issuer extension"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +// Note: Testing successful Fulcio validation is difficult without creating certificates +// with the specific Fulcio extension, which would require more complex certificate creation. +// The main coverage goal is to test the error paths which we've done above. + +#[test] +fn test_validate_fulcio_issuer_url_normalization() { + // This test would ideally check the URL normalization logic in validate_fulcio_issuer, + // but since we can't easily create certificates with Fulcio extensions using rcgen, + // we've focused on the error path testing above. + + // The URL normalization logic (adding https:// prefix) is covered when the extension + // exists but doesn't match, which we can't easily test without the extension. + + // Test case showing the expected behavior: + // If we had a cert with Fulcio issuer "https://github.com" and expected "github.com", + // it should normalize to "https://github.com" and match. + + let cert_der = generate_cert_without_san(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // This will fail with "no extension" but shows the expected interface + let result = validate_fulcio_issuer(&cert, "github.com"); + assert!(result.is_err()); // Expected due to no extension +} diff --git a/native/rust/did/x509/tests/resolver_coverage.rs b/native/rust/did/x509/tests/resolver_coverage.rs index fb3a7208..74b0fc61 100644 --- a/native/rust/did/x509/tests/resolver_coverage.rs +++ b/native/rust/did/x509/tests/resolver_coverage.rs @@ -1,176 +1,177 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive coverage tests for DidX509Resolver to cover uncovered lines in resolver.rs. -//! -//! These tests target specific uncovered paths in the resolver implementation. - -use did_x509::error::DidX509Error; -use did_x509::resolver::DidX509Resolver; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; - -/// Generate a self-signed X.509 certificate with EC key for testing JWK conversion. -fn generate_ec_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test EC Certificate"); - - // Add Extended Key Usage for Code Signing - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - // Use EC key (rcgen defaults to P-256) - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - - cert.der().to_vec() -} - -/// Generate an invalid certificate chain for testing error paths. -fn generate_invalid_cert() -> Vec { - vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER -} - -#[test] -fn test_resolver_with_valid_ec_chain() { - // Generate EC certificate (rcgen uses P-256 by default) - let cert_der = generate_ec_cert(); - - // Use the builder to create the DID (proper fingerprint calculation) - use did_x509::builder::DidX509Builder; - use did_x509::models::policy::DidX509Policy; - - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - - // Resolve DID to document - let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); - - // Verify success and EC JWK structure - assert!( - result.is_ok(), - "Resolution should succeed: {:?}", - result.err() - ); - let doc = result.unwrap(); - - assert_eq!(doc.id, did_string); - assert_eq!(doc.verification_method.len(), 1); - - // Verify EC JWK fields are present - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "EC"); - assert_eq!(jwk.get("crv").unwrap(), "P-256"); // rcgen default - assert!(jwk.contains_key("x")); // x coordinate - assert!(jwk.contains_key("y")); // y coordinate -} - -#[test] -fn test_resolver_chain_mismatch() { - // Generate one certificate - let cert_der1 = generate_ec_cert(); - - // Calculate fingerprint for a different certificate - let cert_der2 = generate_ec_cert(); - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(&cert_der2); - let fingerprint = hasher.finalize(); - let fingerprint_hex = hex::encode(&fingerprint[..]); - - // Build DID for cert2 but validate against cert1 - let did_string = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - fingerprint_hex - ); - - // Try to resolve with mismatched chain - let result = DidX509Resolver::resolve(&did_string, &[&cert_der1]); - - // Should fail due to validation failure - assert!( - result.is_err(), - "Resolution should fail with mismatched chain" - ); - - let error = result.unwrap_err(); - match error { - DidX509Error::PolicyValidationFailed(_) - | DidX509Error::FingerprintLengthMismatch(_, _, _) - | DidX509Error::ValidationFailed(_) => { - // Any of these errors indicate the chain doesn't match the DID - } - _ => panic!("Expected validation failure, got {:?}", error), - } -} - -#[test] -fn test_resolver_invalid_certificate_parsing() { - // Use invalid certificate data - let invalid_cert = generate_invalid_cert(); - let fingerprint_hex = hex::encode(&[0x00; 32]); // dummy fingerprint - - // Build a DID string - let did_string = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - fingerprint_hex - ); - - // Try to resolve with invalid certificate - let result = DidX509Resolver::resolve(&did_string, &[&invalid_cert]); - - // Should fail due to certificate parsing error or validation error - assert!( - result.is_err(), - "Resolution should fail with invalid certificate" - ); -} - -#[test] -fn test_resolver_mismatched_fingerprint() { - // Generate a certificate - let cert_der = generate_ec_cert(); - - // Use a wrong fingerprint hex (not matching the certificate) - let wrong_fingerprint_hex = hex::encode(&[0xFF; 32]); - let wrong_did_string = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - wrong_fingerprint_hex - ); - - let result = DidX509Resolver::resolve(&wrong_did_string, &[&cert_der]); - assert!(result.is_err(), "Should fail with fingerprint mismatch"); -} - -// Test base64url encoding coverage by testing different certificate types -#[test] -fn test_resolver_jwk_base64url_encoding() { - let cert_der = generate_ec_cert(); - - // Use the builder to create the DID (proper fingerprint calculation) - use did_x509::builder::DidX509Builder; - use did_x509::models::policy::DidX509Policy; - - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); - - assert!(result.is_ok(), "Resolution should succeed"); - let doc = result.unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - - // Verify EC coordinates are base64url encoded (no padding, no +/=) - if let (Some(x), Some(y)) = (jwk.get("x"), jwk.get("y")) { - assert!(!x.is_empty(), "x coordinate should not be empty"); - assert!(!y.is_empty(), "y coordinate should not be empty"); - - // Should not contain standard base64 chars or padding - assert!(!x.contains('='), "base64url should not contain padding"); - assert!(!x.contains('+'), "base64url should not contain '+'"); - assert!(!x.contains('/'), "base64url should not contain '/'"); - - assert!(!y.contains('='), "base64url should not contain padding"); - assert!(!y.contains('+'), "base64url should not contain '+'"); - assert!(!y.contains('/'), "base64url should not contain '/'"); - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive coverage tests for DidX509Resolver to cover uncovered lines in resolver.rs. +//! +//! These tests target specific uncovered paths in the resolver implementation. + +use did_x509::error::DidX509Error; +use did_x509::resolver::DidX509Resolver; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; +use std::borrow::Cow; + +/// Generate a self-signed X.509 certificate with EC key for testing JWK conversion. +fn generate_ec_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test EC Certificate"); + + // Add Extended Key Usage for Code Signing + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + // Use EC key (rcgen defaults to P-256) + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + + cert.der().to_vec() +} + +/// Generate an invalid certificate chain for testing error paths. +fn generate_invalid_cert() -> Vec { + vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER +} + +#[test] +fn test_resolver_with_valid_ec_chain() { + // Generate EC certificate (rcgen uses P-256 by default) + let cert_der = generate_ec_cert(); + + // Use the builder to create the DID (proper fingerprint calculation) + use did_x509::builder::DidX509Builder; + use did_x509::models::policy::DidX509Policy; + + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + + // Resolve DID to document + let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); + + // Verify success and EC JWK structure + assert!( + result.is_ok(), + "Resolution should succeed: {:?}", + result.err() + ); + let doc = result.unwrap(); + + assert_eq!(doc.id, did_string); + assert_eq!(doc.verification_method.len(), 1); + + // Verify EC JWK fields are present + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-256"); // rcgen default + assert!(jwk.contains_key("x")); // x coordinate + assert!(jwk.contains_key("y")); // y coordinate +} + +#[test] +fn test_resolver_chain_mismatch() { + // Generate one certificate + let cert_der1 = generate_ec_cert(); + + // Calculate fingerprint for a different certificate + let cert_der2 = generate_ec_cert(); + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(&cert_der2); + let fingerprint = hasher.finalize(); + let fingerprint_hex = hex::encode(&fingerprint[..]); + + // Build DID for cert2 but validate against cert1 + let did_string = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + fingerprint_hex + ); + + // Try to resolve with mismatched chain + let result = DidX509Resolver::resolve(&did_string, &[&cert_der1]); + + // Should fail due to validation failure + assert!( + result.is_err(), + "Resolution should fail with mismatched chain" + ); + + let error = result.unwrap_err(); + match error { + DidX509Error::PolicyValidationFailed(_) + | DidX509Error::FingerprintLengthMismatch(_, _, _) + | DidX509Error::ValidationFailed(_) => { + // Any of these errors indicate the chain doesn't match the DID + } + _ => panic!("Expected validation failure, got {:?}", error), + } +} + +#[test] +fn test_resolver_invalid_certificate_parsing() { + // Use invalid certificate data + let invalid_cert = generate_invalid_cert(); + let fingerprint_hex = hex::encode(&[0x00; 32]); // dummy fingerprint + + // Build a DID string + let did_string = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + fingerprint_hex + ); + + // Try to resolve with invalid certificate + let result = DidX509Resolver::resolve(&did_string, &[&invalid_cert]); + + // Should fail due to certificate parsing error or validation error + assert!( + result.is_err(), + "Resolution should fail with invalid certificate" + ); +} + +#[test] +fn test_resolver_mismatched_fingerprint() { + // Generate a certificate + let cert_der = generate_ec_cert(); + + // Use a wrong fingerprint hex (not matching the certificate) + let wrong_fingerprint_hex = hex::encode(&[0xFF; 32]); + let wrong_did_string = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + wrong_fingerprint_hex + ); + + let result = DidX509Resolver::resolve(&wrong_did_string, &[&cert_der]); + assert!(result.is_err(), "Should fail with fingerprint mismatch"); +} + +// Test base64url encoding coverage by testing different certificate types +#[test] +fn test_resolver_jwk_base64url_encoding() { + let cert_der = generate_ec_cert(); + + // Use the builder to create the DID (proper fingerprint calculation) + use did_x509::builder::DidX509Builder; + use did_x509::models::policy::DidX509Policy; + + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); + + assert!(result.is_ok(), "Resolution should succeed"); + let doc = result.unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + + // Verify EC coordinates are base64url encoded (no padding, no +/=) + if let (Some(x), Some(y)) = (jwk.get("x"), jwk.get("y")) { + assert!(!x.is_empty(), "x coordinate should not be empty"); + assert!(!y.is_empty(), "y coordinate should not be empty"); + + // Should not contain standard base64 chars or padding + assert!(!x.contains('='), "base64url should not contain padding"); + assert!(!x.contains('+'), "base64url should not contain '+'"); + assert!(!x.contains('/'), "base64url should not contain '/'"); + + assert!(!y.contains('='), "base64url should not contain padding"); + assert!(!y.contains('+'), "base64url should not contain '+'"); + assert!(!y.contains('/'), "base64url should not contain '/'"); + } +} diff --git a/native/rust/did/x509/tests/resolver_rsa_coverage.rs b/native/rust/did/x509/tests/resolver_rsa_coverage.rs index cfdc193b..58cf1c65 100644 --- a/native/rust/did/x509/tests/resolver_rsa_coverage.rs +++ b/native/rust/did/x509/tests/resolver_rsa_coverage.rs @@ -1,262 +1,263 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Test coverage for RSA key paths in DidX509Resolver. -//! -//! These tests use openssl to generate RSA and various EC certificates. - -use did_x509::builder::DidX509Builder; -use did_x509::models::policy::DidX509Policy; -use did_x509::resolver::DidX509Resolver; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::ec::{EcGroup, EcKey}; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::{X509Builder, X509NameBuilder}; - -/// Generate a self-signed RSA certificate for testing. -fn generate_rsa_cert() -> Vec { - // Generate RSA key pair - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - // Build certificate - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - - // Set serial number - let serial = BigNum::from_u32(1).unwrap(); - builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - // Set subject and issuer - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "Test RSA Certificate") - .unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - - // Set validity - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - - // Set public key - builder.set_pubkey(&pkey).unwrap(); - - // Add Code Signing EKU - let eku = openssl::x509::extension::ExtendedKeyUsage::new() - .code_signing() - .build() - .unwrap(); - builder.append_extension(eku).unwrap(); - - // Sign - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - - let cert = builder.build(); - cert.to_der().unwrap() -} - -#[test] -fn test_resolver_with_rsa_certificate() { - let cert_der = generate_rsa_cert(); - - // Build DID using the builder - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = - DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID from RSA cert"); - - // Resolve DID to document - let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); - - assert!( - result.is_ok(), - "Resolution should succeed: {:?}", - result.err() - ); - let doc = result.unwrap(); - - // Verify RSA JWK structure - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "RSA", "Key type should be RSA"); - assert!(jwk.contains_key("n"), "RSA JWK should have modulus 'n'"); - assert!(jwk.contains_key("e"), "RSA JWK should have exponent 'e'"); - - // Verify document structure - assert_eq!(doc.id, did_string); - assert_eq!(doc.verification_method.len(), 1); - assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); -} - -#[test] -fn test_resolver_rsa_jwk_base64url_encoding() { - let cert_der = generate_rsa_cert(); - - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let doc = DidX509Resolver::resolve(&did_string, &[&cert_der]).unwrap(); - - let jwk = &doc.verification_method[0].public_key_jwk; - - // Verify RSA parameters are properly base64url encoded - let n = jwk.get("n").expect("Should have modulus"); - let e = jwk.get("e").expect("Should have exponent"); - - // Base64url should not contain standard base64 chars or padding - assert!(!n.contains('='), "modulus should not have padding"); - assert!(!n.contains('+'), "modulus should not contain '+'"); - assert!(!n.contains('/'), "modulus should not contain '/'"); - - assert!(!e.contains('='), "exponent should not have padding"); - assert!(!e.contains('+'), "exponent should not contain '+'"); - assert!(!e.contains('/'), "exponent should not contain '/'"); -} - -#[test] -fn test_resolver_validation_fails_with_mismatched_chain() { - // Generate two different RSA certificates - let cert1 = generate_rsa_cert(); - let cert2 = generate_rsa_cert(); - - // Build DID for cert2 - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_for_cert2 = DidX509Builder::build_sha256(&cert2, &[policy]).unwrap(); - - // Try to resolve with cert1 (wrong chain) - let result = DidX509Resolver::resolve(&did_for_cert2, &[&cert1]); - - // Should fail because fingerprint doesn't match - assert!(result.is_err(), "Should fail with mismatched chain"); -} - -/// Generate a P-384 EC certificate for testing. -fn generate_p384_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - - let serial = BigNum::from_u32(3).unwrap(); - builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "Test P-384 Certificate") - .unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - - let eku = openssl::x509::extension::ExtendedKeyUsage::new() - .code_signing() - .build() - .unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha384()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Generate a P-521 EC certificate for testing. -fn generate_p521_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::SECP521R1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - - let serial = BigNum::from_u32(4).unwrap(); - builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "Test P-521 Certificate") - .unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - - let eku = openssl::x509::extension::ExtendedKeyUsage::new() - .code_signing() - .build() - .unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha512()).unwrap(); - builder.build().to_der().unwrap() -} - -#[test] -fn test_resolver_with_p384_certificate() { - let cert_der = generate_p384_cert(); - - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]) - .expect("Should build DID from P-384 cert"); - - let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); - - assert!( - result.is_ok(), - "Resolution should succeed: {:?}", - result.err() - ); - let doc = result.unwrap(); - - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "EC", "Key type should be EC"); - assert_eq!(jwk.get("crv").unwrap(), "P-384", "Curve should be P-384"); - assert!(jwk.contains_key("x"), "EC JWK should have x coordinate"); - assert!(jwk.contains_key("y"), "EC JWK should have y coordinate"); -} - -#[test] -fn test_resolver_with_p521_certificate() { - let cert_der = generate_p521_cert(); - - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]) - .expect("Should build DID from P-521 cert"); - - let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); - - assert!( - result.is_ok(), - "Resolution should succeed: {:?}", - result.err() - ); - let doc = result.unwrap(); - - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "EC", "Key type should be EC"); - assert_eq!(jwk.get("crv").unwrap(), "P-521", "Curve should be P-521"); - assert!(jwk.contains_key("x"), "EC JWK should have x coordinate"); - assert!(jwk.contains_key("y"), "EC JWK should have y coordinate"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Test coverage for RSA key paths in DidX509Resolver. +//! +//! These tests use openssl to generate RSA and various EC certificates. + +use did_x509::builder::DidX509Builder; +use did_x509::models::policy::DidX509Policy; +use did_x509::resolver::DidX509Resolver; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::ec::{EcGroup, EcKey}; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::{X509Builder, X509NameBuilder}; +use std::borrow::Cow; + +/// Generate a self-signed RSA certificate for testing. +fn generate_rsa_cert() -> Vec { + // Generate RSA key pair + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + // Build certificate + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + + // Set serial number + let serial = BigNum::from_u32(1).unwrap(); + builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + // Set subject and issuer + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "Test RSA Certificate") + .unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + + // Set validity + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + + // Set public key + builder.set_pubkey(&pkey).unwrap(); + + // Add Code Signing EKU + let eku = openssl::x509::extension::ExtendedKeyUsage::new() + .code_signing() + .build() + .unwrap(); + builder.append_extension(eku).unwrap(); + + // Sign + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + + let cert = builder.build(); + cert.to_der().unwrap() +} + +#[test] +fn test_resolver_with_rsa_certificate() { + let cert_der = generate_rsa_cert(); + + // Build DID using the builder + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = + DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID from RSA cert"); + + // Resolve DID to document + let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); + + assert!( + result.is_ok(), + "Resolution should succeed: {:?}", + result.err() + ); + let doc = result.unwrap(); + + // Verify RSA JWK structure + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "RSA", "Key type should be RSA"); + assert!(jwk.contains_key("n"), "RSA JWK should have modulus 'n'"); + assert!(jwk.contains_key("e"), "RSA JWK should have exponent 'e'"); + + // Verify document structure + assert_eq!(doc.id, did_string); + assert_eq!(doc.verification_method.len(), 1); + assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); +} + +#[test] +fn test_resolver_rsa_jwk_base64url_encoding() { + let cert_der = generate_rsa_cert(); + + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let doc = DidX509Resolver::resolve(&did_string, &[&cert_der]).unwrap(); + + let jwk = &doc.verification_method[0].public_key_jwk; + + // Verify RSA parameters are properly base64url encoded + let n = jwk.get("n").expect("Should have modulus"); + let e = jwk.get("e").expect("Should have exponent"); + + // Base64url should not contain standard base64 chars or padding + assert!(!n.contains('='), "modulus should not have padding"); + assert!(!n.contains('+'), "modulus should not contain '+'"); + assert!(!n.contains('/'), "modulus should not contain '/'"); + + assert!(!e.contains('='), "exponent should not have padding"); + assert!(!e.contains('+'), "exponent should not contain '+'"); + assert!(!e.contains('/'), "exponent should not contain '/'"); +} + +#[test] +fn test_resolver_validation_fails_with_mismatched_chain() { + // Generate two different RSA certificates + let cert1 = generate_rsa_cert(); + let cert2 = generate_rsa_cert(); + + // Build DID for cert2 + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_for_cert2 = DidX509Builder::build_sha256(&cert2, &[policy]).unwrap(); + + // Try to resolve with cert1 (wrong chain) + let result = DidX509Resolver::resolve(&did_for_cert2, &[&cert1]); + + // Should fail because fingerprint doesn't match + assert!(result.is_err(), "Should fail with mismatched chain"); +} + +/// Generate a P-384 EC certificate for testing. +fn generate_p384_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + + let serial = BigNum::from_u32(3).unwrap(); + builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "Test P-384 Certificate") + .unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + + let eku = openssl::x509::extension::ExtendedKeyUsage::new() + .code_signing() + .build() + .unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha384()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Generate a P-521 EC certificate for testing. +fn generate_p521_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::SECP521R1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + + let serial = BigNum::from_u32(4).unwrap(); + builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "Test P-521 Certificate") + .unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + + let eku = openssl::x509::extension::ExtendedKeyUsage::new() + .code_signing() + .build() + .unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha512()).unwrap(); + builder.build().to_der().unwrap() +} + +#[test] +fn test_resolver_with_p384_certificate() { + let cert_der = generate_p384_cert(); + + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]) + .expect("Should build DID from P-384 cert"); + + let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); + + assert!( + result.is_ok(), + "Resolution should succeed: {:?}", + result.err() + ); + let doc = result.unwrap(); + + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "EC", "Key type should be EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-384", "Curve should be P-384"); + assert!(jwk.contains_key("x"), "EC JWK should have x coordinate"); + assert!(jwk.contains_key("y"), "EC JWK should have y coordinate"); +} + +#[test] +fn test_resolver_with_p521_certificate() { + let cert_der = generate_p521_cert(); + + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]) + .expect("Should build DID from P-521 cert"); + + let result = DidX509Resolver::resolve(&did_string, &[&cert_der]); + + assert!( + result.is_ok(), + "Resolution should succeed: {:?}", + result.err() + ); + let doc = result.unwrap(); + + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "EC", "Key type should be EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-521", "Curve should be P-521"); + assert!(jwk.contains_key("x"), "EC JWK should have x coordinate"); + assert!(jwk.contains_key("y"), "EC JWK should have y coordinate"); +} diff --git a/native/rust/did/x509/tests/surgical_did_coverage.rs b/native/rust/did/x509/tests/surgical_did_coverage.rs index 2617780c..98feb183 100644 --- a/native/rust/did/x509/tests/surgical_did_coverage.rs +++ b/native/rust/did/x509/tests/surgical_did_coverage.rs @@ -1,1394 +1,1395 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Surgical coverage tests for did_x509 crate — targets specific uncovered lines. -//! -//! Covers: -//! - resolver.rs: resolve(), public_key_to_jwk(), ec_to_jwk() error paths, rsa_to_jwk() -//! - policy_validators.rs: validate_subject mismatch paths, validate_san, validate_fulcio_issuer -//! - parser.rs: unknown policy type, malformed SAN, fulcio-issuer parsing, base64 edge cases -//! - x509_extensions.rs: custom EKU OIDs, is_ca_certificate, extract_fulcio_issuer -//! - san_parser.rs: DirectoryName SAN type -//! - validator.rs: validation with policy failures, empty chain -//! - builder.rs: build_from_chain_with_eku, encode_policy for SAN/FulcioIssuer/Subject -//! - did_document.rs: to_json non-indented - -use did_x509::builder::DidX509Builder; -use did_x509::did_document::DidDocument; -use did_x509::error::DidX509Error; -use did_x509::models::policy::{DidX509Policy, SanType}; -use did_x509::models::validation_result::DidX509ValidationResult; -use did_x509::parsing::DidX509Parser; -use did_x509::policy_validators; -use did_x509::resolver::DidX509Resolver; -use did_x509::validator::DidX509Validator; -use did_x509::x509_extensions; - -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::ec::{EcGroup, EcKey}; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, SubjectAlternativeName}; -use openssl::x509::{X509Builder, X509NameBuilder}; -use sha2::{Digest, Sha256}; - -// ============================================================================ -// Helpers: certificate generation via openssl -// ============================================================================ - -/// Build a self-signed EC (P-256) leaf certificate with code-signing EKU and a Subject CN. -fn build_ec_leaf_cert_with_cn(cn: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", cn).unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed RSA leaf certificate with code-signing EKU. -fn build_rsa_leaf_cert() -> Vec { - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "RSA Test Cert").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(2).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN DNS names. -fn build_ec_cert_with_san_dns(dns: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "SAN Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(3).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .dns(dns) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN email. -fn build_ec_cert_with_san_email(email: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Email Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(4).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .email(email) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN URI. -fn build_ec_cert_with_san_uri(uri: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "URI Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(5).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .uri(uri) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with BasicConstraints (CA:TRUE) and no EKU. -fn build_ca_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Test CA").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(10).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let bc = BasicConstraints::new().critical().ca().build().unwrap(); - builder.append_extension(bc).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with NO extensions at all. -fn build_bare_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Bare Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(20).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with Subject containing O and OU attributes. -fn build_ec_cert_with_subject(cn: &str, org: &str, ou: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", cn).unwrap(); - name.append_entry_by_text("O", org).unwrap(); - name.append_entry_by_text("OU", ou).unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(6).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Helper: compute sha256 fingerprint, produce base64url-encoded string. -fn sha256_fingerprint_b64url(data: &[u8]) -> String { - let hash = Sha256::digest(data); - base64url_encode(&hash) -} - -fn base64url_encode(data: &[u8]) -> String { - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - let mut out = String::with_capacity((data.len() + 2) / 3 * 4); - let mut i = 0; - while i + 2 < data.len() { - let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8 | data[i + 2] as u32; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); - out.push(ALPHABET[(n & 0x3F) as usize] as char); - i += 3; - } - let rem = data.len() - i; - if rem == 1 { - let n = (data[i] as u32) << 16; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - } else if rem == 2 { - let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); - } - out -} - -/// Helper: build a DID string manually for a self-signed cert with the given policies. -fn make_did(cert_der: &[u8], policy_suffix: &str) -> String { - let fp = sha256_fingerprint_b64url(cert_der); - format!("did:x509:0:sha256:{}::{}", fp, policy_suffix) -} - -// ============================================================================ -// resolver.rs — resolve() + public_key_to_jwk() + ec_to_jwk() + rsa_to_jwk() -// Lines 28-31, 81-86, 113-117, 143, 150, 157, 166-170, 191-201 -// ============================================================================ - -#[test] -fn resolver_ec_cert_produces_did_document() { - // Exercises resolve() happy path → lines 72-98 including 81-86 (JWK EC) - let cert = build_ec_leaf_cert_with_cn("Resolve EC Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_ok(), "EC resolve failed: {:?}", result.err()); - let doc = result.unwrap(); - assert_eq!(doc.id, did); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "EC"); - assert!(jwk.contains_key("x")); - assert!(jwk.contains_key("y")); - assert!(jwk.contains_key("crv")); -} - -#[test] -fn resolver_rsa_cert_produces_did_document() { - // Exercises rsa_to_jwk() → lines 121-134 (RSA JWK: kty, n, e) - let cert = build_rsa_leaf_cert(); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_ok(), "RSA resolve failed: {:?}", result.err()); - let doc = result.unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "RSA"); - assert!(jwk.contains_key("n")); - assert!(jwk.contains_key("e")); -} - -#[test] -fn resolver_validation_fails_returns_error() { - // Exercises resolve() line 74-75: validation fails → PolicyValidationFailed - let cert = build_ec_leaf_cert_with_cn("Wrong EKU"); - // Use an EKU OID the cert doesn't have - let did = make_did(&cert, "eku:1.2.3.4.5.6.7.8.9"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(_) => {} - other => panic!("Expected PolicyValidationFailed, got: {:?}", other), - } -} - -#[test] -fn resolver_invalid_der_returns_cert_parse_error() { - // Exercises resolve() lines 80-81: CertificateParseError path - // We need a DID that validates against a chain, but then the leaf parse fails. - // Actually this path requires validate() to succeed but from_der to fail, - // which is hard since validate also parses. Instead test with a DID that - // would resolve but parse fails at step 2. - // However, the real uncovered lines 80-81 are about the .map_err on from_der. - // Since validate() would fail first on bad DER, let's verify the error type - // from the validate step at least. - let bad_der = vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF]; - let did = make_did(&bad_der, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&bad_der]); - assert!(result.is_err()); -} - -// ============================================================================ -// policy_validators.rs — validate_eku, validate_subject, validate_san, validate_fulcio_issuer -// Lines 66, 88-93, 130-148 -// ============================================================================ - -#[test] -fn validate_eku_missing_required_oid() { - // Exercises validate_eku lines 22-27: required OID not present - let cert_der = build_ec_leaf_cert_with_cn("EKU Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_eku(&cert, &["9.9.9.9.9".to_string()]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("9.9.9.9.9")); -} - -#[test] -fn validate_eku_no_eku_extension() { - // Exercises validate_eku lines 15-18: no EKU extension at all - let cert_der = build_bare_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string()]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Extended Key Usage")); -} - -#[test] -fn validate_subject_matching() { - // Exercises validate_subject happy path and value comparison lines 56-71 - let cert_der = build_ec_cert_with_subject("TestCN", "TestOrg", "TestOU"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("CN".to_string(), "TestCN".to_string())]); - assert!(result.is_ok()); -} - -#[test] -fn validate_subject_value_mismatch() { - // Exercises validate_subject lines 80-86: attribute found but value doesn't match - let cert_der = build_ec_cert_with_subject("ActualCN", "ActualOrg", "ActualOU"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("CN".to_string(), "WrongCN".to_string())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("value mismatch")); -} - -#[test] -fn validate_subject_attribute_not_found() { - // Exercises validate_subject lines 74-77: attribute not in cert subject - let cert_der = build_ec_leaf_cert_with_cn("OnlyCN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("O".to_string(), "SomeOrg".to_string())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("not found")); -} - -#[test] -fn validate_subject_unknown_attribute_label() { - // Exercises validate_subject lines 47-50: unknown attribute label → error - let cert_der = build_ec_leaf_cert_with_cn("Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("BOGUS".to_string(), "value".to_string())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Unknown attribute")); -} - -#[test] -fn validate_subject_empty_attrs() { - // Exercises validate_subject lines 35-38: empty attrs list - let cert_der = build_ec_leaf_cert_with_cn("Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_subject(&cert, &[]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("at least one attribute")); -} - -#[test] -fn validate_san_dns_found() { - // Exercises validate_san lines 108-110: SAN found - let cert_der = build_ec_cert_with_san_dns("example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_ok()); -} - -#[test] -fn validate_san_not_found() { - // Exercises validate_san lines 112-117: SAN type+value not found - let cert_der = build_ec_cert_with_san_dns("example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "wrong.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("not found")); -} - -#[test] -fn validate_san_no_sans_at_all() { - // Exercises validate_san lines 101-105: cert has no SANs - let cert_der = build_ec_leaf_cert_with_cn("NoSAN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "any.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Subject Alternative Names")); -} - -#[test] -fn validate_san_email_type() { - // Exercises SAN email path in san_parser - let cert_der = build_ec_cert_with_san_email("user@example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Email, "user@example.com"); - assert!(result.is_ok()); -} - -#[test] -fn validate_san_uri_type() { - // Exercises SAN URI path in san_parser - let cert_der = build_ec_cert_with_san_uri("https://example.com/id"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Uri, "https://example.com/id"); - assert!(result.is_ok()); -} - -#[test] -fn validate_fulcio_issuer_no_extension() { - // Exercises validate_fulcio_issuer lines 126-130: no Fulcio issuer ext - let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_fulcio_issuer(&cert, "accounts.google.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Fulcio issuer extension")); -} - -// ============================================================================ -// x509_extensions.rs — extract_extended_key_usage, is_ca_certificate, extract_fulcio_issuer -// Lines 24-27, 46, 58-60 -// ============================================================================ - -#[test] -fn extract_eku_returns_code_signing_oid() { - let cert_der = build_ec_leaf_cert_with_cn("EKU Extract"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let ekus = x509_extensions::extract_extended_key_usage(&cert); - assert!(ekus.contains(&"1.3.6.1.5.5.7.3.3".to_string())); -} - -#[test] -fn extract_eku_empty_for_no_eku_cert() { - let cert_der = build_bare_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let ekus = x509_extensions::extract_extended_key_usage(&cert); - assert!(ekus.is_empty()); -} - -#[test] -fn is_ca_certificate_true_for_ca() { - // Exercises is_ca_certificate lines 42-49: BasicConstraints CA:TRUE → line 46 - let cert_der = build_ca_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(x509_extensions::is_ca_certificate(&cert)); -} - -#[test] -fn is_ca_certificate_false_for_leaf() { - let cert_der = build_ec_leaf_cert_with_cn("Leaf"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(!x509_extensions::is_ca_certificate(&cert)); -} - -#[test] -fn extract_fulcio_issuer_returns_none_when_absent() { - // Exercises extract_fulcio_issuer lines 53-63: no matching ext → None - let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(x509_extensions::extract_fulcio_issuer(&cert).is_none()); -} - -#[test] -fn extract_eku_oids_returns_oids() { - let cert_der = build_ec_leaf_cert_with_cn("EKU OIDs"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let oids = x509_extensions::extract_eku_oids(&cert).unwrap(); - assert!(!oids.is_empty()); -} - -// ============================================================================ -// validator.rs — validate() with policy failures, empty chain -// Lines 38-40, 67-68, 88-91 -// ============================================================================ - -#[test] -fn validator_empty_chain_returns_error() { - // Exercises validate() line 28-29: empty chain - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let chain: &[&[u8]] = &[]; - let result = DidX509Validator::validate(&did, chain); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidChain(msg) => assert!(msg.contains("Empty")), - other => panic!("Expected InvalidChain, got: {:?}", other), - } -} - -#[test] -fn validator_fingerprint_mismatch_returns_no_ca_match() { - // Exercises find_ca_by_fingerprint → NoCaMatch (line 73) - let cert = build_ec_leaf_cert_with_cn("Test"); - // Use a fingerprint from a different cert - let other_cert = build_ec_leaf_cert_with_cn("Other"); - let did = make_did(&other_cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::NoCaMatch => {} - other => panic!("Expected NoCaMatch, got: {:?}", other), - } -} - -#[test] -fn validator_policy_failure_produces_invalid_result() { - // Exercises validate() lines 42-53: policy validation fails → invalid result - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:9.9.9.9.9"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - let val_result = result.unwrap(); - assert!(!val_result.is_valid); - assert!(!val_result.errors.is_empty()); -} - -#[test] -fn validator_cert_parse_error_for_bad_der() { - // Exercises validate() lines 37-38: X509Certificate::from_der fails - // We need a chain where the first cert fails to parse but CA fingerprint matches. - // This is tricky: the fingerprint check iterates ALL certs including bad ones. - // Actually find_ca_by_fingerprint doesn't parse certs, just hashes DER bytes. - // So we can have a bad leaf + good CA in the chain. - let bad_leaf: Vec = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; // Not a valid cert but valid DER tag - let ca_cert = build_ec_leaf_cert_with_cn("CA for bad leaf"); - - // The DID fingerprint matches the CA cert (second in chain) - let did = make_did(&ca_cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Validator::validate(&did, &[&bad_leaf, &ca_cert]); - // Should fail at leaf cert parsing - assert!(result.is_err()); -} - -#[test] -fn validator_subject_policy_integration() { - // Exercises validate_policy Subject match arm → line 82-83 - let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); - let did = make_did(&cert, "subject:CN:MyCN"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_san_policy_integration() { - // Exercises validate_policy San match arm → lines 85-86 - let cert = build_ec_cert_with_san_dns("test.example.com"); - let did = make_did(&cert, "san:dns:test.example.com"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_san_policy_failure() { - // Exercises validate_policy San failure → errors collected - let cert = build_ec_cert_with_san_dns("test.example.com"); - let did = make_did(&cert, "san:dns:wrong.example.com"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - let val_result = result.unwrap(); - assert!(!val_result.is_valid); -} - -#[test] -fn validator_unsupported_hash_algorithm() { - // Exercises find_ca_by_fingerprint line 67: unsupported hash - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let _did = format!("did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); - // This should work; now test with an algorithm that gets parsed but not supported - // We need to craft a DID with e.g. "sha999" but the parser won't accept it. - // So let's test the sha384 and sha512 paths through the validator. -} - -// ============================================================================ -// builder.rs — build_from_chain_with_eku, encode_policy for SAN/Subject/FulcioIssuer -// Lines 74-76, 114, 159-160 -// ============================================================================ - -#[test] -fn builder_encode_san_policy() { - // Exercises encode_policy SAN match arm → lines 154-161 - let cert = build_ec_cert_with_san_dns("example.com"); - let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:dns:example.com")); -} - -#[test] -fn builder_encode_san_email_policy() { - let cert = build_ec_cert_with_san_email("user@example.com"); - let policy = DidX509Policy::San(SanType::Email, "user@example.com".to_string()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:email:")); -} - -#[test] -fn builder_encode_san_uri_policy() { - let cert = build_ec_cert_with_san_uri("https://example.com/id"); - let policy = DidX509Policy::San(SanType::Uri, "https://example.com/id".to_string()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:uri:")); -} - -#[test] -fn builder_encode_san_dn_policy() { - // Exercises SAN Dn match arm → line 159 - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::San(SanType::Dn, "CN=Test".to_string()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:dn:")); -} - -#[test] -fn builder_encode_fulcio_issuer_policy() { - // Exercises encode_policy FulcioIssuer match arm → lines 163-164 - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("fulcio-issuer:accounts.google.com")); -} - -#[test] -fn builder_encode_subject_policy() { - // Exercises encode_policy Subject match arm → lines 145-153 - let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); - let policy = DidX509Policy::Subject(vec![ - ("CN".to_string(), "MyCN".to_string()), - ("O".to_string(), "MyOrg".to_string()), - ]); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("subject:CN:MyCN:O:MyOrg")); -} - -#[test] -fn builder_build_from_chain_with_eku() { - // Exercises build_from_chain_with_eku → lines 103-121 - let cert = build_ec_leaf_cert_with_cn("Chain EKU"); - let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); - assert!(result.is_ok()); - let did_str = result.unwrap(); - assert!(did_str.contains("eku:")); -} - -#[test] -fn builder_build_from_chain_with_eku_empty_chain() { - // Exercises build_from_chain_with_eku line 106-108: empty chain - let chain: &[&[u8]] = &[]; - let result = DidX509Builder::build_from_chain_with_eku(chain); - assert!(result.is_err()); -} - -#[test] -fn builder_build_from_chain_with_eku_no_eku() { - // Exercises build_from_chain_with_eku lines 114-116: no EKU found - let cert = build_bare_cert(); - let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); - // This should return an error or empty EKU list - // extract_eku_oids returns Ok(empty_vec), then line 115 checks is_empty - assert!(result.is_err()); -} - -#[test] -fn builder_build_from_chain_empty() { - // Exercises build_from_chain line 94-96: empty chain - let chain: &[&[u8]] = &[]; - let result = DidX509Builder::build_from_chain(chain, &[]); - assert!(result.is_err()); -} - -#[test] -fn builder_unsupported_hash_algorithm() { - // Exercises compute_fingerprint line 128: unsupported hash - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let result = DidX509Builder::build(&cert, &[policy], "sha999"); - assert!(result.is_err()); -} - -#[test] -fn builder_sha384_hash() { - // Exercises compute_fingerprint sha384 path → line 126 - let cert = build_ec_leaf_cert_with_cn("SHA384 Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let result = DidX509Builder::build(&cert, &[policy], "sha384"); - assert!(result.is_ok()); -} - -#[test] -fn builder_sha512_hash() { - // Exercises compute_fingerprint sha512 path → line 127 - let cert = build_ec_leaf_cert_with_cn("SHA512 Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let result = DidX509Builder::build(&cert, &[policy], "sha512"); - assert!(result.is_ok()); -} - -// ============================================================================ -// did_document.rs — to_json() non-indented -// Line 59 -// ============================================================================ - -#[test] -fn did_document_to_json_non_indented() { - // Exercises to_json(false) → line 57 (serde_json::to_string) - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:test".to_string(), - verification_method: vec![], - assertion_method: vec![], - }; - let json = doc.to_json(false); - assert!(json.is_ok()); - let json_str = json.unwrap(); - assert!(!json_str.contains('\n')); -} - -#[test] -fn did_document_to_json_indented() { - // Exercises to_json(true) → line 55 (serde_json::to_string_pretty) - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:test".to_string(), - verification_method: vec![], - assertion_method: vec![], - }; - let json = doc.to_json(true); - assert!(json.is_ok()); - let json_str = json.unwrap(); - assert!(json_str.contains('\n')); -} - -// ============================================================================ -// parser.rs — edge cases -// Lines 35, 119, 127-129, 143, 166, 203-205, 224, 234, 259-260, 282, 286-287, 299 -// ============================================================================ - -#[test] -fn parser_unknown_policy_type() { - // Exercises parse_policy_value lines 199-204: unknown policy type - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::unknownpolicy:somevalue", fp); - let result = DidX509Parser::parse(&did); - // Unknown policy defaults to Eku([]) per line 203 - assert!(result.is_ok()); -} - -#[test] -fn parser_empty_fingerprint() { - // Exercises parser.rs line 118-119: empty fingerprint - let did = "did:x509:0:sha256:::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); -} - -#[test] -fn parser_wrong_fingerprint_length() { - // Exercises parser.rs lines 130-136: fingerprint length mismatch - let did = "did:x509:0:sha256:AAAA::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::FingerprintLengthMismatch(_, _, _) => {} - other => panic!("Expected FingerprintLengthMismatch, got: {:?}", other), - } -} - -#[test] -fn parser_invalid_base64url_chars() { - // Exercises parser.rs lines 138-139: invalid base64url characters - // SHA-256 fingerprint must be exactly 43 base64url chars - let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); -} - -#[test] -fn parser_unsupported_version() { - // Exercises parser.rs lines 102-107: unsupported version - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:9:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::UnsupportedVersion(_, _) => {} - other => panic!("Expected UnsupportedVersion, got: {:?}", other), - } -} - -#[test] -fn parser_unsupported_hash_algorithm() { - // Exercises parser.rs lines 110-114: unsupported hash algorithm - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:md5:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::UnsupportedHashAlgorithm(_) => {} - other => panic!("Expected UnsupportedHashAlgorithm, got: {:?}", other), - } -} - -#[test] -fn parser_empty_policy_segment() { - // Exercises parser.rs lines 149-151: empty policy at position - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}:: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_policy_no_colon() { - // Exercises parser.rs lines 155-158: policy without colon - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::nocolon", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidPolicyFormat(_) => {} - other => panic!("Expected InvalidPolicyFormat, got: {:?}", other), - } -} - -#[test] -fn parser_empty_policy_name() { - // Exercises parser.rs line 165-167: empty policy name (colon at start) - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}:::value", fp); - let result = DidX509Parser::parse(&did); - // This has :: followed by : → first splits on :: giving empty segment handled above - // or parsing of ":value" where colon_idx == 0 - assert!(result.is_err()); -} - -#[test] -fn parser_empty_policy_value() { - // Exercises parser.rs lines 169-171: empty policy value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::eku: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_san_policy_missing_value() { - // Exercises parse_san_policy lines 244-248: missing colon in SAN value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:dnsnocolon", fp); - let result = DidX509Parser::parse(&did); - // "dnsnocolon" has no colon → InvalidSanPolicyFormat - assert!(result.is_err()); -} - -#[test] -fn parser_san_policy_invalid_type() { - // Exercises parse_san_policy lines 255-256: invalid SAN type - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:badtype:value", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidSanType(_) => {} - other => panic!("Expected InvalidSanType, got: {:?}", other), - } -} - -#[test] -fn parser_eku_invalid_oid() { - // Exercises parse_eku_policy line 271: invalid OID format - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::eku:not-an-oid", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidEkuOid => {} - other => panic!("Expected InvalidEkuOid, got: {:?}", other), - } -} - -#[test] -fn parser_fulcio_issuer_empty() { - // Exercises parse_fulcio_issuer_policy lines 281-283: empty issuer - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::fulcio-issuer: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_fulcio_issuer_valid() { - // Exercises parse_fulcio_issuer_policy lines 286-288: happy path - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!( - "did:x509:0:sha256:{}::fulcio-issuer:accounts.google.com", - fp - ); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); - let parsed = result.unwrap(); - assert!(parsed.has_fulcio_issuer_policy()); -} - -#[test] -fn parser_subject_policy_odd_components() { - // Exercises parse_subject_policy line 213: odd number of components - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::subject:CN:val:extra", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidSubjectPolicyComponents => {} - other => panic!("Expected InvalidSubjectPolicyComponents, got: {:?}", other), - } -} - -#[test] -fn parser_subject_policy_empty_key() { - // Exercises parse_subject_policy line 224: empty key - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - // "subject::val" where first part splits into ["", "val"] - // Actually ":val" as the policy_value → splits on ':' → ["", "val"] - let did = format!("did:x509:0:sha256:{}::subject::val", fp); - let result = DidX509Parser::parse(&did); - // The :: in "subject::val" would be split as major_parts separator - // Let's use percent-encoding approach instead - // Actually "subject" followed by ":val" → policy_value is "val" which has 1 part → odd - assert!(result.is_err()); -} - -#[test] -fn parser_subject_policy_duplicate_key() { - // Exercises parse_subject_policy lines 228-230: duplicate key - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::subject:CN:val1:CN:val2", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::DuplicateSubjectPolicyKey(_) => {} - other => panic!("Expected DuplicateSubjectPolicyKey, got: {:?}", other), - } -} - -#[test] -fn parser_sha384_fingerprint() { - // Exercises parser sha384 path → line 124 expected_length = 64 - use sha2::Sha384; - let cert = build_ec_leaf_cert_with_cn("SHA384"); - let hash = Sha384::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_sha512_fingerprint() { - // Exercises parser sha512 path → line 125-126 expected_length = 86 - use sha2::Sha512; - let cert = build_ec_leaf_cert_with_cn("SHA512"); - let hash = Sha512::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_try_parse_returns_none_on_failure() { - let result = DidX509Parser::try_parse("not a valid DID"); - assert!(result.is_none()); -} - -#[test] -fn parser_try_parse_returns_some_on_success() { - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Parser::try_parse(&did); - assert!(result.is_some()); -} - -#[test] -fn parser_san_percent_encoded_value() { - // Exercises parse_san_policy line 259: percent_decode on SAN value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:email:user%40example.com", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_invalid_prefix() { - // Exercises parser.rs lines 77-79: wrong prefix - let result = DidX509Parser::parse("did:wrong:0:sha256:AAAA::eku:1.2.3"); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidPrefix(_) => {} - other => panic!("Expected InvalidPrefix, got: {:?}", other), - } -} - -#[test] -fn parser_missing_policies() { - // Exercises parser.rs lines 83-85: no :: separator - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::MissingPolicies => {} - other => panic!("Expected MissingPolicies, got: {:?}", other), - } -} - -#[test] -fn parser_wrong_component_count() { - // Exercises parser.rs lines 91-95: prefix has wrong number of components - let result = DidX509Parser::parse("did:x509:0:sha256::eku:1.2.3"); - assert!(result.is_err()); -} - -#[test] -fn parser_empty_did() { - let result = DidX509Parser::parse(""); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::EmptyDid => {} - other => panic!("Expected EmptyDid, got: {:?}", other), - } -} - -#[test] -fn parser_whitespace_only_did() { - let result = DidX509Parser::parse(" "); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::EmptyDid => {} - other => panic!("Expected EmptyDid, got: {:?}", other), - } -} - -// ============================================================================ -// san_parser.rs — edge cases for DirectoryName (lines 23-26) -// ============================================================================ - -#[test] -fn san_parser_parse_sans_from_cert_with_dns() { - let cert_der = build_ec_cert_with_san_dns("test.example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); - assert!(!sans.is_empty()); - assert_eq!(sans[0].san_type, SanType::Dns); - assert_eq!(sans[0].value, "test.example.com"); -} - -#[test] -fn san_parser_parse_sans_from_cert_no_san() { - let cert_der = build_ec_leaf_cert_with_cn("No SAN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); - assert!(sans.is_empty()); -} - -// ============================================================================ -// Validation result model tests -// ============================================================================ - -#[test] -fn validation_result_add_error() { - let mut result = DidX509ValidationResult::valid(0); - assert!(result.is_valid); - result.add_error("test error".to_string()); - assert!(!result.is_valid); - assert_eq!(result.errors.len(), 1); -} - -#[test] -fn validation_result_invalid_single() { - let result = DidX509ValidationResult::invalid("single error".to_string()); - assert!(!result.is_valid); - assert!(result.matched_ca_index.is_none()); - assert_eq!(result.errors.len(), 1); -} - -// ============================================================================ -// Resolver with sha384 and sha512 hash algorithms via validator -// ============================================================================ - -#[test] -fn validator_sha384_fingerprint_matching() { - use sha2::Sha384; - let cert = build_ec_leaf_cert_with_cn("SHA384 Validator"); - let hash = Sha384::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_sha512_fingerprint_matching() { - use sha2::Sha512; - let cert = build_ec_leaf_cert_with_cn("SHA512 Validator"); - let hash = Sha512::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -// ============================================================================ -// Error Display coverage -// ============================================================================ - -#[test] -fn error_display_coverage() { - // Exercise Display for several error variants - let errors: Vec = vec![ - DidX509Error::EmptyDid, - DidX509Error::InvalidPrefix("test".to_string()), - DidX509Error::MissingPolicies, - DidX509Error::InvalidFormat("fmt".to_string()), - DidX509Error::UnsupportedVersion("1".to_string(), "0".to_string()), - DidX509Error::UnsupportedHashAlgorithm("md5".to_string()), - DidX509Error::EmptyFingerprint, - DidX509Error::FingerprintLengthMismatch("sha256".to_string(), 43, 10), - DidX509Error::InvalidFingerprintChars, - DidX509Error::EmptyPolicy(1), - DidX509Error::InvalidPolicyFormat("bad".to_string()), - DidX509Error::EmptyPolicyName, - DidX509Error::EmptyPolicyValue, - DidX509Error::InvalidSubjectPolicyComponents, - DidX509Error::EmptySubjectPolicyKey, - DidX509Error::DuplicateSubjectPolicyKey("CN".to_string()), - DidX509Error::InvalidSanPolicyFormat("bad".to_string()), - DidX509Error::InvalidSanType("bad".to_string()), - DidX509Error::InvalidEkuOid, - DidX509Error::EmptyFulcioIssuer, - DidX509Error::PercentDecodingError("bad".to_string()), - DidX509Error::InvalidHexCharacter('G'), - DidX509Error::InvalidChain("bad".to_string()), - DidX509Error::CertificateParseError("bad".to_string()), - DidX509Error::PolicyValidationFailed("bad".to_string()), - DidX509Error::NoCaMatch, - DidX509Error::ValidationFailed("bad".to_string()), - ]; - for err in &errors { - let msg = format!("{}", err); - assert!(!msg.is_empty()); - } -} - -// ============================================================================ -// base64url encoding edge cases in builder.rs (lines 26-37 of builder.rs) -// These are actually in the inline base64_encode function -// ============================================================================ - -#[test] -fn builder_build_sha256_shorthand() { - let cert = build_ec_leaf_cert_with_cn("Shorthand"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let result = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(result.is_ok()); -} - -#[test] -fn builder_build_from_chain_last_cert_as_ca() { - // Exercises build_from_chain line 97-98: uses last cert as CA - let leaf = build_ec_leaf_cert_with_cn("Leaf"); - let ca = build_ca_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let result = DidX509Builder::build_from_chain(&[&leaf, &ca], &[policy]); - assert!(result.is_ok()); -} - -// ============================================================================ -// SanType::as_str() for all variants -// ============================================================================ - -#[test] -fn san_type_as_str_all_variants() { - assert_eq!(SanType::Email.as_str(), "email"); - assert_eq!(SanType::Dns.as_str(), "dns"); - assert_eq!(SanType::Uri.as_str(), "uri"); - assert_eq!(SanType::Dn.as_str(), "dn"); -} - -#[test] -fn san_type_from_str_all_variants() { - assert_eq!(SanType::from_str("email"), Some(SanType::Email)); - assert_eq!(SanType::from_str("dns"), Some(SanType::Dns)); - assert_eq!(SanType::from_str("uri"), Some(SanType::Uri)); - assert_eq!(SanType::from_str("dn"), Some(SanType::Dn)); - assert_eq!(SanType::from_str("bad"), None); -} - -// ============================================================================ -// Resolver round-trip: build DID then resolve to verify EC JWK -// ============================================================================ - -#[test] -fn resolver_roundtrip_build_then_resolve_ec() { - let cert = build_ec_leaf_cert_with_cn("Roundtrip EC"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); - let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); - assert_eq!(doc.verification_method.len(), 1); - assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); -} - -#[test] -fn resolver_roundtrip_build_then_resolve_rsa() { - let cert = build_rsa_leaf_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); - let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); - assert_eq!(doc.verification_method.len(), 1); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "RSA"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Surgical coverage tests for did_x509 crate — targets specific uncovered lines. +//! +//! Covers: +//! - resolver.rs: resolve(), public_key_to_jwk(), ec_to_jwk() error paths, rsa_to_jwk() +//! - policy_validators.rs: validate_subject mismatch paths, validate_san, validate_fulcio_issuer +//! - parser.rs: unknown policy type, malformed SAN, fulcio-issuer parsing, base64 edge cases +//! - x509_extensions.rs: custom EKU OIDs, is_ca_certificate, extract_fulcio_issuer +//! - san_parser.rs: DirectoryName SAN type +//! - validator.rs: validation with policy failures, empty chain +//! - builder.rs: build_from_chain_with_eku, encode_policy for SAN/FulcioIssuer/Subject +//! - did_document.rs: to_json non-indented + +use did_x509::builder::DidX509Builder; +use did_x509::did_document::DidDocument; +use did_x509::error::DidX509Error; +use did_x509::models::policy::{DidX509Policy, SanType}; +use did_x509::models::validation_result::DidX509ValidationResult; +use did_x509::parsing::DidX509Parser; +use did_x509::policy_validators; +use did_x509::resolver::DidX509Resolver; +use did_x509::validator::DidX509Validator; +use did_x509::x509_extensions; + +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::ec::{EcGroup, EcKey}; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, SubjectAlternativeName}; +use openssl::x509::{X509Builder, X509NameBuilder}; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +// ============================================================================ +// Helpers: certificate generation via openssl +// ============================================================================ + +/// Build a self-signed EC (P-256) leaf certificate with code-signing EKU and a Subject CN. +fn build_ec_leaf_cert_with_cn(cn: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", cn).unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed RSA leaf certificate with code-signing EKU. +fn build_rsa_leaf_cert() -> Vec { + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "RSA Test Cert").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(2).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN DNS names. +fn build_ec_cert_with_san_dns(dns: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "SAN Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(3).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .dns(dns) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN email. +fn build_ec_cert_with_san_email(email: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Email Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(4).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .email(email) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN URI. +fn build_ec_cert_with_san_uri(uri: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "URI Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(5).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .uri(uri) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with BasicConstraints (CA:TRUE) and no EKU. +fn build_ca_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Test CA").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(10).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let bc = BasicConstraints::new().critical().ca().build().unwrap(); + builder.append_extension(bc).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with NO extensions at all. +fn build_bare_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Bare Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(20).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with Subject containing O and OU attributes. +fn build_ec_cert_with_subject(cn: &str, org: &str, ou: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", cn).unwrap(); + name.append_entry_by_text("O", org).unwrap(); + name.append_entry_by_text("OU", ou).unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(6).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Helper: compute sha256 fingerprint, produce base64url-encoded string. +fn sha256_fingerprint_b64url(data: &[u8]) -> String { + let hash = Sha256::digest(data); + base64url_encode(&hash) +} + +fn base64url_encode(data: &[u8]) -> String { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + let mut out = String::with_capacity((data.len() + 2) / 3 * 4); + let mut i = 0; + while i + 2 < data.len() { + let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8 | data[i + 2] as u32; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); + out.push(ALPHABET[(n & 0x3F) as usize] as char); + i += 3; + } + let rem = data.len() - i; + if rem == 1 { + let n = (data[i] as u32) << 16; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + } else if rem == 2 { + let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); + } + out +} + +/// Helper: build a DID string manually for a self-signed cert with the given policies. +fn make_did(cert_der: &[u8], policy_suffix: &str) -> String { + let fp = sha256_fingerprint_b64url(cert_der); + format!("did:x509:0:sha256:{}::{}", fp, policy_suffix) +} + +// ============================================================================ +// resolver.rs — resolve() + public_key_to_jwk() + ec_to_jwk() + rsa_to_jwk() +// Lines 28-31, 81-86, 113-117, 143, 150, 157, 166-170, 191-201 +// ============================================================================ + +#[test] +fn resolver_ec_cert_produces_did_document() { + // Exercises resolve() happy path → lines 72-98 including 81-86 (JWK EC) + let cert = build_ec_leaf_cert_with_cn("Resolve EC Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_ok(), "EC resolve failed: {:?}", result.err()); + let doc = result.unwrap(); + assert_eq!(doc.id, did); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert!(jwk.contains_key("x")); + assert!(jwk.contains_key("y")); + assert!(jwk.contains_key("crv")); +} + +#[test] +fn resolver_rsa_cert_produces_did_document() { + // Exercises rsa_to_jwk() → lines 121-134 (RSA JWK: kty, n, e) + let cert = build_rsa_leaf_cert(); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_ok(), "RSA resolve failed: {:?}", result.err()); + let doc = result.unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "RSA"); + assert!(jwk.contains_key("n")); + assert!(jwk.contains_key("e")); +} + +#[test] +fn resolver_validation_fails_returns_error() { + // Exercises resolve() line 74-75: validation fails → PolicyValidationFailed + let cert = build_ec_leaf_cert_with_cn("Wrong EKU"); + // Use an EKU OID the cert doesn't have + let did = make_did(&cert, "eku:1.2.3.4.5.6.7.8.9"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(_) => {} + other => panic!("Expected PolicyValidationFailed, got: {:?}", other), + } +} + +#[test] +fn resolver_invalid_der_returns_cert_parse_error() { + // Exercises resolve() lines 80-81: CertificateParseError path + // We need a DID that validates against a chain, but then the leaf parse fails. + // Actually this path requires validate() to succeed but from_der to fail, + // which is hard since validate also parses. Instead test with a DID that + // would resolve but parse fails at step 2. + // However, the real uncovered lines 80-81 are about the .map_err on from_der. + // Since validate() would fail first on bad DER, let's verify the error type + // from the validate step at least. + let bad_der = vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF]; + let did = make_did(&bad_der, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&bad_der]); + assert!(result.is_err()); +} + +// ============================================================================ +// policy_validators.rs — validate_eku, validate_subject, validate_san, validate_fulcio_issuer +// Lines 66, 88-93, 130-148 +// ============================================================================ + +#[test] +fn validate_eku_missing_required_oid() { + // Exercises validate_eku lines 22-27: required OID not present + let cert_der = build_ec_leaf_cert_with_cn("EKU Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_eku(&cert, &["9.9.9.9.9".to_string().into()]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("9.9.9.9.9")); +} + +#[test] +fn validate_eku_no_eku_extension() { + // Exercises validate_eku lines 15-18: no EKU extension at all + let cert_der = build_bare_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Extended Key Usage")); +} + +#[test] +fn validate_subject_matching() { + // Exercises validate_subject happy path and value comparison lines 56-71 + let cert_der = build_ec_cert_with_subject("TestCN", "TestOrg", "TestOU"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = + policy_validators::validate_subject(&cert, &[("CN".to_string().into(), "TestCN".to_string().into())]); + assert!(result.is_ok()); +} + +#[test] +fn validate_subject_value_mismatch() { + // Exercises validate_subject lines 80-86: attribute found but value doesn't match + let cert_der = build_ec_cert_with_subject("ActualCN", "ActualOrg", "ActualOU"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = + policy_validators::validate_subject(&cert, &[("CN".to_string().into(), "WrongCN".to_string().into())]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("value mismatch")); +} + +#[test] +fn validate_subject_attribute_not_found() { + // Exercises validate_subject lines 74-77: attribute not in cert subject + let cert_der = build_ec_leaf_cert_with_cn("OnlyCN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = + policy_validators::validate_subject(&cert, &[("O".to_string().into(), "SomeOrg".to_string().into())]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("not found")); +} + +#[test] +fn validate_subject_unknown_attribute_label() { + // Exercises validate_subject lines 47-50: unknown attribute label → error + let cert_der = build_ec_leaf_cert_with_cn("Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = + policy_validators::validate_subject(&cert, &[("BOGUS".to_string().into(), "value".to_string().into())]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Unknown attribute")); +} + +#[test] +fn validate_subject_empty_attrs() { + // Exercises validate_subject lines 35-38: empty attrs list + let cert_der = build_ec_leaf_cert_with_cn("Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject(&cert, &[]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("at least one attribute")); +} + +#[test] +fn validate_san_dns_found() { + // Exercises validate_san lines 108-110: SAN found + let cert_der = build_ec_cert_with_san_dns("example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_ok()); +} + +#[test] +fn validate_san_not_found() { + // Exercises validate_san lines 112-117: SAN type+value not found + let cert_der = build_ec_cert_with_san_dns("example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "wrong.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("not found")); +} + +#[test] +fn validate_san_no_sans_at_all() { + // Exercises validate_san lines 101-105: cert has no SANs + let cert_der = build_ec_leaf_cert_with_cn("NoSAN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "any.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Subject Alternative Names")); +} + +#[test] +fn validate_san_email_type() { + // Exercises SAN email path in san_parser + let cert_der = build_ec_cert_with_san_email("user@example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Email, "user@example.com"); + assert!(result.is_ok()); +} + +#[test] +fn validate_san_uri_type() { + // Exercises SAN URI path in san_parser + let cert_der = build_ec_cert_with_san_uri("https://example.com/id"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Uri, "https://example.com/id"); + assert!(result.is_ok()); +} + +#[test] +fn validate_fulcio_issuer_no_extension() { + // Exercises validate_fulcio_issuer lines 126-130: no Fulcio issuer ext + let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_fulcio_issuer(&cert, "accounts.google.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Fulcio issuer extension")); +} + +// ============================================================================ +// x509_extensions.rs — extract_extended_key_usage, is_ca_certificate, extract_fulcio_issuer +// Lines 24-27, 46, 58-60 +// ============================================================================ + +#[test] +fn extract_eku_returns_code_signing_oid() { + let cert_der = build_ec_leaf_cert_with_cn("EKU Extract"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let ekus = x509_extensions::extract_extended_key_usage(&cert); + assert!(ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3")); +} + +#[test] +fn extract_eku_empty_for_no_eku_cert() { + let cert_der = build_bare_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let ekus = x509_extensions::extract_extended_key_usage(&cert); + assert!(ekus.is_empty()); +} + +#[test] +fn is_ca_certificate_true_for_ca() { + // Exercises is_ca_certificate lines 42-49: BasicConstraints CA:TRUE → line 46 + let cert_der = build_ca_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(x509_extensions::is_ca_certificate(&cert)); +} + +#[test] +fn is_ca_certificate_false_for_leaf() { + let cert_der = build_ec_leaf_cert_with_cn("Leaf"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(!x509_extensions::is_ca_certificate(&cert)); +} + +#[test] +fn extract_fulcio_issuer_returns_none_when_absent() { + // Exercises extract_fulcio_issuer lines 53-63: no matching ext → None + let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(x509_extensions::extract_fulcio_issuer(&cert).is_none()); +} + +#[test] +fn extract_eku_oids_returns_oids() { + let cert_der = build_ec_leaf_cert_with_cn("EKU OIDs"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let oids = x509_extensions::extract_eku_oids(&cert).unwrap(); + assert!(!oids.is_empty()); +} + +// ============================================================================ +// validator.rs — validate() with policy failures, empty chain +// Lines 38-40, 67-68, 88-91 +// ============================================================================ + +#[test] +fn validator_empty_chain_returns_error() { + // Exercises validate() line 28-29: empty chain + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let chain: &[&[u8]] = &[]; + let result = DidX509Validator::validate(&did, chain); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidChain(msg) => assert!(msg.contains("Empty")), + other => panic!("Expected InvalidChain, got: {:?}", other), + } +} + +#[test] +fn validator_fingerprint_mismatch_returns_no_ca_match() { + // Exercises find_ca_by_fingerprint → NoCaMatch (line 73) + let cert = build_ec_leaf_cert_with_cn("Test"); + // Use a fingerprint from a different cert + let other_cert = build_ec_leaf_cert_with_cn("Other"); + let did = make_did(&other_cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::NoCaMatch => {} + other => panic!("Expected NoCaMatch, got: {:?}", other), + } +} + +#[test] +fn validator_policy_failure_produces_invalid_result() { + // Exercises validate() lines 42-53: policy validation fails → invalid result + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:9.9.9.9.9"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + let val_result = result.unwrap(); + assert!(!val_result.is_valid); + assert!(!val_result.errors.is_empty()); +} + +#[test] +fn validator_cert_parse_error_for_bad_der() { + // Exercises validate() lines 37-38: X509Certificate::from_der fails + // We need a chain where the first cert fails to parse but CA fingerprint matches. + // This is tricky: the fingerprint check iterates ALL certs including bad ones. + // Actually find_ca_by_fingerprint doesn't parse certs, just hashes DER bytes. + // So we can have a bad leaf + good CA in the chain. + let bad_leaf: Vec = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; // Not a valid cert but valid DER tag + let ca_cert = build_ec_leaf_cert_with_cn("CA for bad leaf"); + + // The DID fingerprint matches the CA cert (second in chain) + let did = make_did(&ca_cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Validator::validate(&did, &[&bad_leaf, &ca_cert]); + // Should fail at leaf cert parsing + assert!(result.is_err()); +} + +#[test] +fn validator_subject_policy_integration() { + // Exercises validate_policy Subject match arm → line 82-83 + let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); + let did = make_did(&cert, "subject:CN:MyCN"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_san_policy_integration() { + // Exercises validate_policy San match arm → lines 85-86 + let cert = build_ec_cert_with_san_dns("test.example.com"); + let did = make_did(&cert, "san:dns:test.example.com"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_san_policy_failure() { + // Exercises validate_policy San failure → errors collected + let cert = build_ec_cert_with_san_dns("test.example.com"); + let did = make_did(&cert, "san:dns:wrong.example.com"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + let val_result = result.unwrap(); + assert!(!val_result.is_valid); +} + +#[test] +fn validator_unsupported_hash_algorithm() { + // Exercises find_ca_by_fingerprint line 67: unsupported hash + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let _did = format!("did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); + // This should work; now test with an algorithm that gets parsed but not supported + // We need to craft a DID with e.g. "sha999" but the parser won't accept it. + // So let's test the sha384 and sha512 paths through the validator. +} + +// ============================================================================ +// builder.rs — build_from_chain_with_eku, encode_policy for SAN/Subject/FulcioIssuer +// Lines 74-76, 114, 159-160 +// ============================================================================ + +#[test] +fn builder_encode_san_policy() { + // Exercises encode_policy SAN match arm → lines 154-161 + let cert = build_ec_cert_with_san_dns("example.com"); + let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:dns:example.com")); +} + +#[test] +fn builder_encode_san_email_policy() { + let cert = build_ec_cert_with_san_email("user@example.com"); + let policy = DidX509Policy::San(SanType::Email, "user@example.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:email:")); +} + +#[test] +fn builder_encode_san_uri_policy() { + let cert = build_ec_cert_with_san_uri("https://example.com/id"); + let policy = DidX509Policy::San(SanType::Uri, "https://example.com/id".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:uri:")); +} + +#[test] +fn builder_encode_san_dn_policy() { + // Exercises SAN Dn match arm → line 159 + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::San(SanType::Dn, "CN=Test".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:dn:")); +} + +#[test] +fn builder_encode_fulcio_issuer_policy() { + // Exercises encode_policy FulcioIssuer match arm → lines 163-164 + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("fulcio-issuer:accounts.google.com")); +} + +#[test] +fn builder_encode_subject_policy() { + // Exercises encode_policy Subject match arm → lines 145-153 + let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); + let policy = DidX509Policy::Subject(vec![ + ("CN".to_string().into(), "MyCN".to_string().into()), + ("O".to_string().into(), "MyOrg".to_string().into()), + ]); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("subject:CN:MyCN:O:MyOrg")); +} + +#[test] +fn builder_build_from_chain_with_eku() { + // Exercises build_from_chain_with_eku → lines 103-121 + let cert = build_ec_leaf_cert_with_cn("Chain EKU"); + let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); + assert!(result.is_ok()); + let did_str = result.unwrap(); + assert!(did_str.contains("eku:")); +} + +#[test] +fn builder_build_from_chain_with_eku_empty_chain() { + // Exercises build_from_chain_with_eku line 106-108: empty chain + let chain: &[&[u8]] = &[]; + let result = DidX509Builder::build_from_chain_with_eku(chain); + assert!(result.is_err()); +} + +#[test] +fn builder_build_from_chain_with_eku_no_eku() { + // Exercises build_from_chain_with_eku lines 114-116: no EKU found + let cert = build_bare_cert(); + let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); + // This should return an error or empty EKU list + // extract_eku_oids returns Ok(empty_vec), then line 115 checks is_empty + assert!(result.is_err()); +} + +#[test] +fn builder_build_from_chain_empty() { + // Exercises build_from_chain line 94-96: empty chain + let chain: &[&[u8]] = &[]; + let result = DidX509Builder::build_from_chain(chain, &[]); + assert!(result.is_err()); +} + +#[test] +fn builder_unsupported_hash_algorithm() { + // Exercises compute_fingerprint line 128: unsupported hash + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha999"); + assert!(result.is_err()); +} + +#[test] +fn builder_sha384_hash() { + // Exercises compute_fingerprint sha384 path → line 126 + let cert = build_ec_leaf_cert_with_cn("SHA384 Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha384"); + assert!(result.is_ok()); +} + +#[test] +fn builder_sha512_hash() { + // Exercises compute_fingerprint sha512 path → line 127 + let cert = build_ec_leaf_cert_with_cn("SHA512 Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha512"); + assert!(result.is_ok()); +} + +// ============================================================================ +// did_document.rs — to_json() non-indented +// Line 59 +// ============================================================================ + +#[test] +fn did_document_to_json_non_indented() { + // Exercises to_json(false) → line 57 (serde_json::to_string) + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], + id: "did:x509:test".to_string().into(), + verification_method: vec![], + assertion_method: vec![], + }; + let json = doc.to_json(false); + assert!(json.is_ok()); + let json_str = json.unwrap(); + assert!(!json_str.contains('\n')); +} + +#[test] +fn did_document_to_json_indented() { + // Exercises to_json(true) → line 55 (serde_json::to_string_pretty) + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], + id: "did:x509:test".to_string().into(), + verification_method: vec![], + assertion_method: vec![], + }; + let json = doc.to_json(true); + assert!(json.is_ok()); + let json_str = json.unwrap(); + assert!(json_str.contains('\n')); +} + +// ============================================================================ +// parser.rs — edge cases +// Lines 35, 119, 127-129, 143, 166, 203-205, 224, 234, 259-260, 282, 286-287, 299 +// ============================================================================ + +#[test] +fn parser_unknown_policy_type() { + // Exercises parse_policy_value lines 199-204: unknown policy type + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::unknownpolicy:somevalue", fp); + let result = DidX509Parser::parse(&did); + // Unknown policy defaults to Eku([]) per line 203 + assert!(result.is_ok()); +} + +#[test] +fn parser_empty_fingerprint() { + // Exercises parser.rs line 118-119: empty fingerprint + let did = "did:x509:0:sha256:::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); +} + +#[test] +fn parser_wrong_fingerprint_length() { + // Exercises parser.rs lines 130-136: fingerprint length mismatch + let did = "did:x509:0:sha256:AAAA::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::FingerprintLengthMismatch(_, _, _) => {} + other => panic!("Expected FingerprintLengthMismatch, got: {:?}", other), + } +} + +#[test] +fn parser_invalid_base64url_chars() { + // Exercises parser.rs lines 138-139: invalid base64url characters + // SHA-256 fingerprint must be exactly 43 base64url chars + let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); +} + +#[test] +fn parser_unsupported_version() { + // Exercises parser.rs lines 102-107: unsupported version + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:9:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::UnsupportedVersion(_, _) => {} + other => panic!("Expected UnsupportedVersion, got: {:?}", other), + } +} + +#[test] +fn parser_unsupported_hash_algorithm() { + // Exercises parser.rs lines 110-114: unsupported hash algorithm + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:md5:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::UnsupportedHashAlgorithm(_) => {} + other => panic!("Expected UnsupportedHashAlgorithm, got: {:?}", other), + } +} + +#[test] +fn parser_empty_policy_segment() { + // Exercises parser.rs lines 149-151: empty policy at position + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}:: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_policy_no_colon() { + // Exercises parser.rs lines 155-158: policy without colon + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::nocolon", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidPolicyFormat(_) => {} + other => panic!("Expected InvalidPolicyFormat, got: {:?}", other), + } +} + +#[test] +fn parser_empty_policy_name() { + // Exercises parser.rs line 165-167: empty policy name (colon at start) + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}:::value", fp); + let result = DidX509Parser::parse(&did); + // This has :: followed by : → first splits on :: giving empty segment handled above + // or parsing of ":value" where colon_idx == 0 + assert!(result.is_err()); +} + +#[test] +fn parser_empty_policy_value() { + // Exercises parser.rs lines 169-171: empty policy value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::eku: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_san_policy_missing_value() { + // Exercises parse_san_policy lines 244-248: missing colon in SAN value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:dnsnocolon", fp); + let result = DidX509Parser::parse(&did); + // "dnsnocolon" has no colon → InvalidSanPolicyFormat + assert!(result.is_err()); +} + +#[test] +fn parser_san_policy_invalid_type() { + // Exercises parse_san_policy lines 255-256: invalid SAN type + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:badtype:value", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidSanType(_) => {} + other => panic!("Expected InvalidSanType, got: {:?}", other), + } +} + +#[test] +fn parser_eku_invalid_oid() { + // Exercises parse_eku_policy line 271: invalid OID format + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::eku:not-an-oid", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidEkuOid => {} + other => panic!("Expected InvalidEkuOid, got: {:?}", other), + } +} + +#[test] +fn parser_fulcio_issuer_empty() { + // Exercises parse_fulcio_issuer_policy lines 281-283: empty issuer + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::fulcio-issuer: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_fulcio_issuer_valid() { + // Exercises parse_fulcio_issuer_policy lines 286-288: happy path + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!( + "did:x509:0:sha256:{}::fulcio-issuer:accounts.google.com", + fp + ); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); + let parsed = result.unwrap(); + assert!(parsed.has_fulcio_issuer_policy()); +} + +#[test] +fn parser_subject_policy_odd_components() { + // Exercises parse_subject_policy line 213: odd number of components + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::subject:CN:val:extra", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidSubjectPolicyComponents => {} + other => panic!("Expected InvalidSubjectPolicyComponents, got: {:?}", other), + } +} + +#[test] +fn parser_subject_policy_empty_key() { + // Exercises parse_subject_policy line 224: empty key + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + // "subject::val" where first part splits into ["", "val"] + // Actually ":val" as the policy_value → splits on ':' → ["", "val"] + let did = format!("did:x509:0:sha256:{}::subject::val", fp); + let result = DidX509Parser::parse(&did); + // The :: in "subject::val" would be split as major_parts separator + // Let's use percent-encoding approach instead + // Actually "subject" followed by ":val" → policy_value is "val" which has 1 part → odd + assert!(result.is_err()); +} + +#[test] +fn parser_subject_policy_duplicate_key() { + // Exercises parse_subject_policy lines 228-230: duplicate key + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::subject:CN:val1:CN:val2", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::DuplicateSubjectPolicyKey(_) => {} + other => panic!("Expected DuplicateSubjectPolicyKey, got: {:?}", other), + } +} + +#[test] +fn parser_sha384_fingerprint() { + // Exercises parser sha384 path → line 124 expected_length = 64 + use sha2::Sha384; + let cert = build_ec_leaf_cert_with_cn("SHA384"); + let hash = Sha384::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_sha512_fingerprint() { + // Exercises parser sha512 path → line 125-126 expected_length = 86 + use sha2::Sha512; + let cert = build_ec_leaf_cert_with_cn("SHA512"); + let hash = Sha512::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_try_parse_returns_none_on_failure() { + let result = DidX509Parser::try_parse("not a valid DID"); + assert!(result.is_none()); +} + +#[test] +fn parser_try_parse_returns_some_on_success() { + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Parser::try_parse(&did); + assert!(result.is_some()); +} + +#[test] +fn parser_san_percent_encoded_value() { + // Exercises parse_san_policy line 259: percent_decode on SAN value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:email:user%40example.com", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_invalid_prefix() { + // Exercises parser.rs lines 77-79: wrong prefix + let result = DidX509Parser::parse("did:wrong:0:sha256:AAAA::eku:1.2.3"); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidPrefix(_) => {} + other => panic!("Expected InvalidPrefix, got: {:?}", other), + } +} + +#[test] +fn parser_missing_policies() { + // Exercises parser.rs lines 83-85: no :: separator + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::MissingPolicies => {} + other => panic!("Expected MissingPolicies, got: {:?}", other), + } +} + +#[test] +fn parser_wrong_component_count() { + // Exercises parser.rs lines 91-95: prefix has wrong number of components + let result = DidX509Parser::parse("did:x509:0:sha256::eku:1.2.3"); + assert!(result.is_err()); +} + +#[test] +fn parser_empty_did() { + let result = DidX509Parser::parse(""); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::EmptyDid => {} + other => panic!("Expected EmptyDid, got: {:?}", other), + } +} + +#[test] +fn parser_whitespace_only_did() { + let result = DidX509Parser::parse(" "); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::EmptyDid => {} + other => panic!("Expected EmptyDid, got: {:?}", other), + } +} + +// ============================================================================ +// san_parser.rs — edge cases for DirectoryName (lines 23-26) +// ============================================================================ + +#[test] +fn san_parser_parse_sans_from_cert_with_dns() { + let cert_der = build_ec_cert_with_san_dns("test.example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); + assert!(!sans.is_empty()); + assert_eq!(sans[0].san_type, SanType::Dns); + assert_eq!(sans[0].value, "test.example.com"); +} + +#[test] +fn san_parser_parse_sans_from_cert_no_san() { + let cert_der = build_ec_leaf_cert_with_cn("No SAN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); + assert!(sans.is_empty()); +} + +// ============================================================================ +// Validation result model tests +// ============================================================================ + +#[test] +fn validation_result_add_error() { + let mut result = DidX509ValidationResult::valid(0); + assert!(result.is_valid); + result.add_error("test error".to_string().into()); + assert!(!result.is_valid); + assert_eq!(result.errors.len(), 1); +} + +#[test] +fn validation_result_invalid_single() { + let result = DidX509ValidationResult::invalid("single error".to_string().into()); + assert!(!result.is_valid); + assert!(result.matched_ca_index.is_none()); + assert_eq!(result.errors.len(), 1); +} + +// ============================================================================ +// Resolver with sha384 and sha512 hash algorithms via validator +// ============================================================================ + +#[test] +fn validator_sha384_fingerprint_matching() { + use sha2::Sha384; + let cert = build_ec_leaf_cert_with_cn("SHA384 Validator"); + let hash = Sha384::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_sha512_fingerprint_matching() { + use sha2::Sha512; + let cert = build_ec_leaf_cert_with_cn("SHA512 Validator"); + let hash = Sha512::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +// ============================================================================ +// Error Display coverage +// ============================================================================ + +#[test] +fn error_display_coverage() { + // Exercise Display for several error variants + let errors: Vec = vec![ + DidX509Error::EmptyDid, + DidX509Error::InvalidPrefix("test".to_string().into()), + DidX509Error::MissingPolicies, + DidX509Error::InvalidFormat("fmt".to_string().into()), + DidX509Error::UnsupportedVersion("1".to_string().into(), "0".to_string().into()), + DidX509Error::UnsupportedHashAlgorithm("md5".to_string().into()), + DidX509Error::EmptyFingerprint, + DidX509Error::FingerprintLengthMismatch("sha256".to_string().into(), 43, 10), + DidX509Error::InvalidFingerprintChars, + DidX509Error::EmptyPolicy(1), + DidX509Error::InvalidPolicyFormat("bad".to_string().into()), + DidX509Error::EmptyPolicyName, + DidX509Error::EmptyPolicyValue, + DidX509Error::InvalidSubjectPolicyComponents, + DidX509Error::EmptySubjectPolicyKey, + DidX509Error::DuplicateSubjectPolicyKey("CN".to_string().into()), + DidX509Error::InvalidSanPolicyFormat("bad".to_string().into()), + DidX509Error::InvalidSanType("bad".to_string().into()), + DidX509Error::InvalidEkuOid, + DidX509Error::EmptyFulcioIssuer, + DidX509Error::PercentDecodingError("bad".to_string().into()), + DidX509Error::InvalidHexCharacter('G'), + DidX509Error::InvalidChain("bad".to_string().into()), + DidX509Error::CertificateParseError("bad".to_string().into()), + DidX509Error::PolicyValidationFailed("bad".to_string().into()), + DidX509Error::NoCaMatch, + DidX509Error::ValidationFailed("bad".to_string().into()), + ]; + for err in &errors { + let msg = format!("{}", err); + assert!(!msg.is_empty()); + } +} + +// ============================================================================ +// base64url encoding edge cases in builder.rs (lines 26-37 of builder.rs) +// These are actually in the inline base64_encode function +// ============================================================================ + +#[test] +fn builder_build_sha256_shorthand() { + let cert = build_ec_leaf_cert_with_cn("Shorthand"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(result.is_ok()); +} + +#[test] +fn builder_build_from_chain_last_cert_as_ca() { + // Exercises build_from_chain line 97-98: uses last cert as CA + let leaf = build_ec_leaf_cert_with_cn("Leaf"); + let ca = build_ca_cert(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build_from_chain(&[&leaf, &ca], &[policy]); + assert!(result.is_ok()); +} + +// ============================================================================ +// SanType::as_str() for all variants +// ============================================================================ + +#[test] +fn san_type_as_str_all_variants() { + assert_eq!(SanType::Email.as_str(), "email"); + assert_eq!(SanType::Dns.as_str(), "dns"); + assert_eq!(SanType::Uri.as_str(), "uri"); + assert_eq!(SanType::Dn.as_str(), "dn"); +} + +#[test] +fn san_type_from_str_all_variants() { + assert_eq!(SanType::from_str("email"), Some(SanType::Email)); + assert_eq!(SanType::from_str("dns"), Some(SanType::Dns)); + assert_eq!(SanType::from_str("uri"), Some(SanType::Uri)); + assert_eq!(SanType::from_str("dn"), Some(SanType::Dn)); + assert_eq!(SanType::from_str("bad"), None); +} + +// ============================================================================ +// Resolver round-trip: build DID then resolve to verify EC JWK +// ============================================================================ + +#[test] +fn resolver_roundtrip_build_then_resolve_ec() { + let cert = build_ec_leaf_cert_with_cn("Roundtrip EC"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); + let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); + assert_eq!(doc.verification_method.len(), 1); + assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); +} + +#[test] +fn resolver_roundtrip_build_then_resolve_rsa() { + let cert = build_rsa_leaf_cert(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); + let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); + assert_eq!(doc.verification_method.len(), 1); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "RSA"); +} diff --git a/native/rust/did/x509/tests/targeted_95_coverage.rs b/native/rust/did/x509/tests/targeted_95_coverage.rs index 9c45adc8..3720d324 100644 --- a/native/rust/did/x509/tests/targeted_95_coverage.rs +++ b/native/rust/did/x509/tests/targeted_95_coverage.rs @@ -1,278 +1,279 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Targeted coverage tests for did_x509 gaps. -//! -//! Targets: resolver.rs (RSA JWK, EC P-384/P-521, unsupported key type), -//! policy_validators.rs (subject attr mismatch, SAN missing, Fulcio URL prefix), -//! x509_extensions.rs (is_ca_certificate, Fulcio issuer), -//! san_parser.rs (various SAN types), -//! validator.rs (multiple policy validation). - -use did_x509::builder::DidX509Builder; -use did_x509::error::DidX509Error; -use did_x509::resolver::DidX509Resolver; -use did_x509::validator::DidX509Validator; - -// Helper: generate a self-signed EC P-256 cert with code signing EKU -fn make_ec_leaf() -> Vec { - use openssl::asn1::Asn1Time; - use openssl::ec::{EcGroup, EcKey}; - use openssl::hash::MessageDigest; - use openssl::nid::Nid; - use openssl::pkey::PKey; - use openssl::x509::extension::ExtendedKeyUsage; - use openssl::x509::{X509Builder, X509NameBuilder}; - - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "Test Leaf") - .unwrap(); - name_builder.append_entry_by_text("O", "TestOrg").unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -// Helper: generate a self-signed RSA cert -fn make_rsa_leaf() -> Vec { - use openssl::asn1::Asn1Time; - use openssl::hash::MessageDigest; - use openssl::pkey::PKey; - use openssl::rsa::Rsa; - use openssl::x509::extension::ExtendedKeyUsage; - use openssl::x509::{X509Builder, X509NameBuilder}; - - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder.append_entry_by_text("CN", "RSA Leaf").unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -// ============================================================================ -// resolver.rs — RSA key resolution to JWK -// ============================================================================ - -#[test] -fn resolve_rsa_certificate_to_jwk() { - let cert_der = make_rsa_leaf(); - let chain = vec![cert_der.as_slice()]; - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - - let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); - assert_eq!(doc.verification_method.len(), 1); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").map(|s| s.as_str()), Some("RSA")); - assert!(jwk.contains_key("n"), "JWK should contain modulus 'n'"); - assert!(jwk.contains_key("e"), "JWK should contain exponent 'e'"); -} - -// ============================================================================ -// resolver.rs — EC P-256 key resolution to JWK -// ============================================================================ - -#[test] -fn resolve_ec_p256_certificate_to_jwk() { - let cert_der = make_ec_leaf(); - let chain = vec![cert_der.as_slice()]; - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - - let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").map(|s| s.as_str()), Some("EC")); - assert!(jwk.contains_key("x"), "JWK should contain 'x' coordinate"); - assert!(jwk.contains_key("y"), "JWK should contain 'y' coordinate"); - assert_eq!(jwk.get("crv").map(|s| s.as_str()), Some("P-256")); -} - -// ============================================================================ -// validator.rs — DID validation with invalid fingerprint -// ============================================================================ - -#[test] -fn validate_with_wrong_fingerprint_errors() { - let cert_der = make_ec_leaf(); - // Create a DID with wrong fingerprint - let result = DidX509Validator::validate( - "did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3", - &[cert_der.as_slice()], - ); - // Should error because the fingerprint is empty/invalid - assert!(result.is_err()); -} - -// ============================================================================ -// validator.rs — DID validation succeeds with correct chain -// ============================================================================ - -#[test] -fn validate_with_correct_chain_succeeds() { - let cert_der = make_ec_leaf(); - let chain = vec![cert_der.as_slice()]; - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - - let result = DidX509Validator::validate(&did, &chain).unwrap(); - assert!(result.is_valid, "Validation should succeed"); -} - -// ============================================================================ -// builder.rs — build from chain with SHA-384 -// ============================================================================ - -#[test] -fn build_did_with_sha384() { - let cert_der = make_ec_leaf(); - let chain = vec![cert_der.as_slice()]; - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - assert!( - did.starts_with("did:x509:"), - "DID should start with did:x509:" - ); -} - -// ============================================================================ -// policy_validators — subject validation with correct attributes -// ============================================================================ - -#[test] -fn policy_subject_validation() { - let cert_der = make_ec_leaf(); - let chain = vec![cert_der.as_slice()]; - - // Build DID with subject policy including CN - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - // The DID should contain the EKU policy - assert!( - did.contains("eku"), - "DID should contain EKU policy: {}", - did - ); -} - -// ============================================================================ -// validator — empty chain error -// ============================================================================ - -#[test] -fn validate_empty_chain_errors() { - let result = - DidX509Validator::validate("did:x509:0:sha256:aGVsbG8::eku:1.3.6.1.5.5.7.3.3", &[]); - assert!(result.is_err()); -} - -// ============================================================================ -// DID Document structure -// ============================================================================ - -#[test] -fn did_document_has_correct_structure() { - let cert_der = make_ec_leaf(); - let chain = vec![cert_der.as_slice()]; - let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); - - let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); - assert!(doc - .context - .contains(&"https://www.w3.org/ns/did/v1".to_string())); - assert_eq!(doc.id, did); - assert!(!doc.assertion_method.is_empty()); - assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); - assert_eq!(doc.verification_method[0].controller, did); -} - -// ============================================================================ -// san_parser — certificate without SANs returns empty -// ============================================================================ - -#[test] -fn san_parser_no_sans_returns_empty() { - let cert_der = make_ec_leaf(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); - // Our test cert has no SANs - assert!(sans.is_empty()); -} - -// ============================================================================ -// x509_extensions — is_ca_certificate for non-CA cert -// ============================================================================ - -#[test] -fn is_ca_certificate_returns_false_for_leaf() { - let cert_der = make_ec_leaf(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(!did_x509::x509_extensions::is_ca_certificate(&cert)); -} - -// ============================================================================ -// x509_extensions — extract_extended_key_usage -// ============================================================================ - -#[test] -fn extract_eku_returns_code_signing() { - let cert_der = make_ec_leaf(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let ekus = did_x509::x509_extensions::extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.3".to_string()), - "Should contain code signing EKU: {:?}", - ekus - ); -} - -// ============================================================================ -// x509_extensions — extract_fulcio_issuer for cert without it -// ============================================================================ - -#[test] -fn extract_fulcio_issuer_returns_none() { - let cert_der = make_ec_leaf(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(did_x509::x509_extensions::extract_fulcio_issuer(&cert).is_none()); -} - -// ============================================================================ -// x509_extensions — extract_eku_oids -// ============================================================================ - -#[test] -fn extract_eku_oids_returns_ok() { - let cert_der = make_ec_leaf(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let oids = did_x509::x509_extensions::extract_eku_oids(&cert).unwrap(); - assert!(!oids.is_empty()); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Targeted coverage tests for did_x509 gaps. +//! +//! Targets: resolver.rs (RSA JWK, EC P-384/P-521, unsupported key type), +//! policy_validators.rs (subject attr mismatch, SAN missing, Fulcio URL prefix), +//! x509_extensions.rs (is_ca_certificate, Fulcio issuer), +//! san_parser.rs (various SAN types), +//! validator.rs (multiple policy validation). + +use did_x509::builder::DidX509Builder; +use did_x509::error::DidX509Error; +use did_x509::resolver::DidX509Resolver; +use did_x509::validator::DidX509Validator; +use std::borrow::Cow; + +// Helper: generate a self-signed EC P-256 cert with code signing EKU +fn make_ec_leaf() -> Vec { + use openssl::asn1::Asn1Time; + use openssl::ec::{EcGroup, EcKey}; + use openssl::hash::MessageDigest; + use openssl::nid::Nid; + use openssl::pkey::PKey; + use openssl::x509::extension::ExtendedKeyUsage; + use openssl::x509::{X509Builder, X509NameBuilder}; + + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "Test Leaf") + .unwrap(); + name_builder.append_entry_by_text("O", "TestOrg").unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +// Helper: generate a self-signed RSA cert +fn make_rsa_leaf() -> Vec { + use openssl::asn1::Asn1Time; + use openssl::hash::MessageDigest; + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + use openssl::x509::extension::ExtendedKeyUsage; + use openssl::x509::{X509Builder, X509NameBuilder}; + + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder.append_entry_by_text("CN", "RSA Leaf").unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +// ============================================================================ +// resolver.rs — RSA key resolution to JWK +// ============================================================================ + +#[test] +fn resolve_rsa_certificate_to_jwk() { + let cert_der = make_rsa_leaf(); + let chain = vec![cert_der.as_slice()]; + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + + let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); + assert_eq!(doc.verification_method.len(), 1); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").map(|s| s.as_str()), Some("RSA")); + assert!(jwk.contains_key("n"), "JWK should contain modulus 'n'"); + assert!(jwk.contains_key("e"), "JWK should contain exponent 'e'"); +} + +// ============================================================================ +// resolver.rs — EC P-256 key resolution to JWK +// ============================================================================ + +#[test] +fn resolve_ec_p256_certificate_to_jwk() { + let cert_der = make_ec_leaf(); + let chain = vec![cert_der.as_slice()]; + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + + let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").map(|s| s.as_str()), Some("EC")); + assert!(jwk.contains_key("x"), "JWK should contain 'x' coordinate"); + assert!(jwk.contains_key("y"), "JWK should contain 'y' coordinate"); + assert_eq!(jwk.get("crv").map(|s| s.as_str()), Some("P-256")); +} + +// ============================================================================ +// validator.rs — DID validation with invalid fingerprint +// ============================================================================ + +#[test] +fn validate_with_wrong_fingerprint_errors() { + let cert_der = make_ec_leaf(); + // Create a DID with wrong fingerprint + let result = DidX509Validator::validate( + "did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3", + &[cert_der.as_slice()], + ); + // Should error because the fingerprint is empty/invalid + assert!(result.is_err()); +} + +// ============================================================================ +// validator.rs — DID validation succeeds with correct chain +// ============================================================================ + +#[test] +fn validate_with_correct_chain_succeeds() { + let cert_der = make_ec_leaf(); + let chain = vec![cert_der.as_slice()]; + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + + let result = DidX509Validator::validate(&did, &chain).unwrap(); + assert!(result.is_valid, "Validation should succeed"); +} + +// ============================================================================ +// builder.rs — build from chain with SHA-384 +// ============================================================================ + +#[test] +fn build_did_with_sha384() { + let cert_der = make_ec_leaf(); + let chain = vec![cert_der.as_slice()]; + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + assert!( + did.starts_with("did:x509:"), + "DID should start with did:x509:" + ); +} + +// ============================================================================ +// policy_validators — subject validation with correct attributes +// ============================================================================ + +#[test] +fn policy_subject_validation() { + let cert_der = make_ec_leaf(); + let chain = vec![cert_der.as_slice()]; + + // Build DID with subject policy including CN + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + // The DID should contain the EKU policy + assert!( + did.contains("eku"), + "DID should contain EKU policy: {}", + did + ); +} + +// ============================================================================ +// validator — empty chain error +// ============================================================================ + +#[test] +fn validate_empty_chain_errors() { + let result = + DidX509Validator::validate("did:x509:0:sha256:aGVsbG8::eku:1.3.6.1.5.5.7.3.3", &[]); + assert!(result.is_err()); +} + +// ============================================================================ +// DID Document structure +// ============================================================================ + +#[test] +fn did_document_has_correct_structure() { + let cert_der = make_ec_leaf(); + let chain = vec![cert_der.as_slice()]; + let did = DidX509Builder::build_from_chain_with_eku(&chain).unwrap(); + + let doc = DidX509Resolver::resolve(&did, &chain).unwrap(); + assert!(doc + .context + .contains(&"https://www.w3.org/ns/did/v1".to_string())); + assert_eq!(doc.id, did); + assert!(!doc.assertion_method.is_empty()); + assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); + assert_eq!(doc.verification_method[0].controller, did); +} + +// ============================================================================ +// san_parser — certificate without SANs returns empty +// ============================================================================ + +#[test] +fn san_parser_no_sans_returns_empty() { + let cert_der = make_ec_leaf(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); + // Our test cert has no SANs + assert!(sans.is_empty()); +} + +// ============================================================================ +// x509_extensions — is_ca_certificate for non-CA cert +// ============================================================================ + +#[test] +fn is_ca_certificate_returns_false_for_leaf() { + let cert_der = make_ec_leaf(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(!did_x509::x509_extensions::is_ca_certificate(&cert)); +} + +// ============================================================================ +// x509_extensions — extract_extended_key_usage +// ============================================================================ + +#[test] +fn extract_eku_returns_code_signing() { + let cert_der = make_ec_leaf(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let ekus = did_x509::x509_extensions::extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Should contain code signing EKU: {:?}", + ekus + ); +} + +// ============================================================================ +// x509_extensions — extract_fulcio_issuer for cert without it +// ============================================================================ + +#[test] +fn extract_fulcio_issuer_returns_none() { + let cert_der = make_ec_leaf(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(did_x509::x509_extensions::extract_fulcio_issuer(&cert).is_none()); +} + +// ============================================================================ +// x509_extensions — extract_eku_oids +// ============================================================================ + +#[test] +fn extract_eku_oids_returns_ok() { + let cert_der = make_ec_leaf(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let oids = did_x509::x509_extensions::extract_eku_oids(&cert).unwrap(); + assert!(!oids.is_empty()); +} diff --git a/native/rust/did/x509/tests/validator_comprehensive.rs b/native/rust/did/x509/tests/validator_comprehensive.rs index b25345ce..646c6b03 100644 --- a/native/rust/did/x509/tests/validator_comprehensive.rs +++ b/native/rust/did/x509/tests/validator_comprehensive.rs @@ -1,374 +1,375 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional validator coverage tests - -use did_x509::builder::DidX509Builder; -use did_x509::error::DidX509Error; -use did_x509::models::policy::DidX509Policy; -use did_x509::models::SanType; -use did_x509::validator::DidX509Validator; -use rcgen::string::Ia5String; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; - -/// Generate certificate with code signing EKU -fn generate_code_signing_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Certificate"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate certificate with multiple EKUs -fn generate_multi_eku_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Multi EKU Test"); - params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::ServerAuth, - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate certificate with subject attributes -fn generate_cert_with_subject() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Subject Test"); - params - .distinguished_name - .push(DnType::OrganizationName, "Test Org"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate certificate with SAN -fn generate_cert_with_san() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "SAN Test"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - params.subject_alt_names = vec![ - RcgenSanType::DnsName(Ia5String::try_from("example.com").unwrap()), - RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_with_eku_policy() { - let cert_der = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!( - result.is_ok(), - "Validation should succeed: {:?}", - result.err() - ); - - let validation = result.unwrap(); - assert!(validation.is_valid, "Should be valid"); - assert!(validation.errors.is_empty(), "Should have no errors"); -} - -#[test] -fn test_validate_with_wrong_eku() { - // Create cert with Server Auth, validate for Code Signing - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Wrong EKU Test"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - let cert_der = cert.der().to_vec(); - - // Build DID requiring code signing using proper builder - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!(result.is_ok()); // Parsing works, but validation result indicates failure - - let validation = result.unwrap(); - assert!( - !validation.is_valid, - "Should not be valid due to EKU mismatch" - ); - assert!(!validation.errors.is_empty(), "Should have errors"); -} - -#[test] -fn test_validate_with_subject_policy() { - let cert_der = generate_cert_with_subject(); - - // Build DID with subject policy - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]), - DidX509Policy::Subject(vec![("CN".to_string(), "Subject Test".to_string())]), - ]; - let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!( - result.is_ok(), - "Validation should succeed: {:?}", - result.err() - ); - - let validation = result.unwrap(); - assert!(validation.is_valid, "Should be valid with matching subject"); -} - -#[test] -fn test_validate_with_san_policy() { - let cert_der = generate_cert_with_san(); - - // Build DID with SAN policy - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]), - DidX509Policy::San(SanType::Dns, "example.com".to_string()), - ]; - let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!( - result.is_ok(), - "Validation should succeed: {:?}", - result.err() - ); - - let validation = result.unwrap(); - assert!(validation.is_valid, "Should be valid with matching SAN"); -} - -#[test] -fn test_validate_empty_chain() { - let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.2.3"; - - let result = DidX509Validator::validate(did, &[]); - assert!(result.is_err()); - - match result.unwrap_err() { - DidX509Error::InvalidChain(msg) => { - assert!(msg.contains("Empty"), "Should indicate empty chain"); - } - other => panic!("Expected InvalidChain, got {:?}", other), - } -} - -#[test] -fn test_validate_fingerprint_mismatch() { - let cert_der = generate_code_signing_cert(); - - // Use wrong fingerprint - must be proper length (64 hex chars = 32 bytes for sha256) - let wrong_fingerprint = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - let did = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - wrong_fingerprint - ); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!(result.is_err()); - - match result.unwrap_err() { - DidX509Error::NoCaMatch => {} // Expected - DidX509Error::FingerprintLengthMismatch(_, _, _) => {} // Also acceptable - other => panic!( - "Expected NoCaMatch or FingerprintLengthMismatch, got {:?}", - other - ), - } -} - -#[test] -fn test_validate_invalid_did_format() { - let cert_der = generate_code_signing_cert(); - let invalid_did = "not-a-valid-did"; - - let result = DidX509Validator::validate(invalid_did, &[&cert_der]); - assert!(result.is_err(), "Should fail with invalid DID format"); -} - -#[test] -fn test_validate_multiple_policies_all_pass() { - let cert_der = generate_cert_with_san(); - - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]), - DidX509Policy::San(SanType::Dns, "example.com".to_string()), - DidX509Policy::San(SanType::Email, "test@example.com".to_string()), - ]; - let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); - - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!(result.is_ok()); - - let validation = result.unwrap(); - assert!(validation.is_valid, "All policies should pass"); -} - -#[test] -fn test_validate_multiple_policies_one_fails() { - let cert_der = generate_cert_with_san(); - - // Build DID with policies that match, then validate with a different SAN - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]), - DidX509Policy::San(SanType::Dns, "example.com".to_string()), - ]; - let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); - - // First validate that the correct policies pass - let result = DidX509Validator::validate(&did, &[&cert_der]); - assert!(result.is_ok()); - let validation = result.unwrap(); - assert!(validation.is_valid, "Correct policies should pass"); - - // Now create a DID with a wrong SAN - use sha2::{Digest, Sha256}; - let fingerprint = Sha256::digest(&cert_der); - let fingerprint_hex = hex::encode(fingerprint); - - // Use base64url encoded fingerprint instead (this is what the parser expects) - let did_wrong = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3::san:dns:nonexistent.com", - fingerprint_hex - ); - - let result2 = DidX509Validator::validate(&did_wrong, &[&cert_der]); - // The DID parser may reject this format - check both possibilities - match result2 { - Ok(validation) => { - // If parsing succeeds, validation should fail - assert!(!validation.is_valid, "Should fail due to wrong SAN"); - } - Err(_) => { - // Parsing failed due to format issues - also acceptable - } - } -} - -#[test] -fn test_validation_result_invalid_multiple() { - // Test the invalid_multiple helper - use did_x509::models::DidX509ValidationResult; - - let errors = vec!["Error 1".to_string(), "Error 2".to_string()]; - let result = DidX509ValidationResult::invalid_multiple(errors.clone()); - - assert!(!result.is_valid); - assert_eq!(result.errors.len(), 2); - assert!(result.matched_ca_index.is_none()); -} - -#[test] -fn test_validation_result_add_error() { - use did_x509::models::DidX509ValidationResult; - - // Start with a valid result - let mut result = DidX509ValidationResult::valid(0); - assert!(result.is_valid); - assert!(result.errors.is_empty()); - - // Add an error - result.add_error("Error 1".to_string()); - - // Should now be invalid - assert!(!result.is_valid); - assert_eq!(result.errors.len(), 1); - assert_eq!(result.errors[0], "Error 1"); - - // Add another error - result.add_error("Error 2".to_string()); - assert!(!result.is_valid); - assert_eq!(result.errors.len(), 2); -} - -#[test] -fn test_validation_result_partial_eq_and_clone() { - use did_x509::models::DidX509ValidationResult; - - let result1 = DidX509ValidationResult::valid(0); - let result2 = result1.clone(); - - // Test PartialEq - assert_eq!(result1, result2); - - let result3 = DidX509ValidationResult::invalid("Error".to_string()); - assert_ne!(result1, result3); -} - -#[test] -fn test_validation_result_debug() { - use did_x509::models::DidX509ValidationResult; - - let result = DidX509ValidationResult::valid(0); - let debug_str = format!("{:?}", result); - assert!(debug_str.contains("is_valid: true")); -} - -#[test] -fn test_validator_with_sha384_did() { - // Generate a certificate - let cert_der = generate_code_signing_cert(); - - // Build DID with SHA384 - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = - DidX509Builder::build(&cert_der, &[policy], "sha384").expect("Should build SHA384 DID"); - - // Validate with the certificate - let result = DidX509Validator::validate(&did_string, &[&cert_der]); - - assert!( - result.is_ok(), - "Validation should succeed: {:?}", - result.err() - ); - let validation = result.unwrap(); - assert!(validation.is_valid, "Certificate should match DID"); -} - -#[test] -fn test_validator_with_sha512_did() { - // Generate a certificate - let cert_der = generate_code_signing_cert(); - - // Build DID with SHA512 - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string()]); - let did_string = - DidX509Builder::build(&cert_der, &[policy], "sha512").expect("Should build SHA512 DID"); - - // Validate with the certificate - let result = DidX509Validator::validate(&did_string, &[&cert_der]); - - assert!( - result.is_ok(), - "Validation should succeed: {:?}", - result.err() - ); - let validation = result.unwrap(); - assert!(validation.is_valid, "Certificate should match DID"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional validator coverage tests + +use did_x509::builder::DidX509Builder; +use did_x509::error::DidX509Error; +use did_x509::models::policy::DidX509Policy; +use did_x509::models::SanType; +use did_x509::validator::DidX509Validator; +use rcgen::string::Ia5String; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; +use std::borrow::Cow; + +/// Generate certificate with code signing EKU +fn generate_code_signing_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Certificate"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate certificate with multiple EKUs +fn generate_multi_eku_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Multi EKU Test"); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::ServerAuth, + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate certificate with subject attributes +fn generate_cert_with_subject() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Subject Test"); + params + .distinguished_name + .push(DnType::OrganizationName, "Test Org"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate certificate with SAN +fn generate_cert_with_san() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "SAN Test"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + params.subject_alt_names = vec![ + RcgenSanType::DnsName(Ia5String::try_from("example.com").unwrap()), + RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_with_eku_policy() { + let cert_der = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!( + result.is_ok(), + "Validation should succeed: {:?}", + result.err() + ); + + let validation = result.unwrap(); + assert!(validation.is_valid, "Should be valid"); + assert!(validation.errors.is_empty(), "Should have no errors"); +} + +#[test] +fn test_validate_with_wrong_eku() { + // Create cert with Server Auth, validate for Code Signing + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Wrong EKU Test"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + let cert_der = cert.der().to_vec(); + + // Build DID requiring code signing using proper builder + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!(result.is_ok()); // Parsing works, but validation result indicates failure + + let validation = result.unwrap(); + assert!( + !validation.is_valid, + "Should not be valid due to EKU mismatch" + ); + assert!(!validation.errors.is_empty(), "Should have errors"); +} + +#[test] +fn test_validate_with_subject_policy() { + let cert_der = generate_cert_with_subject(); + + // Build DID with subject policy + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]), + DidX509Policy::Subject(vec![("CN".to_string(), "Subject Test".to_string())]), + ]; + let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!( + result.is_ok(), + "Validation should succeed: {:?}", + result.err() + ); + + let validation = result.unwrap(); + assert!(validation.is_valid, "Should be valid with matching subject"); +} + +#[test] +fn test_validate_with_san_policy() { + let cert_der = generate_cert_with_san(); + + // Build DID with SAN policy + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]), + DidX509Policy::San(SanType::Dns, "example.com".to_string()), + ]; + let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!( + result.is_ok(), + "Validation should succeed: {:?}", + result.err() + ); + + let validation = result.unwrap(); + assert!(validation.is_valid, "Should be valid with matching SAN"); +} + +#[test] +fn test_validate_empty_chain() { + let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.2.3"; + + let result = DidX509Validator::validate(did, &[]); + assert!(result.is_err()); + + match result.unwrap_err() { + DidX509Error::InvalidChain(msg) => { + assert!(msg.contains("Empty"), "Should indicate empty chain"); + } + other => panic!("Expected InvalidChain, got {:?}", other), + } +} + +#[test] +fn test_validate_fingerprint_mismatch() { + let cert_der = generate_code_signing_cert(); + + // Use wrong fingerprint - must be proper length (64 hex chars = 32 bytes for sha256) + let wrong_fingerprint = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + let did = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + wrong_fingerprint + ); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!(result.is_err()); + + match result.unwrap_err() { + DidX509Error::NoCaMatch => {} // Expected + DidX509Error::FingerprintLengthMismatch(_, _, _) => {} // Also acceptable + other => panic!( + "Expected NoCaMatch or FingerprintLengthMismatch, got {:?}", + other + ), + } +} + +#[test] +fn test_validate_invalid_did_format() { + let cert_der = generate_code_signing_cert(); + let invalid_did = "not-a-valid-did"; + + let result = DidX509Validator::validate(invalid_did, &[&cert_der]); + assert!(result.is_err(), "Should fail with invalid DID format"); +} + +#[test] +fn test_validate_multiple_policies_all_pass() { + let cert_der = generate_cert_with_san(); + + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]), + DidX509Policy::San(SanType::Dns, "example.com".to_string()), + DidX509Policy::San(SanType::Email, "test@example.com".to_string()), + ]; + let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); + + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!(result.is_ok()); + + let validation = result.unwrap(); + assert!(validation.is_valid, "All policies should pass"); +} + +#[test] +fn test_validate_multiple_policies_one_fails() { + let cert_der = generate_cert_with_san(); + + // Build DID with policies that match, then validate with a different SAN + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]), + DidX509Policy::San(SanType::Dns, "example.com".to_string()), + ]; + let did = DidX509Builder::build_sha256(&cert_der, &policies).unwrap(); + + // First validate that the correct policies pass + let result = DidX509Validator::validate(&did, &[&cert_der]); + assert!(result.is_ok()); + let validation = result.unwrap(); + assert!(validation.is_valid, "Correct policies should pass"); + + // Now create a DID with a wrong SAN + use sha2::{Digest, Sha256}; + let fingerprint = Sha256::digest(&cert_der); + let fingerprint_hex = hex::encode(fingerprint); + + // Use base64url encoded fingerprint instead (this is what the parser expects) + let did_wrong = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3::san:dns:nonexistent.com", + fingerprint_hex + ); + + let result2 = DidX509Validator::validate(&did_wrong, &[&cert_der]); + // The DID parser may reject this format - check both possibilities + match result2 { + Ok(validation) => { + // If parsing succeeds, validation should fail + assert!(!validation.is_valid, "Should fail due to wrong SAN"); + } + Err(_) => { + // Parsing failed due to format issues - also acceptable + } + } +} + +#[test] +fn test_validation_result_invalid_multiple() { + // Test the invalid_multiple helper + use did_x509::models::DidX509ValidationResult; + + let errors = vec!["Error 1".to_string(), "Error 2".to_string()]; + let result = DidX509ValidationResult::invalid_multiple(errors.clone()); + + assert!(!result.is_valid); + assert_eq!(result.errors.len(), 2); + assert!(result.matched_ca_index.is_none()); +} + +#[test] +fn test_validation_result_add_error() { + use did_x509::models::DidX509ValidationResult; + + // Start with a valid result + let mut result = DidX509ValidationResult::valid(0); + assert!(result.is_valid); + assert!(result.errors.is_empty()); + + // Add an error + result.add_error("Error 1".to_string()); + + // Should now be invalid + assert!(!result.is_valid); + assert_eq!(result.errors.len(), 1); + assert_eq!(result.errors[0], "Error 1"); + + // Add another error + result.add_error("Error 2".to_string()); + assert!(!result.is_valid); + assert_eq!(result.errors.len(), 2); +} + +#[test] +fn test_validation_result_partial_eq_and_clone() { + use did_x509::models::DidX509ValidationResult; + + let result1 = DidX509ValidationResult::valid(0); + let result2 = result1.clone(); + + // Test PartialEq + assert_eq!(result1, result2); + + let result3 = DidX509ValidationResult::invalid("Error".to_string()); + assert_ne!(result1, result3); +} + +#[test] +fn test_validation_result_debug() { + use did_x509::models::DidX509ValidationResult; + + let result = DidX509ValidationResult::valid(0); + let debug_str = format!("{:?}", result); + assert!(debug_str.contains("is_valid: true")); +} + +#[test] +fn test_validator_with_sha384_did() { + // Generate a certificate + let cert_der = generate_code_signing_cert(); + + // Build DID with SHA384 + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = + DidX509Builder::build(&cert_der, &[policy], "sha384").expect("Should build SHA384 DID"); + + // Validate with the certificate + let result = DidX509Validator::validate(&did_string, &[&cert_der]); + + assert!( + result.is_ok(), + "Validation should succeed: {:?}", + result.err() + ); + let validation = result.unwrap(); + assert!(validation.is_valid, "Certificate should match DID"); +} + +#[test] +fn test_validator_with_sha512_did() { + // Generate a certificate + let cert_der = generate_code_signing_cert(); + + // Build DID with SHA512 + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did_string = + DidX509Builder::build(&cert_der, &[policy], "sha512").expect("Should build SHA512 DID"); + + // Validate with the certificate + let result = DidX509Validator::validate(&did_string, &[&cert_der]); + + assert!( + result.is_ok(), + "Validation should succeed: {:?}", + result.err() + ); + let validation = result.unwrap(); + assert!(validation.is_valid, "Certificate should match DID"); +} diff --git a/native/rust/did/x509/tests/x509_extensions_rcgen.rs b/native/rust/did/x509/tests/x509_extensions_rcgen.rs index 502e0d3d..9415b2d3 100644 --- a/native/rust/did/x509/tests/x509_extensions_rcgen.rs +++ b/native/rust/did/x509/tests/x509_extensions_rcgen.rs @@ -1,236 +1,237 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive coverage tests for x509_extensions module. -//! -//! Tests with real certificates generated via rcgen to cover all code paths. - -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use rcgen::{BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair}; -use x509_parser::prelude::*; - -/// Generate a certificate with multiple EKU flags. -fn generate_cert_with_multiple_ekus() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Multi-EKU Test"); - - params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::EmailProtection, - ExtendedKeyUsagePurpose::TimeStamping, - ExtendedKeyUsagePurpose::OcspSigning, - ]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a CA certificate with Basic Constraints. -fn generate_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test CA"); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a non-CA certificate (leaf). -fn generate_leaf_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Leaf"); - params.is_ca = IsCa::NoCa; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a certificate with specific single EKU. -fn generate_cert_with_single_eku(purpose: ExtendedKeyUsagePurpose) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Single EKU Test"); - params.extended_key_usages = vec![purpose]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -#[test] -fn test_extract_eku_server_auth() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ServerAuth); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.1".to_string()), - "Should contain server auth OID" - ); -} - -#[test] -fn test_extract_eku_client_auth() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ClientAuth); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.2".to_string()), - "Should contain client auth OID" - ); -} - -#[test] -fn test_extract_eku_code_signing() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::CodeSigning); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.3".to_string()), - "Should contain code signing OID" - ); -} - -#[test] -fn test_extract_eku_email_protection() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::EmailProtection); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.4".to_string()), - "Should contain email protection OID" - ); -} - -#[test] -fn test_extract_eku_time_stamping() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::TimeStamping); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.8".to_string()), - "Should contain time stamping OID" - ); -} - -#[test] -fn test_extract_eku_ocsp_signing() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::OcspSigning); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.9".to_string()), - "Should contain OCSP signing OID" - ); -} - -#[test] -fn test_extract_eku_multiple_flags() { - let cert_der = generate_cert_with_multiple_ekus(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - - // Should contain all the EKU OIDs - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.1".to_string()), - "Missing server auth" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.2".to_string()), - "Missing client auth" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.3".to_string()), - "Missing code signing" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.4".to_string()), - "Missing email protection" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.8".to_string()), - "Missing time stamping" - ); - assert!( - ekus.contains(&"1.3.6.1.5.5.7.3.9".to_string()), - "Missing OCSP signing" - ); -} - -#[test] -fn test_extract_eku_oids_wrapper() { - let cert_der = generate_cert_with_multiple_ekus(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - - let oids = result.unwrap(); - assert!(!oids.is_empty(), "Should have EKU OIDs"); -} - -#[test] -fn test_is_ca_certificate_true() { - let cert_der = generate_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(is_ca, "CA certificate should be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_false() { - let cert_der = generate_leaf_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Leaf certificate should not be detected as CA"); -} - -#[test] -fn test_extract_fulcio_issuer_not_present() { - // Regular certificate without Fulcio extension - let cert_der = generate_leaf_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!( - issuer.is_none(), - "Should return None when Fulcio extension not present" - ); -} - -#[test] -fn test_extract_eku_no_extension() { - // Certificate without EKU extension - let mut params = CertificateParams::default(); - params.distinguished_name.push(DnType::CommonName, "No EKU"); - // Don't add any EKU - - let key = KeyPair::generate().unwrap(); - let cert_der = params.self_signed(&key).unwrap().der().to_vec(); - - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.is_empty(), - "Should return empty list when no EKU extension" - ); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive coverage tests for x509_extensions module. +//! +//! Tests with real certificates generated via rcgen to cover all code paths. + +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use rcgen::{BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair}; +use x509_parser::prelude::*; +use std::borrow::Cow; + +/// Generate a certificate with multiple EKU flags. +fn generate_cert_with_multiple_ekus() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Multi-EKU Test"); + + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ExtendedKeyUsagePurpose::TimeStamping, + ExtendedKeyUsagePurpose::OcspSigning, + ]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a CA certificate with Basic Constraints. +fn generate_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test CA"); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a non-CA certificate (leaf). +fn generate_leaf_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Leaf"); + params.is_ca = IsCa::NoCa; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a certificate with specific single EKU. +fn generate_cert_with_single_eku(purpose: ExtendedKeyUsagePurpose) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Single EKU Test"); + params.extended_key_usages = vec![purpose]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +#[test] +fn test_extract_eku_server_auth() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ServerAuth); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Should contain server auth OID" + ); +} + +#[test] +fn test_extract_eku_client_auth() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ClientAuth); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Should contain client auth OID" + ); +} + +#[test] +fn test_extract_eku_code_signing() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::CodeSigning); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Should contain code signing OID" + ); +} + +#[test] +fn test_extract_eku_email_protection() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::EmailProtection); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Should contain email protection OID" + ); +} + +#[test] +fn test_extract_eku_time_stamping() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::TimeStamping); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Should contain time stamping OID" + ); +} + +#[test] +fn test_extract_eku_ocsp_signing() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::OcspSigning); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Should contain OCSP signing OID" + ); +} + +#[test] +fn test_extract_eku_multiple_flags() { + let cert_der = generate_cert_with_multiple_ekus(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + + // Should contain all the EKU OIDs + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Missing server auth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Missing client auth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Missing code signing" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Missing email protection" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Missing time stamping" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Missing OCSP signing" + ); +} + +#[test] +fn test_extract_eku_oids_wrapper() { + let cert_der = generate_cert_with_multiple_ekus(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + + let oids = result.unwrap(); + assert!(!oids.is_empty(), "Should have EKU OIDs"); +} + +#[test] +fn test_is_ca_certificate_true() { + let cert_der = generate_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(is_ca, "CA certificate should be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_false() { + let cert_der = generate_leaf_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Leaf certificate should not be detected as CA"); +} + +#[test] +fn test_extract_fulcio_issuer_not_present() { + // Regular certificate without Fulcio extension + let cert_der = generate_leaf_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!( + issuer.is_none(), + "Should return None when Fulcio extension not present" + ); +} + +#[test] +fn test_extract_eku_no_extension() { + // Certificate without EKU extension + let mut params = CertificateParams::default(); + params.distinguished_name.push(DnType::CommonName, "No EKU"); + // Don't add any EKU + + let key = KeyPair::generate().unwrap(); + let cert_der = params.self_signed(&key).unwrap().der().to_vec(); + + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.is_empty(), + "Should return empty list when no EKU extension" + ); +} diff --git a/native/rust/did/x509/tests/x509_extensions_tests.rs b/native/rust/did/x509/tests/x509_extensions_tests.rs index 9258daeb..d2085871 100644 --- a/native/rust/did/x509/tests/x509_extensions_tests.rs +++ b/native/rust/did/x509/tests/x509_extensions_tests.rs @@ -1,151 +1,152 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Tests for x509_extensions module - -use did_x509::error::DidX509Error; -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use x509_parser::prelude::*; - -// Helper function to create test certificate with extensions -fn create_test_cert_bytes() -> &'static [u8] { - // This should be a real certificate DER with extensions for testing - // For now, we'll use a minimal certificate structure - &[ - 0x30, 0x82, 0x02, - 0x00, // Certificate SEQUENCE - // ... This would contain a full certificate with extensions - // For testing purposes, we'll create mock scenarios - ] -} - -#[test] -fn test_extract_extended_key_usage_empty() { - // Test with a certificate that has no EKU extension - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let ekus = extract_extended_key_usage(&cert); - assert!(ekus.is_empty() || !ekus.is_empty()); // Should not panic - } -} - -#[test] -fn test_extract_eku_oids_wrapper() { - // Test the wrapper function - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - let _oids = result.unwrap(); - // Function should return Ok even if no EKUs found - } -} - -#[test] -fn test_is_ca_certificate_false() { - // Test with a certificate that doesn't have Basic Constraints or is not a CA - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let is_ca = is_ca_certificate(&cert); - // Should return false for non-CA or missing Basic Constraints - assert!(!is_ca || is_ca); // Should not panic - } -} - -#[test] -fn test_extract_fulcio_issuer_none() { - // Test with a certificate that has no Fulcio issuer extension - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let issuer = extract_fulcio_issuer(&cert); - // Should return None if no Fulcio issuer extension found - assert!(issuer.is_none() || issuer.is_some()); // Should not panic - } -} - -// More comprehensive tests with mock certificate data -#[test] -fn test_extract_functions_basic_coverage() { - // Test the functions exist and work with minimal data - // In production, these would use real test certificates - - let minimal_cert_der = &[ - 0x30, 0x82, 0x02, 0x00, // Certificate SEQUENCE - 0x30, 0x82, 0x01, - 0x00, // TBSCertificate - // Minimal certificate structure - ]; - - // Test that functions can be called (even if parsing fails) - if let Ok((_rem, cert)) = X509Certificate::from_der(minimal_cert_der) { - let _ekus = extract_extended_key_usage(&cert); - let _eku_result = extract_eku_oids(&cert); - let _is_ca = is_ca_certificate(&cert); - let _fulcio = extract_fulcio_issuer(&cert); - } - - // Verify function signatures exist - let _ = extract_extended_key_usage as fn(&X509Certificate) -> Vec; - let _ = extract_eku_oids as fn(&X509Certificate) -> Result, DidX509Error>; - let _ = is_ca_certificate as fn(&X509Certificate) -> bool; - let _ = extract_fulcio_issuer as fn(&X509Certificate) -> Option; -} - -// Test error handling paths -#[test] -fn test_extract_eku_oids_error_handling() { - // Test that extract_eku_oids handles all code paths - let empty_cert_der = &[0x30, 0x00]; // Empty SEQUENCE - if let Ok((_rem, cert)) = X509Certificate::from_der(empty_cert_der) { - let result = extract_eku_oids(&cert); - // Should still return Ok even with malformed certificate - assert!(result.is_ok()); - } -} - -#[test] -fn test_extension_parsing_coverage() { - // Test coverage for different extension parsing scenarios - - // This test ensures we cover the code paths in the extension parsing functions - // by creating certificates with and without the relevant extensions - - let test_cases = vec![ - ("No extensions", create_minimal_cert_with_no_extensions()), - ( - "With basic constraints only", - create_cert_with_basic_constraints(), - ), - ]; - - for (name, cert_der) in test_cases { - if let Ok((_rem, cert)) = X509Certificate::from_der(&cert_der) { - // Test all functions - let _ekus = extract_extended_key_usage(&cert); - let _eku_result = extract_eku_oids(&cert); - let _is_ca = is_ca_certificate(&cert); - let _fulcio = extract_fulcio_issuer(&cert); - - // All should complete without panicking - println!("Tested scenario: {}", name); - } - } -} - -fn create_minimal_cert_with_no_extensions() -> Vec { - // Return a minimal valid certificate DER with no extensions - // This is a simplified example - in practice, use a real minimal cert - vec![ - 0x30, 0x82, 0x01, 0x22, // Certificate SEQUENCE - // ... minimal certificate structure without extensions - 0x30, 0x00, // Empty extensions - ] -} - -fn create_cert_with_basic_constraints() -> Vec { - // Return a certificate DER with Basic Constraints extension - // This would contain a real certificate for testing - vec![ - 0x30, 0x82, 0x01, 0x30, // Certificate SEQUENCE - // ... certificate with Basic Constraints extension - 0x30, 0x10, // Extensions with Basic Constraints - ] -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for x509_extensions module + +use did_x509::error::DidX509Error; +use std::borrow::Cow; +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use x509_parser::prelude::*; + +// Helper function to create test certificate with extensions +fn create_test_cert_bytes() -> &'static [u8] { + // This should be a real certificate DER with extensions for testing + // For now, we'll use a minimal certificate structure + &[ + 0x30, 0x82, 0x02, + 0x00, // Certificate SEQUENCE + // ... This would contain a full certificate with extensions + // For testing purposes, we'll create mock scenarios + ] +} + +#[test] +fn test_extract_extended_key_usage_empty() { + // Test with a certificate that has no EKU extension + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let ekus = extract_extended_key_usage(&cert); + assert!(ekus.is_empty() || !ekus.is_empty()); // Should not panic + } +} + +#[test] +fn test_extract_eku_oids_wrapper() { + // Test the wrapper function + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + let _oids = result.unwrap(); + // Function should return Ok even if no EKUs found + } +} + +#[test] +fn test_is_ca_certificate_false() { + // Test with a certificate that doesn't have Basic Constraints or is not a CA + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let is_ca = is_ca_certificate(&cert); + // Should return false for non-CA or missing Basic Constraints + assert!(!is_ca || is_ca); // Should not panic + } +} + +#[test] +fn test_extract_fulcio_issuer_none() { + // Test with a certificate that has no Fulcio issuer extension + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let issuer = extract_fulcio_issuer(&cert); + // Should return None if no Fulcio issuer extension found + assert!(issuer.is_none() || issuer.is_some()); // Should not panic + } +} + +// More comprehensive tests with mock certificate data +#[test] +fn test_extract_functions_basic_coverage() { + // Test the functions exist and work with minimal data + // In production, these would use real test certificates + + let minimal_cert_der = &[ + 0x30, 0x82, 0x02, 0x00, // Certificate SEQUENCE + 0x30, 0x82, 0x01, + 0x00, // TBSCertificate + // Minimal certificate structure + ]; + + // Test that functions can be called (even if parsing fails) + if let Ok((_rem, cert)) = X509Certificate::from_der(minimal_cert_der) { + let _ekus = extract_extended_key_usage(&cert); + let _eku_result = extract_eku_oids(&cert); + let _is_ca = is_ca_certificate(&cert); + let _fulcio = extract_fulcio_issuer(&cert); + } + + // Verify function signatures exist + let _ = extract_extended_key_usage as fn(&X509Certificate) -> Vec>; + let _ = extract_eku_oids as fn(&X509Certificate) -> Result>, DidX509Error>; + let _ = is_ca_certificate as fn(&X509Certificate) -> bool; + let _ = extract_fulcio_issuer as fn(&X509Certificate) -> Option; +} + +// Test error handling paths +#[test] +fn test_extract_eku_oids_error_handling() { + // Test that extract_eku_oids handles all code paths + let empty_cert_der = &[0x30, 0x00]; // Empty SEQUENCE + if let Ok((_rem, cert)) = X509Certificate::from_der(empty_cert_der) { + let result = extract_eku_oids(&cert); + // Should still return Ok even with malformed certificate + assert!(result.is_ok()); + } +} + +#[test] +fn test_extension_parsing_coverage() { + // Test coverage for different extension parsing scenarios + + // This test ensures we cover the code paths in the extension parsing functions + // by creating certificates with and without the relevant extensions + + let test_cases = vec![ + ("No extensions", create_minimal_cert_with_no_extensions()), + ( + "With basic constraints only", + create_cert_with_basic_constraints(), + ), + ]; + + for (name, cert_der) in test_cases { + if let Ok((_rem, cert)) = X509Certificate::from_der(&cert_der) { + // Test all functions + let _ekus = extract_extended_key_usage(&cert); + let _eku_result = extract_eku_oids(&cert); + let _is_ca = is_ca_certificate(&cert); + let _fulcio = extract_fulcio_issuer(&cert); + + // All should complete without panicking + println!("Tested scenario: {}", name); + } + } +} + +fn create_minimal_cert_with_no_extensions() -> Vec { + // Return a minimal valid certificate DER with no extensions + // This is a simplified example - in practice, use a real minimal cert + vec![ + 0x30, 0x82, 0x01, 0x22, // Certificate SEQUENCE + // ... minimal certificate structure without extensions + 0x30, 0x00, // Empty extensions + ] +} + +fn create_cert_with_basic_constraints() -> Vec { + // Return a certificate DER with Basic Constraints extension + // This would contain a real certificate for testing + vec![ + 0x30, 0x82, 0x01, 0x30, // Certificate SEQUENCE + // ... certificate with Basic Constraints extension + 0x30, 0x10, // Extensions with Basic Constraints + ] +} diff --git a/native/rust/extension_packs/azure_artifact_signing/src/signing/did_x509_helper.rs b/native/rust/extension_packs/azure_artifact_signing/src/signing/did_x509_helper.rs index 75e1f0aa..304e1c68 100644 --- a/native/rust/extension_packs/azure_artifact_signing/src/signing/did_x509_helper.rs +++ b/native/rust/extension_packs/azure_artifact_signing/src/signing/did_x509_helper.rs @@ -26,7 +26,7 @@ pub fn build_did_x509_from_ats_chain(chain_ders: &[&[u8]]) -> Result, + pub details: Option>, } /// The receipt issuer (`iss`) extracted from the MST receipt claims. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MstReceiptIssuerFact { - pub issuer: String, + pub issuer: Arc, } /// The receipt signing key id (`kid`) used to resolve the receipt signing key. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MstReceiptKidFact { - pub kid: String, + pub kid: Arc, } /// SHA-256 digest of the statement bytes that the MST verifier binds the receipt to. @@ -33,13 +34,13 @@ pub struct MstReceiptKidFact { /// with *all* unprotected headers cleared (matching the Azure .NET verifier). #[derive(Clone, Debug, PartialEq, Eq)] pub struct MstReceiptStatementSha256Fact { - pub sha256_hex: String, + pub sha256_hex: Arc, } /// Describes what bytes are covered by the statement digest that the receipt binds to. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MstReceiptStatementCoverageFact { - pub coverage: String, + pub coverage: &'static str, } /// Indicates whether the receipt's own COSE signature verified. @@ -159,7 +160,7 @@ impl FactProperties for MstReceiptIssuerFact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { fields::mst_receipt_issuer::ISSUER => { - Some(FactValue::Str(Cow::Borrowed(self.issuer.as_str()))) + Some(FactValue::Str(Cow::Borrowed(&self.issuer))) } _ => None, } @@ -170,7 +171,7 @@ impl FactProperties for MstReceiptKidFact { /// Return the property value for declarative trust policies. fn get_property<'a>(&'a self, name: &str) -> Option> { match name { - fields::mst_receipt_kid::KID => Some(FactValue::Str(Cow::Borrowed(self.kid.as_str()))), + fields::mst_receipt_kid::KID => Some(FactValue::Str(Cow::Borrowed(&self.kid))), _ => None, } } @@ -181,7 +182,7 @@ impl FactProperties for MstReceiptStatementSha256Fact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { fields::mst_receipt_statement_sha256::SHA256_HEX => { - Some(FactValue::Str(Cow::Borrowed(self.sha256_hex.as_str()))) + Some(FactValue::Str(Cow::Borrowed(&self.sha256_hex))) } _ => None, } @@ -193,7 +194,7 @@ impl FactProperties for MstReceiptStatementCoverageFact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { fields::mst_receipt_statement_coverage::COVERAGE => { - Some(FactValue::Str(Cow::Borrowed(self.coverage.as_str()))) + Some(FactValue::Str(Cow::Borrowed(self.coverage))) } _ => None, } diff --git a/native/rust/extension_packs/mst/src/validation/pack.rs b/native/rust/extension_packs/mst/src/validation/pack.rs index 3b082a19..ecfe0ad0 100644 --- a/native/rust/extension_packs/mst/src/validation/pack.rs +++ b/native/rust/extension_packs/mst/src/validation/pack.rs @@ -1,372 +1,372 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::validation::facts::{ - MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, - MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, - MstReceiptStatementSha256Fact, MstReceiptTrustedFact, -}; -use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; -use cose_sign1_primitives::{ArcSlice, CoseHeaderLabel, CoseHeaderValue}; -use cose_sign1_validation::fluent::*; -use cose_sign1_validation_primitives::error::TrustError; -use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFactProducer}; -use cose_sign1_validation_primitives::ids::sha256_of_bytes; -use cose_sign1_validation_primitives::plan::CompiledTrustPlan; -use cose_sign1_validation_primitives::subject::TrustSubject; -use once_cell::sync::Lazy; -use std::collections::HashSet; - -use crate::validation::receipt_verify::{ - verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, -}; - -pub mod fluent_ext { - pub use crate::validation::fluent_ext::*; -} - -/// Encode bytes as lowercase hex string. -fn hex_encode(bytes: &[u8]) -> String { - bytes - .iter() - .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { - use std::fmt::Write; - // write! to a String is infallible; this expect is defensive. - write!(s, "{:02x}", b).expect("hex formatting to String cannot fail"); - s - }) -} - -/// COSE header label used by MST receipts (matches .NET): 394. -pub const MST_RECEIPT_HEADER_LABEL: i64 = 394; - -#[derive(Clone, Debug, Default)] -pub struct MstTrustPack { - /// If true, allow the verifier to fetch JWKS online when offline keys are missing or do not - /// contain the required `kid`. - /// - /// This is an operational switch. Trust decisions (e.g., issuer allowlisting) belong in policy. - pub allow_network: bool, - - /// Offline JWKS JSON used to resolve receipt signing keys by `kid`. - /// - /// This enables deterministic verification for test vectors without requiring network access. - pub offline_jwks_json: Option, - - /// Optional api-version to use for the CodeTransparency `/jwks` endpoint. - /// If not set, the verifier will try without an api-version parameter. - pub jwks_api_version: Option, -} - -impl MstTrustPack { - /// Create an MST pack with the given options. - pub fn new( - allow_network: bool, - offline_jwks_json: Option, - jwks_api_version: Option, - ) -> Self { - Self { - allow_network, - offline_jwks_json, - jwks_api_version, - } - } - - /// Create an MST pack configured for offline-only verification. - /// - /// This disables network fetching and uses the provided JWKS JSON to resolve receipt signing - /// keys. - pub fn offline_with_jwks(jwks_json: impl Into) -> Self { - Self { - allow_network: false, - offline_jwks_json: Some(jwks_json.into()), - jwks_api_version: None, - } - } - - /// Create an MST pack configured to allow online JWKS fetching. - /// - /// This is an operational switch only; issuer allowlisting should still be expressed via trust - /// policy. - pub fn online() -> Self { - Self { - allow_network: true, - offline_jwks_json: None, - jwks_api_version: None, - } - } -} - -impl TrustFactProducer for MstTrustPack { - /// Stable producer name used for diagnostics/audit. - fn name(&self) -> &'static str { - "cose_sign1_transparent_mst::MstTrustPack" - } - - /// Produce MST-related facts for the current subject. - /// - /// - On `Message` subjects: projects each receipt into a derived `CounterSignature` subject. - /// - On `CounterSignature` subjects: verifies the receipt and emits MST facts. - fn produce(&self, ctx: &mut TrustFactContext<'_>) -> Result<(), TrustError> { - // MST receipts are modeled as counter-signatures: - // - On the Message subject, we *project* each receipt into a derived CounterSignature subject. - // - On the CounterSignature subject, we produce MST-specific facts (present/trusted). - - match ctx.subject().kind { - "Message" => { - // If the COSE message is unavailable, counter-signature discovery is Missing. - if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - } - - let receipts = read_receipts(ctx)?; - - let message_subject = match ctx.cose_sign1_bytes() { - Some(bytes) => TrustSubject::message(bytes), - None => TrustSubject::message(b"seed"), - }; - - let mut seen: HashSet = - HashSet::new(); - - for r in receipts { - let cs_subject = TrustSubject::counter_signature(&message_subject, &r); - let cs_key_subject = TrustSubject::counter_signature_signing_key(&cs_subject); - - ctx.observe(CounterSignatureSubjectFact { - subject: cs_subject, - is_protected_header: false, - })?; - ctx.observe(CounterSignatureSigningKeySubjectFact { - subject: cs_key_subject, - is_protected_header: false, - })?; - - let id = sha256_of_bytes(&r); - if seen.insert(id) { - ctx.observe(UnknownCounterSignatureBytesFact { - counter_signature_id: id, - raw_counter_signature_bytes: std::sync::Arc::from(r.as_bytes()), - })?; - } - } - - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - "CounterSignature" => { - // If the COSE message is unavailable, we can't map this subject to a receipt. - if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { - ctx.mark_missing::("MissingMessage"); - ctx.mark_missing::("MissingMessage"); - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - } - - let receipts = read_receipts(ctx)?; - - let Some(message_bytes) = ctx.cose_sign1_bytes() else { - // Fallback: without bytes we can't compute the same subject IDs. - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - let message_subject = TrustSubject::message(message_bytes); - - let mut matched_receipt: Option = None; - for r in receipts { - let cs = TrustSubject::counter_signature(&message_subject, &r); - if cs.id == ctx.subject().id { - matched_receipt = Some(r); - break; - } - } - - let Some(receipt_bytes) = matched_receipt else { - // Not an MST receipt counter-signature; leave as Available(empty). - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - // Receipt identified. - ctx.observe(MstReceiptPresentFact { present: true })?; - - // Get provider from message (required for receipt verification) - let Some(_msg) = ctx.cose_sign1_message() else { - ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some("no message in context for verification".to_string()), - })?; - for k in self.provides() { - ctx.mark_produced(*k); - } - return Ok(()); - }; - - let jwks_json = self.offline_jwks_json.as_deref(); - let factory = OpenSslJwkVerifierFactory; - let out = verify_mst_receipt(ReceiptVerifyInput { - statement_bytes_with_receipts: message_bytes, - receipt_bytes: &receipt_bytes, - offline_jwks_json: jwks_json, - allow_network_fetch: self.allow_network, - jwks_api_version: self.jwks_api_version.as_deref(), - client: None, // Creates temporary client per-issuer - jwk_verifier_factory: &factory, - }); - - match out { - Ok(v) => { - ctx.observe(MstReceiptTrustedFact { - trusted: v.trusted, - details: v.details.clone(), - })?; - - ctx.observe(MstReceiptIssuerFact { - issuer: v.issuer.clone(), - })?; - ctx.observe(MstReceiptKidFact { kid: v.kid.clone() })?; - ctx.observe(MstReceiptStatementSha256Fact { - sha256_hex: hex_encode(&v.statement_sha256), - })?; - ctx.observe(MstReceiptStatementCoverageFact { - coverage: "sha256(COSE_Sign1 bytes with unprotected headers cleared)" - .to_string(), - })?; - ctx.observe(MstReceiptSignatureVerifiedFact { verified: true })?; - - ctx.observe(CounterSignatureEnvelopeIntegrityFact { - sig_structure_intact: v.trusted, - details: Some( - "covers: sha256(COSE_Sign1 bytes with unprotected headers cleared)" - .to_string(), - ), - })?; - } - Err(e @ ReceiptVerifyError::UnsupportedVds(_)) => { - // Non-Microsoft receipts can coexist with MST receipts. - // Make the fact Available(false) so AnyOf semantics can still succeed. - ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some(e.to_string()), - })?; - } - Err(e) => ctx.observe(MstReceiptTrustedFact { - trusted: false, - details: Some(e.to_string()), - })?, - } - - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - _ => { - for k in self.provides() { - ctx.mark_produced(*k); - } - Ok(()) - } - } - } - - /// Return the set of fact keys this pack can produce. - fn provides(&self) -> &'static [FactKey] { - static PROVIDED: Lazy<[FactKey; 11]> = Lazy::new(|| { - [ - // Counter-signature projection (message-scoped) - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - // MST-specific facts (counter-signature scoped) - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - FactKey::of::(), - ] - }); - &*PROVIDED - } -} - -impl CoseSign1TrustPack for MstTrustPack { - /// Short display name for this trust pack. - fn name(&self) -> &'static str { - "MstTrustPack" - } - - /// Return a `TrustFactProducer` instance for this pack. - fn fact_producer(&self) -> std::sync::Arc { - std::sync::Arc::new(self.clone()) - } - - /// Return the default trust plan for MST-only validation. - /// - /// This plan requires that a counter-signature receipt is trusted. - fn default_trust_plan(&self) -> Option { - use crate::validation::fluent_ext::MstReceiptTrustedWhereExt; - - // Secure-by-default MST policy: - // - require a receipt to be trusted (verification must be enabled) - let bundled = TrustPlanBuilder::new(vec![std::sync::Arc::new(self.clone())]) - .for_counter_signature(|cs| { - cs.require::(|f| f.require_receipt_trusted()) - }) - .compile() - .expect("default trust plan should be satisfiable by the MST trust pack"); - - Some(bundled.plan().clone()) - } -} - -/// Read all MST receipt blobs from the current message. -/// -/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. -fn read_receipts(ctx: &TrustFactContext<'_>) -> Result, TrustError> { - if let Some(msg) = ctx.cose_sign1_message() { - let label = CoseHeaderLabel::Int(MST_RECEIPT_HEADER_LABEL); - match msg.unprotected.get(&label) { - None => return Ok(Vec::new()), - Some(CoseHeaderValue::Array(arr)) => { - let mut result = Vec::new(); - for v in arr { - if let CoseHeaderValue::Bytes(b) = v { - result.push(b.clone()); - } else { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - } - return Ok(result); - } - Some(CoseHeaderValue::Bytes(_)) => { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - Some(_) => { - return Err(TrustError::FactProduction("invalid header".to_string())); - } - } - } - - // Without a parsed message, we cannot read receipts - Ok(Vec::new()) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::validation::facts::{ + MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, + MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, + MstReceiptStatementSha256Fact, MstReceiptTrustedFact, +}; +use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; +use cose_sign1_primitives::{ArcSlice, CoseHeaderLabel, CoseHeaderValue}; +use cose_sign1_validation::fluent::*; +use cose_sign1_validation_primitives::error::TrustError; +use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFactProducer}; +use cose_sign1_validation_primitives::ids::sha256_of_bytes; +use cose_sign1_validation_primitives::plan::CompiledTrustPlan; +use cose_sign1_validation_primitives::subject::TrustSubject; +use once_cell::sync::Lazy; +use std::borrow::Cow; +use std::collections::HashSet; +use std::sync::Arc; + +use crate::validation::receipt_verify::{ + verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, +}; + +pub mod fluent_ext { + pub use crate::validation::fluent_ext::*; +} + +/// Encode bytes as lowercase hex string. +fn hex_encode(bytes: &[u8]) -> String { + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { + use std::fmt::Write; + // write! to a String is infallible; this expect is defensive. + write!(s, "{:02x}", b).expect("hex formatting to String cannot fail"); + s + }) +} + +/// COSE header label used by MST receipts (matches .NET): 394. +pub const MST_RECEIPT_HEADER_LABEL: i64 = 394; + +#[derive(Clone, Debug, Default)] +pub struct MstTrustPack { + /// If true, allow the verifier to fetch JWKS online when offline keys are missing or do not + /// contain the required `kid`. + /// + /// This is an operational switch. Trust decisions (e.g., issuer allowlisting) belong in policy. + pub allow_network: bool, + + /// Offline JWKS JSON used to resolve receipt signing keys by `kid`. + /// + /// This enables deterministic verification for test vectors without requiring network access. + pub offline_jwks_json: Option, + + /// Optional api-version to use for the CodeTransparency `/jwks` endpoint. + /// If not set, the verifier will try without an api-version parameter. + pub jwks_api_version: Option, +} + +impl MstTrustPack { + /// Create an MST pack with the given options. + pub fn new( + allow_network: bool, + offline_jwks_json: Option, + jwks_api_version: Option, + ) -> Self { + Self { + allow_network, + offline_jwks_json, + jwks_api_version, + } + } + + /// Create an MST pack configured for offline-only verification. + /// + /// This disables network fetching and uses the provided JWKS JSON to resolve receipt signing + /// keys. + pub fn offline_with_jwks(jwks_json: impl Into) -> Self { + Self { + allow_network: false, + offline_jwks_json: Some(jwks_json.into()), + jwks_api_version: None, + } + } + + /// Create an MST pack configured to allow online JWKS fetching. + /// + /// This is an operational switch only; issuer allowlisting should still be expressed via trust + /// policy. + pub fn online() -> Self { + Self { + allow_network: true, + offline_jwks_json: None, + jwks_api_version: None, + } + } +} + +impl TrustFactProducer for MstTrustPack { + /// Stable producer name used for diagnostics/audit. + fn name(&self) -> &'static str { + "cose_sign1_transparent_mst::MstTrustPack" + } + + /// Produce MST-related facts for the current subject. + /// + /// - On `Message` subjects: projects each receipt into a derived `CounterSignature` subject. + /// - On `CounterSignature` subjects: verifies the receipt and emits MST facts. + fn produce(&self, ctx: &mut TrustFactContext<'_>) -> Result<(), TrustError> { + // MST receipts are modeled as counter-signatures: + // - On the Message subject, we *project* each receipt into a derived CounterSignature subject. + // - On the CounterSignature subject, we produce MST-specific facts (present/trusted). + + match ctx.subject().kind { + "Message" => { + // If the COSE message is unavailable, counter-signature discovery is Missing. + if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + + let receipts = read_receipts(ctx)?; + + let message_subject = match ctx.cose_sign1_bytes() { + Some(bytes) => TrustSubject::message(bytes), + None => TrustSubject::message(b"seed"), + }; + + let mut seen: HashSet = + HashSet::new(); + + for r in receipts { + let cs_subject = TrustSubject::counter_signature(&message_subject, &r); + let cs_key_subject = TrustSubject::counter_signature_signing_key(&cs_subject); + + ctx.observe(CounterSignatureSubjectFact { + subject: cs_subject, + is_protected_header: false, + })?; + ctx.observe(CounterSignatureSigningKeySubjectFact { + subject: cs_key_subject, + is_protected_header: false, + })?; + + let id = sha256_of_bytes(&r); + if seen.insert(id) { + ctx.observe(UnknownCounterSignatureBytesFact { + counter_signature_id: id, + raw_counter_signature_bytes: std::sync::Arc::from(r.as_bytes()), + })?; + } + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + "CounterSignature" => { + // If the COSE message is unavailable, we can't map this subject to a receipt. + if ctx.cose_sign1_message().is_none() && ctx.cose_sign1_bytes().is_none() { + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + + let receipts = read_receipts(ctx)?; + + let Some(message_bytes) = ctx.cose_sign1_bytes() else { + // Fallback: without bytes we can't compute the same subject IDs. + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + let message_subject = TrustSubject::message(message_bytes); + + let mut matched_receipt: Option = None; + for r in receipts { + let cs = TrustSubject::counter_signature(&message_subject, &r); + if cs.id == ctx.subject().id { + matched_receipt = Some(r); + break; + } + } + + let Some(receipt_bytes) = matched_receipt else { + // Not an MST receipt counter-signature; leave as Available(empty). + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + // Receipt identified. + ctx.observe(MstReceiptPresentFact { present: true })?; + + // Get provider from message (required for receipt verification) + let Some(_msg) = ctx.cose_sign1_message() else { + ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some("no message in context for verification".into()), + })?; + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + }; + + let jwks_json = self.offline_jwks_json.as_deref(); + let factory = OpenSslJwkVerifierFactory; + let out = verify_mst_receipt(ReceiptVerifyInput { + statement_bytes_with_receipts: message_bytes, + receipt_bytes: &receipt_bytes, + offline_jwks_json: jwks_json, + allow_network_fetch: self.allow_network, + jwks_api_version: self.jwks_api_version.as_deref(), + client: None, // Creates temporary client per-issuer + jwk_verifier_factory: &factory, + }); + + match out { + Ok(v) => { + ctx.observe(MstReceiptTrustedFact { + trusted: v.trusted, + details: v.details.clone(), + })?; + + ctx.observe(MstReceiptIssuerFact { + issuer: v.issuer.clone(), + })?; + ctx.observe(MstReceiptKidFact { kid: v.kid.clone() })?; + ctx.observe(MstReceiptStatementSha256Fact { + sha256_hex: Arc::from(hex_encode(&v.statement_sha256)), + })?; + ctx.observe(MstReceiptStatementCoverageFact { + coverage: "sha256(COSE_Sign1 bytes with unprotected headers cleared)", + })?; + ctx.observe(MstReceiptSignatureVerifiedFact { verified: true })?; + + ctx.observe(CounterSignatureEnvelopeIntegrityFact { + sig_structure_intact: v.trusted, + details: Some(Cow::Borrowed( + "covers: sha256(COSE_Sign1 bytes with unprotected headers cleared)", + )), + })?; + } + Err(e @ ReceiptVerifyError::UnsupportedVds(_)) => { + // Non-Microsoft receipts can coexist with MST receipts. + // Make the fact Available(false) so AnyOf semantics can still succeed. + ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some(e.to_string().into()), + })?; + } + Err(e) => ctx.observe(MstReceiptTrustedFact { + trusted: false, + details: Some(e.to_string().into()), + })?, + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + _ => { + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + } + } + + /// Return the set of fact keys this pack can produce. + fn provides(&self) -> &'static [FactKey] { + static PROVIDED: Lazy<[FactKey; 11]> = Lazy::new(|| { + [ + // Counter-signature projection (message-scoped) + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + // MST-specific facts (counter-signature scoped) + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + ] + }); + &*PROVIDED + } +} + +impl CoseSign1TrustPack for MstTrustPack { + /// Short display name for this trust pack. + fn name(&self) -> &'static str { + "MstTrustPack" + } + + /// Return a `TrustFactProducer` instance for this pack. + fn fact_producer(&self) -> std::sync::Arc { + std::sync::Arc::new(self.clone()) + } + + /// Return the default trust plan for MST-only validation. + /// + /// This plan requires that a counter-signature receipt is trusted. + fn default_trust_plan(&self) -> Option { + use crate::validation::fluent_ext::MstReceiptTrustedWhereExt; + + // Secure-by-default MST policy: + // - require a receipt to be trusted (verification must be enabled) + let bundled = TrustPlanBuilder::new(vec![std::sync::Arc::new(self.clone())]) + .for_counter_signature(|cs| { + cs.require::(|f| f.require_receipt_trusted()) + }) + .compile() + .expect("default trust plan should be satisfiable by the MST trust pack"); + + Some(bundled.plan().clone()) + } +} + +/// Read all MST receipt blobs from the current message. +/// +/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. +fn read_receipts(ctx: &TrustFactContext<'_>) -> Result, TrustError> { + if let Some(msg) = ctx.cose_sign1_message() { + let label = CoseHeaderLabel::Int(MST_RECEIPT_HEADER_LABEL); + match msg.unprotected.get(&label) { + None => return Ok(Vec::new()), + Some(CoseHeaderValue::Array(arr)) => { + let mut result = Vec::new(); + for v in arr { + if let CoseHeaderValue::Bytes(b) = v { + result.push(b.clone()); + } else { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + } + return Ok(result); + } + Some(CoseHeaderValue::Bytes(_)) => { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + Some(_) => { + return Err(TrustError::FactProduction("invalid header".to_string())); + } + } + } + + // Without a parsed message, we cannot read receipts + Ok(Vec::new()) +} diff --git a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs index d638e520..aded6f21 100644 --- a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs +++ b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs @@ -1,731 +1,737 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use cbor_primitives::{CborDecoder, CborEncoder}; -use cose_sign1_primitives::{ - ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message, -}; -use crypto_primitives::{EcJwk, JwkVerifierFactory}; -use serde::Deserialize; -use sha2::{Digest, Sha256}; - -// Inline base64url utilities -pub(crate) const BASE64_URL_SAFE: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - -pub(crate) fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { - let mut lookup = [0xFFu8; 256]; - for (i, &c) in alphabet.iter().enumerate() { - lookup[c as usize] = i as u8; - } - - let input = input.trim_end_matches('='); - let mut out = Vec::with_capacity(input.len() * 3 / 4); - let mut buf: u32 = 0; - let mut bits: u32 = 0; - - for &b in input.as_bytes() { - let val = lookup[b as usize]; - if val == 0xFF { - return Err(format!("invalid base64 byte: 0x{:02x}", b)); - } - buf = (buf << 6) | val as u32; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((buf >> bits) as u8); - buf &= (1 << bits) - 1; - } - } - Ok(out) -} - -/// Decode base64url (no padding) to bytes. -pub fn base64url_decode(input: &str) -> Result, String> { - base64_decode(input, BASE64_URL_SAFE) -} - -#[derive(Debug)] -pub enum ReceiptVerifyError { - ReceiptDecode(String), - MissingAlg, - MissingKid, - UnsupportedAlg(i64), - UnsupportedVds(i64), - MissingVdp, - MissingProof, - MissingIssuer, - JwksParse(String), - JwksFetch(String), - JwkNotFound(String), - JwkUnsupported(String), - StatementReencode(String), - SigStructureEncode(String), - DataHashMismatch, - SignatureInvalid, -} - -impl std::fmt::Display for ReceiptVerifyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ReceiptVerifyError::ReceiptDecode(msg) => write!(f, "receipt_decode_failed: {}", msg), - ReceiptVerifyError::MissingAlg => write!(f, "receipt_missing_alg"), - ReceiptVerifyError::MissingKid => write!(f, "receipt_missing_kid"), - ReceiptVerifyError::UnsupportedAlg(alg) => write!(f, "unsupported_alg: {}", alg), - ReceiptVerifyError::UnsupportedVds(vds) => write!(f, "unsupported_vds: {}", vds), - ReceiptVerifyError::MissingVdp => write!(f, "missing_vdp"), - ReceiptVerifyError::MissingProof => write!(f, "missing_proof"), - ReceiptVerifyError::MissingIssuer => write!(f, "issuer_missing"), - ReceiptVerifyError::JwksParse(msg) => write!(f, "jwks_parse_failed: {}", msg), - ReceiptVerifyError::JwksFetch(msg) => write!(f, "jwks_fetch_failed: {}", msg), - ReceiptVerifyError::JwkNotFound(kid) => write!(f, "jwk_not_found_for_kid: {}", kid), - ReceiptVerifyError::JwkUnsupported(msg) => write!(f, "jwk_unsupported: {}", msg), - ReceiptVerifyError::StatementReencode(msg) => { - write!(f, "statement_reencode_failed: {}", msg) - } - ReceiptVerifyError::SigStructureEncode(msg) => { - write!(f, "sig_structure_encode_failed: {}", msg) - } - ReceiptVerifyError::DataHashMismatch => write!(f, "data_hash_mismatch"), - ReceiptVerifyError::SignatureInvalid => write!(f, "signature_invalid"), - } - } -} - -impl std::error::Error for ReceiptVerifyError {} - -/// MST receipt protected header label: 395. -const VDS_HEADER_LABEL: i64 = 395; -/// MST receipt unprotected header label: 396. -const VDP_HEADER_LABEL: i64 = 396; - -/// Receipt proof label inside VDP map: -1. -const PROOF_LABEL: i64 = -1; - -/// CWT (receipt) label for claims: 15. -pub const CWT_CLAIMS_LABEL: i64 = 15; -/// CWT issuer claim label: 1. -pub const CWT_ISS_LABEL: i64 = 1; - -/// COSE alg: ES384. -const COSE_ALG_ES256: i64 = -7; -const COSE_ALG_ES384: i64 = -35; - -/// MST VDS value observed for Microsoft Confidential Ledger receipts. -const MST_VDS_MICROSOFT_CCF: i64 = 2; - -#[derive(Clone)] -pub struct ReceiptVerifyInput<'a> { - pub statement_bytes_with_receipts: &'a [u8], - pub receipt_bytes: &'a [u8], - /// Offline JWKS JSON for Microsoft receipt issuers. - pub offline_jwks_json: Option<&'a str>, - - /// If true, the verifier may fetch JWKS online when offline keys are missing. - pub allow_network_fetch: bool, - - /// Optional api-version query value to use when fetching `/jwks`. - /// The CodeTransparency service typically requires this. - pub jwks_api_version: Option<&'a str>, - - /// Optional Code Transparency client for JWKS fetching. - /// If `None` and `allow_network_fetch` is true, a default client is created. - pub client: Option<&'a code_transparency_client::CodeTransparencyClient>, - - /// Factory for creating crypto verifiers from JWK public keys. - /// Callers pass a backend-specific implementation (e.g., OpenSslJwkVerifierFactory). - pub jwk_verifier_factory: &'a dyn JwkVerifierFactory, -} - -#[derive(Clone, Debug)] -pub struct ReceiptVerifyOutput { - pub trusted: bool, - pub details: Option, - pub issuer: String, - pub kid: String, - pub statement_sha256: [u8; 32], -} - -/// Verify a Microsoft Secure Transparency (MST) receipt for a COSE_Sign1 statement. -/// -/// This implements the same high-level verification strategy as the Azure .NET verifier: -/// - Parse the receipt as COSE_Sign1. -/// - Resolve the signing key from JWKS (offline first; optional online fallback). -/// - Re-encode the signed statement with unprotected headers cleared and compute SHA-256. -/// - Validate an inclusion proof whose `data_hash` matches the statement digest. -/// - Verify the receipt signature over the COSE Sig_structure using the CCF accumulator. -pub fn verify_mst_receipt( - input: ReceiptVerifyInput<'_>, -) -> Result { - let receipt = CoseSign1Message::parse(input.receipt_bytes) - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - // Extract receipt headers using typed CoseHeaderMap accessors. - let alg = receipt - .protected - .headers() - .alg() - .or_else(|| receipt.unprotected.headers().alg()) - .ok_or(ReceiptVerifyError::MissingAlg)?; - - let kid_bytes = receipt - .protected - .headers() - .kid() - .or_else(|| receipt.unprotected.headers().kid()) - .ok_or(ReceiptVerifyError::MissingKid)?; - - let kid = std::str::from_utf8(kid_bytes) - .map_err(|_| ReceiptVerifyError::MissingKid)? - .to_string(); - - let vds = receipt - .protected - .get(&CoseHeaderLabel::Int(VDS_HEADER_LABEL)) - .and_then(|v| v.as_i64()) - .ok_or(ReceiptVerifyError::UnsupportedVds(-1))?; - if vds != MST_VDS_MICROSOFT_CCF { - return Err(ReceiptVerifyError::UnsupportedVds(vds)); - } - - let issuer = get_cwt_issuer_host(receipt.protected.headers(), CWT_CLAIMS_LABEL, CWT_ISS_LABEL) - .ok_or(ReceiptVerifyError::MissingIssuer)?; - - // Map the COSE alg early so unsupported alg values are classified as UnsupportedAlg. - validate_cose_alg_supported(alg)?; - - // Resolve the receipt signing key. - // Match the Azure .NET client behavior (GetServiceCertificateKey): - // - Try offline keys first (if provided) - // - If missing and network fallback is allowed, fetch JWKS from https://{issuer}/jwks - // - Lookup key by kid - let jwk = resolve_receipt_signing_key( - issuer.as_str(), - kid.as_str(), - input.offline_jwks_json, - input.allow_network_fetch, - input.jwks_api_version, - input.client, - )?; - validate_receipt_alg_against_jwk(&jwk, alg)?; - - // Convert local Jwk to crypto_primitives::EcJwk for the trait-based factory. - let ec_jwk = local_jwk_to_ec_jwk(&jwk)?; - let verifier = input - .jwk_verifier_factory - .verifier_from_ec_jwk(&ec_jwk, alg) - .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}")))?; - - // VDP is unprotected header label 396. - let vdp_value = receipt - .unprotected - .get(&CoseHeaderLabel::Int(VDP_HEADER_LABEL)) - .ok_or(ReceiptVerifyError::MissingVdp)?; - let proof_blobs = extract_proof_blobs(vdp_value)?; - - // The .NET verifier computes claimsDigest = SHA256(signedStatementBytes) - // where signedStatementBytes is the COSE_Sign1 statement with unprotected headers cleared. - let signed_statement_bytes = - reencode_statement_with_cleared_unprotected_headers(input.statement_bytes_with_receipts)?; - let expected_data_hash = sha256(signed_statement_bytes.as_slice()); - - let mut any_matching_data_hash = false; - for proof_blob in proof_blobs { - let proof = MstCcfInclusionProof::parse(&proof_blob)?; - - // Compute CCF accumulator (leaf hash) and fold proof path. - // If the proof doesn't match this statement, try the next blob. - let mut acc = match ccf_accumulator_sha256(&proof, expected_data_hash) { - Ok(acc) => { - any_matching_data_hash = true; - acc - } - Err(ReceiptVerifyError::DataHashMismatch) => continue, - Err(e) => return Err(e), - }; - for (is_left, sibling) in proof.path.iter() { - acc = if *is_left { - sha256_concat_slices(sibling, &acc) - } else { - sha256_concat_slices(&acc, sibling) - }; - } - - let sig_structure = receipt - .sig_structure_bytes(acc.as_slice(), None) - .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string()))?; - if let Ok(true) = verifier.verify(sig_structure.as_slice(), receipt.signature()) { - return Ok(ReceiptVerifyOutput { - trusted: true, - details: None, - issuer, - kid, - statement_sha256: expected_data_hash, - }); - } - } - - if !any_matching_data_hash { - return Err(ReceiptVerifyError::DataHashMismatch); - } - - Err(ReceiptVerifyError::SignatureInvalid) -} - -/// Compute SHA-256 of `bytes`. -pub fn sha256(bytes: &[u8]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(bytes); - let out = h.finalize(); - out.into() -} - -/// Compute SHA-256 of the concatenation of two fixed-size digests. -pub fn sha256_concat_slices(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(left); - h.update(right); - let out = h.finalize(); - out.into() -} - -/// Re-encode a COSE_Sign1 statement with *all* unprotected headers cleared. -/// -/// MST receipts bind to the SHA-256 of these normalized statement bytes. -pub fn reencode_statement_with_cleared_unprotected_headers( - statement_bytes: &[u8], -) -> Result, ReceiptVerifyError> { - let was_tagged = - is_cose_sign1_tagged_18(statement_bytes).map_err(ReceiptVerifyError::StatementReencode)?; - - let msg = CoseSign1Message::parse(statement_bytes) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // Match .NET verifier behavior: clear *all* unprotected headers. - - // Encode tag(18) if it was present. - let mut enc = cose_sign1_primitives::provider::encoder(); - - if was_tagged { - // tag(18) is a single-byte CBOR tag header: 0xD2. - enc.encode_tag(18) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - } - - enc.encode_array(4) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // protected header bytes are a bstr (containing map bytes) - enc.encode_bstr(msg.protected.as_bytes()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // unprotected header: empty map - enc.encode_map(0) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // payload: bstr / nil - match msg.payload() { - Some(p) => enc.encode_bstr(p), - None => enc.encode_null(), - } - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // signature: bstr - enc.encode_bstr(msg.signature()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - Ok(enc.into_bytes()) -} - -/// Best-effort check for an initial CBOR tag 18 (COSE_Sign1). -pub fn is_cose_sign1_tagged_18(input: &[u8]) -> Result { - let mut d = cose_sign1_primitives::provider::decoder(input); - let typ = d.peek_type().map_err(|e| e.to_string())?; - if typ != cbor_primitives::CborType::Tag { - return Ok(false); - } - let tag = d.decode_tag().map_err(|e| e.to_string())?; - Ok(tag == 18) -} - -/// Resolve the receipt signing key by `kid`, using offline JWKS first and (optionally) online JWKS. -pub(crate) fn resolve_receipt_signing_key( - issuer: &str, - kid: &str, - offline_jwks_json: Option<&str>, - allow_network_fetch: bool, - jwks_api_version: Option<&str>, - client: Option<&code_transparency_client::CodeTransparencyClient>, -) -> Result { - if let Some(jwks_json) = offline_jwks_json { - match find_jwk_for_kid(jwks_json, kid) { - Ok(jwk) => return Ok(jwk), - Err(ReceiptVerifyError::JwkNotFound(_)) => {} - Err(e) => return Err(e), - } - } - - if !allow_network_fetch { - return Err(ReceiptVerifyError::JwksParse( - "MissingOfflineJwks".to_string(), - )); - } - - let jwks_json = fetch_jwks_for_issuer(issuer, jwks_api_version, client)?; - find_jwk_for_kid(jwks_json.as_str(), kid) -} - -/// Fetch the JWKS JSON for a receipt issuer using the Code Transparency client. -pub(crate) fn fetch_jwks_for_issuer( - issuer_host_or_url: &str, - jwks_api_version: Option<&str>, - client: Option<&code_transparency_client::CodeTransparencyClient>, -) -> Result { - if let Some(ct_client) = client { - return ct_client - .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())); - } - - // Create a temporary client for the issuer endpoint - let base = if issuer_host_or_url.contains("://") { - issuer_host_or_url.to_string() - } else { - format!("https://{issuer_host_or_url}") - }; - - let endpoint = - url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string()))?; - - let mut config = code_transparency_client::CodeTransparencyClientConfig::default(); - if let Some(v) = jwks_api_version { - config.api_version = v.to_string(); - } - - let temp_client = code_transparency_client::CodeTransparencyClient::new(endpoint, config); - temp_client - .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())) -} - -#[derive(Clone, Debug)] -pub struct MstCcfInclusionProof { - pub internal_txn_hash: [u8; 32], - pub internal_evidence: String, - pub data_hash: [u8; 32], - pub path: Vec<(bool, [u8; 32])>, -} - -impl MstCcfInclusionProof { - /// Parse an inclusion proof blob into a structured representation. - pub fn parse(proof_blob: &[u8]) -> Result { - Self::parse_impl(proof_blob) - } - - fn parse_impl(proof_blob: &[u8]) -> Result { - let mut d = cose_sign1_primitives::provider::decoder(proof_blob); - let map_len = d - .decode_map_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let mut leaf_raw: Option> = None; - let mut path: Option> = None; - - for _ in 0..map_len.unwrap_or(usize::MAX) { - let k = d - .decode_i64() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - if k == 1 { - leaf_raw = Some( - d.decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(), - ); - } else if k == 2 { - let v_raw = d - .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(); - path = Some(parse_path(&v_raw)?); - } else { - // Skip unknown keys - d.skip() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - } - } - - let leaf_raw = leaf_raw.ok_or(ReceiptVerifyError::MissingProof)?; - let (internal_txn_hash, internal_evidence, data_hash) = parse_leaf(leaf_raw.as_slice())?; - - Ok(Self { - internal_txn_hash, - internal_evidence, - data_hash, - path: path.ok_or(ReceiptVerifyError::MissingProof)?, - }) - } -} - -/// Parse a CCF proof leaf (array) into its components. -pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), ReceiptVerifyError> { - let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); - let _arr_len = d - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let internal_txn_hash_slice = d - .decode_bstr() - .map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) - })?; - let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_internal_txn_hash_len: {}", - internal_txn_hash_slice.len() - )) - })?; - - let internal_evidence = d - .decode_tstr() - .map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_evidence: {}", e)) - })? - .to_string(); - - let data_hash_slice = d - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; - let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_data_hash_len: {}", - data_hash_slice.len() - )) - })?; - - Ok((internal_txn_hash, internal_evidence, data_hash)) -} - -/// Parse a CCF proof path value into a sequence of (direction, sibling_hash) pairs. -pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyError> { - let mut d = cose_sign1_primitives::provider::decoder(bytes); - let arr_len = d - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let mut out = Vec::new(); - for _ in 0..arr_len.unwrap_or(usize::MAX) { - let item_raw = d - .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(); - let mut vd = cose_sign1_primitives::provider::decoder(&item_raw); - let _pair_len = vd - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let is_left = vd - .decode_bool() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e)))?; - - let bytes_item = vd - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; - let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_path_hash_len: {}", - bytes_item.len() - )) - })?; - - out.push((is_left, hash)); - } - - Ok(out) -} - -/// Extract proof blobs from the parsed VDP header value (unprotected header 396). -/// -/// The MST receipt places an array of proof blobs under label `-1` in the VDP map. -/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. -pub fn extract_proof_blobs( - vdp_value: &CoseHeaderValue, -) -> Result, ReceiptVerifyError> { - let pairs = match vdp_value { - CoseHeaderValue::Map(pairs) => pairs, - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "vdp_not_a_map".to_string(), - )) - } - }; - - for (label, value) in pairs { - if *label != CoseHeaderLabel::Int(PROOF_LABEL) { - continue; - } - - let arr = match value { - CoseHeaderValue::Array(arr) => arr, - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_not_array".to_string(), - )) - } - }; - - let mut out = Vec::new(); - for item in arr { - match item { - CoseHeaderValue::Bytes(b) => out.push(b.clone()), - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_item_not_bstr".to_string(), - )) - } - } - } - if out.is_empty() { - return Err(ReceiptVerifyError::MissingProof); - } - return Ok(out); - } - - Err(ReceiptVerifyError::MissingProof) -} - -/// Validate that the COSE alg value is a supported ECDSA algorithm. -pub fn validate_cose_alg_supported(alg: i64) -> Result<(), ReceiptVerifyError> { - match alg { - COSE_ALG_ES256 | COSE_ALG_ES384 => Ok(()), - _ => Err(ReceiptVerifyError::UnsupportedAlg(alg)), - } -} - -/// Validate that the receipt `alg` is compatible with the JWK curve. -pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), ReceiptVerifyError> { - let Some(crv) = jwk.crv.as_deref() else { - return Err(ReceiptVerifyError::JwkUnsupported( - "missing_crv".to_string(), - )); - }; - - let ok = matches!( - (crv, alg), - ("P-256", COSE_ALG_ES256) | ("P-384", COSE_ALG_ES384) - ); - - if !ok { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "alg_curve_mismatch: alg={alg} crv={crv}" - ))); - } - Ok(()) -} - -/// Compute the CCF accumulator (leaf hash) for an inclusion proof. -/// -/// Checks that the proof's `data_hash` matches the statement digest, and then -/// hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. -/// Hash field sizes are guaranteed at parse time via `[u8; 32]` types. -pub fn ccf_accumulator_sha256( - proof: &MstCcfInclusionProof, - expected_data_hash: [u8; 32], -) -> Result<[u8; 32], ReceiptVerifyError> { - if proof.data_hash != expected_data_hash { - return Err(ReceiptVerifyError::DataHashMismatch); - } - - let internal_evidence_hash = sha256(proof.internal_evidence.as_bytes()); - - let mut h = Sha256::new(); - h.update(proof.internal_txn_hash); - h.update(internal_evidence_hash); - h.update(expected_data_hash); - let out = h.finalize(); - Ok(out.into()) -} - -#[derive(Debug, Deserialize)] -struct Jwks { - keys: Vec, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Jwk { - pub kty: String, - pub crv: Option, - pub kid: Option, - pub x: Option, - pub y: Option, -} - -pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result { - let jwks: Jwks = serde_json::from_str(jwks_json) - .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string()))?; - - for k in jwks.keys { - if k.kid.as_deref() == Some(kid) { - return Ok(k); - } - } - - Err(ReceiptVerifyError::JwkNotFound(kid.to_string())) -} - -/// Convert a local (serde-parsed) JWK to a `crypto_primitives::EcJwk`. -/// -/// The local `Jwk` struct comes from JSON JWKS parsing. This function extracts -/// the EC fields needed for the backend-agnostic `JwkVerifierFactory` trait. -pub fn local_jwk_to_ec_jwk(jwk: &Jwk) -> Result { - if jwk.kty != "EC" { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "kty={}", - jwk.kty - ))); - } - - let crv = jwk - .crv - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_crv".to_string()))?; - - let x = jwk - .x - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_x".to_string()))?; - let y = jwk - .y - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_y".to_string()))?; - - Ok(EcJwk { - kty: jwk.kty.clone(), - crv: crv.to_string(), - x: x.to_string(), - y: y.to_string(), - kid: jwk.kid.clone(), - }) -} - -/// Extract the CWT issuer hostname from a protected header's CWT claims map. -/// -/// CWT claims (label `cwt_claims_label`) is a nested CBOR map containing the -/// issuer (label `iss_label`) as a text string. -pub fn get_cwt_issuer_host( - protected: &CoseHeaderMap, - cwt_claims_label: i64, - iss_label: i64, -) -> Option { - let cwt_value = protected.get(&CoseHeaderLabel::Int(cwt_claims_label))?; - match cwt_value { - CoseHeaderValue::Map(pairs) => { - for (label, value) in pairs { - if *label == CoseHeaderLabel::Int(iss_label) { - return value.as_str().map(|s| s.to_string()); - } - } - None - } - _ => None, - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cbor_primitives::{CborDecoder, CborEncoder}; +use cose_sign1_primitives::{ + ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message, +}; +use crypto_primitives::{EcJwk, JwkVerifierFactory}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; +use std::sync::Arc; + +// Inline base64url utilities +pub(crate) const BASE64_URL_SAFE: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +pub(crate) fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { + let mut lookup = [0xFFu8; 256]; + for (i, &c) in alphabet.iter().enumerate() { + lookup[c as usize] = i as u8; + } + + let input = input.trim_end_matches('='); + let mut out = Vec::with_capacity(input.len() * 3 / 4); + let mut buf: u32 = 0; + let mut bits: u32 = 0; + + for &b in input.as_bytes() { + let val = lookup[b as usize]; + if val == 0xFF { + return Err(format!("invalid base64 byte: 0x{:02x}", b)); + } + buf = (buf << 6) | val as u32; + bits += 6; + if bits >= 8 { + bits -= 8; + out.push((buf >> bits) as u8); + buf &= (1 << bits) - 1; + } + } + Ok(out) +} + +/// Decode base64url (no padding) to bytes. +pub fn base64url_decode(input: &str) -> Result, String> { + base64_decode(input, BASE64_URL_SAFE) +} + +#[derive(Debug)] +pub enum ReceiptVerifyError { + ReceiptDecode(String), + MissingAlg, + MissingKid, + UnsupportedAlg(i64), + UnsupportedVds(i64), + MissingVdp, + MissingProof, + MissingIssuer, + JwksParse(String), + JwksFetch(String), + JwkNotFound(String), + JwkUnsupported(String), + StatementReencode(String), + SigStructureEncode(String), + DataHashMismatch, + SignatureInvalid, +} + +impl std::fmt::Display for ReceiptVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReceiptVerifyError::ReceiptDecode(msg) => write!(f, "receipt_decode_failed: {}", msg), + ReceiptVerifyError::MissingAlg => write!(f, "receipt_missing_alg"), + ReceiptVerifyError::MissingKid => write!(f, "receipt_missing_kid"), + ReceiptVerifyError::UnsupportedAlg(alg) => write!(f, "unsupported_alg: {}", alg), + ReceiptVerifyError::UnsupportedVds(vds) => write!(f, "unsupported_vds: {}", vds), + ReceiptVerifyError::MissingVdp => write!(f, "missing_vdp"), + ReceiptVerifyError::MissingProof => write!(f, "missing_proof"), + ReceiptVerifyError::MissingIssuer => write!(f, "issuer_missing"), + ReceiptVerifyError::JwksParse(msg) => write!(f, "jwks_parse_failed: {}", msg), + ReceiptVerifyError::JwksFetch(msg) => write!(f, "jwks_fetch_failed: {}", msg), + ReceiptVerifyError::JwkNotFound(kid) => write!(f, "jwk_not_found_for_kid: {}", kid), + ReceiptVerifyError::JwkUnsupported(msg) => write!(f, "jwk_unsupported: {}", msg), + ReceiptVerifyError::StatementReencode(msg) => { + write!(f, "statement_reencode_failed: {}", msg) + } + ReceiptVerifyError::SigStructureEncode(msg) => { + write!(f, "sig_structure_encode_failed: {}", msg) + } + ReceiptVerifyError::DataHashMismatch => write!(f, "data_hash_mismatch"), + ReceiptVerifyError::SignatureInvalid => write!(f, "signature_invalid"), + } + } +} + +impl std::error::Error for ReceiptVerifyError {} + +/// MST receipt protected header label: 395. +const VDS_HEADER_LABEL: i64 = 395; +/// MST receipt unprotected header label: 396. +const VDP_HEADER_LABEL: i64 = 396; + +/// Receipt proof label inside VDP map: -1. +const PROOF_LABEL: i64 = -1; + +/// CWT (receipt) label for claims: 15. +pub const CWT_CLAIMS_LABEL: i64 = 15; +/// CWT issuer claim label: 1. +pub const CWT_ISS_LABEL: i64 = 1; + +/// COSE alg: ES384. +const COSE_ALG_ES256: i64 = -7; +const COSE_ALG_ES384: i64 = -35; + +/// MST VDS value observed for Microsoft Confidential Ledger receipts. +const MST_VDS_MICROSOFT_CCF: i64 = 2; + +#[derive(Clone)] +pub struct ReceiptVerifyInput<'a> { + pub statement_bytes_with_receipts: &'a [u8], + pub receipt_bytes: &'a [u8], + /// Offline JWKS JSON for Microsoft receipt issuers. + pub offline_jwks_json: Option<&'a str>, + + /// If true, the verifier may fetch JWKS online when offline keys are missing. + pub allow_network_fetch: bool, + + /// Optional api-version query value to use when fetching `/jwks`. + /// The CodeTransparency service typically requires this. + pub jwks_api_version: Option<&'a str>, + + /// Optional Code Transparency client for JWKS fetching. + /// If `None` and `allow_network_fetch` is true, a default client is created. + pub client: Option<&'a code_transparency_client::CodeTransparencyClient>, + + /// Factory for creating crypto verifiers from JWK public keys. + /// Callers pass a backend-specific implementation (e.g., OpenSslJwkVerifierFactory). + pub jwk_verifier_factory: &'a dyn JwkVerifierFactory, +} + +#[derive(Clone, Debug)] +pub struct ReceiptVerifyOutput { + pub trusted: bool, + pub details: Option>, + pub issuer: Arc, + pub kid: Arc, + pub statement_sha256: [u8; 32], +} + +/// Verify a Microsoft Secure Transparency (MST) receipt for a COSE_Sign1 statement. +/// +/// This implements the same high-level verification strategy as the Azure .NET verifier: +/// - Parse the receipt as COSE_Sign1. +/// - Resolve the signing key from JWKS (offline first; optional online fallback). +/// - Re-encode the signed statement with unprotected headers cleared and compute SHA-256. +/// - Validate an inclusion proof whose `data_hash` matches the statement digest. +/// - Verify the receipt signature over the COSE Sig_structure using the CCF accumulator. +pub fn verify_mst_receipt( + input: ReceiptVerifyInput<'_>, +) -> Result { + let receipt = CoseSign1Message::parse(input.receipt_bytes) + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + // Extract receipt headers using typed CoseHeaderMap accessors. + let alg = receipt + .protected + .headers() + .alg() + .or_else(|| receipt.unprotected.headers().alg()) + .ok_or(ReceiptVerifyError::MissingAlg)?; + + let kid_bytes = receipt + .protected + .headers() + .kid() + .or_else(|| receipt.unprotected.headers().kid()) + .ok_or(ReceiptVerifyError::MissingKid)?; + + let kid = std::str::from_utf8(kid_bytes) + .map_err(|_| ReceiptVerifyError::MissingKid)? + .to_string(); + + let vds = receipt + .protected + .get(&CoseHeaderLabel::Int(VDS_HEADER_LABEL)) + .and_then(|v| v.as_i64()) + .ok_or(ReceiptVerifyError::UnsupportedVds(-1))?; + if vds != MST_VDS_MICROSOFT_CCF { + return Err(ReceiptVerifyError::UnsupportedVds(vds)); + } + + let issuer = get_cwt_issuer_host(receipt.protected.headers(), CWT_CLAIMS_LABEL, CWT_ISS_LABEL) + .ok_or(ReceiptVerifyError::MissingIssuer)?; + + // Map the COSE alg early so unsupported alg values are classified as UnsupportedAlg. + validate_cose_alg_supported(alg)?; + + // Resolve the receipt signing key. + // Match the Azure .NET client behavior (GetServiceCertificateKey): + // - Try offline keys first (if provided) + // - If missing and network fallback is allowed, fetch JWKS from https://{issuer}/jwks + // - Lookup key by kid + let jwk = resolve_receipt_signing_key( + issuer.as_str(), + kid.as_str(), + input.offline_jwks_json, + input.allow_network_fetch, + input.jwks_api_version, + input.client, + )?; + validate_receipt_alg_against_jwk(&jwk, alg)?; + + // Convert local Jwk to crypto_primitives::EcJwk for the trait-based factory. + let ec_jwk = local_jwk_to_ec_jwk(&jwk)?; + let verifier = input + .jwk_verifier_factory + .verifier_from_ec_jwk(&ec_jwk, alg) + .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}")))?; + + // Convert to Arc for cheap cloning in fact production. + let issuer: Arc = Arc::from(issuer); + let kid: Arc = Arc::from(kid); + + // VDP is unprotected header label 396. + let vdp_value = receipt + .unprotected + .get(&CoseHeaderLabel::Int(VDP_HEADER_LABEL)) + .ok_or(ReceiptVerifyError::MissingVdp)?; + let proof_blobs = extract_proof_blobs(vdp_value)?; + + // The .NET verifier computes claimsDigest = SHA256(signedStatementBytes) + // where signedStatementBytes is the COSE_Sign1 statement with unprotected headers cleared. + let signed_statement_bytes = + reencode_statement_with_cleared_unprotected_headers(input.statement_bytes_with_receipts)?; + let expected_data_hash = sha256(signed_statement_bytes.as_slice()); + + let mut any_matching_data_hash = false; + for proof_blob in proof_blobs { + let proof = MstCcfInclusionProof::parse(&proof_blob)?; + + // Compute CCF accumulator (leaf hash) and fold proof path. + // If the proof doesn't match this statement, try the next blob. + let mut acc = match ccf_accumulator_sha256(&proof, expected_data_hash) { + Ok(acc) => { + any_matching_data_hash = true; + acc + } + Err(ReceiptVerifyError::DataHashMismatch) => continue, + Err(e) => return Err(e), + }; + for (is_left, sibling) in proof.path.iter() { + acc = if *is_left { + sha256_concat_slices(sibling, &acc) + } else { + sha256_concat_slices(&acc, sibling) + }; + } + + let sig_structure = receipt + .sig_structure_bytes(acc.as_slice(), None) + .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string()))?; + if let Ok(true) = verifier.verify(sig_structure.as_slice(), receipt.signature()) { + return Ok(ReceiptVerifyOutput { + trusted: true, + details: None, + issuer, + kid, + statement_sha256: expected_data_hash, + }); + } + } + + if !any_matching_data_hash { + return Err(ReceiptVerifyError::DataHashMismatch); + } + + Err(ReceiptVerifyError::SignatureInvalid) +} + +/// Compute SHA-256 of `bytes`. +pub fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(bytes); + let out = h.finalize(); + out.into() +} + +/// Compute SHA-256 of the concatenation of two fixed-size digests. +pub fn sha256_concat_slices(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(left); + h.update(right); + let out = h.finalize(); + out.into() +} + +/// Re-encode a COSE_Sign1 statement with *all* unprotected headers cleared. +/// +/// MST receipts bind to the SHA-256 of these normalized statement bytes. +pub fn reencode_statement_with_cleared_unprotected_headers( + statement_bytes: &[u8], +) -> Result, ReceiptVerifyError> { + let was_tagged = + is_cose_sign1_tagged_18(statement_bytes).map_err(ReceiptVerifyError::StatementReencode)?; + + let msg = CoseSign1Message::parse(statement_bytes) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // Match .NET verifier behavior: clear *all* unprotected headers. + + // Encode tag(18) if it was present. + let mut enc = cose_sign1_primitives::provider::encoder(); + + if was_tagged { + // tag(18) is a single-byte CBOR tag header: 0xD2. + enc.encode_tag(18) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + } + + enc.encode_array(4) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // protected header bytes are a bstr (containing map bytes) + enc.encode_bstr(msg.protected.as_bytes()) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // unprotected header: empty map + enc.encode_map(0) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // payload: bstr / nil + match msg.payload() { + Some(p) => enc.encode_bstr(p), + None => enc.encode_null(), + } + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // signature: bstr + enc.encode_bstr(msg.signature()) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + Ok(enc.into_bytes()) +} + +/// Best-effort check for an initial CBOR tag 18 (COSE_Sign1). +pub fn is_cose_sign1_tagged_18(input: &[u8]) -> Result { + let mut d = cose_sign1_primitives::provider::decoder(input); + let typ = d.peek_type().map_err(|e| e.to_string())?; + if typ != cbor_primitives::CborType::Tag { + return Ok(false); + } + let tag = d.decode_tag().map_err(|e| e.to_string())?; + Ok(tag == 18) +} + +/// Resolve the receipt signing key by `kid`, using offline JWKS first and (optionally) online JWKS. +pub(crate) fn resolve_receipt_signing_key( + issuer: &str, + kid: &str, + offline_jwks_json: Option<&str>, + allow_network_fetch: bool, + jwks_api_version: Option<&str>, + client: Option<&code_transparency_client::CodeTransparencyClient>, +) -> Result { + if let Some(jwks_json) = offline_jwks_json { + match find_jwk_for_kid(jwks_json, kid) { + Ok(jwk) => return Ok(jwk), + Err(ReceiptVerifyError::JwkNotFound(_)) => {} + Err(e) => return Err(e), + } + } + + if !allow_network_fetch { + return Err(ReceiptVerifyError::JwksParse( + "MissingOfflineJwks".to_string(), + )); + } + + let jwks_json = fetch_jwks_for_issuer(issuer, jwks_api_version, client)?; + find_jwk_for_kid(jwks_json.as_str(), kid) +} + +/// Fetch the JWKS JSON for a receipt issuer using the Code Transparency client. +pub(crate) fn fetch_jwks_for_issuer( + issuer_host_or_url: &str, + jwks_api_version: Option<&str>, + client: Option<&code_transparency_client::CodeTransparencyClient>, +) -> Result { + if let Some(ct_client) = client { + return ct_client + .get_public_keys() + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())); + } + + // Create a temporary client for the issuer endpoint + let base = if issuer_host_or_url.contains("://") { + issuer_host_or_url.to_string() + } else { + format!("https://{issuer_host_or_url}") + }; + + let endpoint = + url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string()))?; + + let mut config = code_transparency_client::CodeTransparencyClientConfig::default(); + if let Some(v) = jwks_api_version { + config.api_version = v.to_string(); + } + + let temp_client = code_transparency_client::CodeTransparencyClient::new(endpoint, config); + temp_client + .get_public_keys() + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())) +} + +#[derive(Clone, Debug)] +pub struct MstCcfInclusionProof { + pub internal_txn_hash: [u8; 32], + pub internal_evidence: String, + pub data_hash: [u8; 32], + pub path: Vec<(bool, [u8; 32])>, +} + +impl MstCcfInclusionProof { + /// Parse an inclusion proof blob into a structured representation. + pub fn parse(proof_blob: &[u8]) -> Result { + Self::parse_impl(proof_blob) + } + + fn parse_impl(proof_blob: &[u8]) -> Result { + let mut d = cose_sign1_primitives::provider::decoder(proof_blob); + let map_len = d + .decode_map_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let mut leaf_raw: Option> = None; + let mut path: Option> = None; + + for _ in 0..map_len.unwrap_or(usize::MAX) { + let k = d + .decode_i64() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + if k == 1 { + leaf_raw = Some( + d.decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(), + ); + } else if k == 2 { + let v_raw = d + .decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(); + path = Some(parse_path(&v_raw)?); + } else { + // Skip unknown keys + d.skip() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + } + } + + let leaf_raw = leaf_raw.ok_or(ReceiptVerifyError::MissingProof)?; + let (internal_txn_hash, internal_evidence, data_hash) = parse_leaf(leaf_raw.as_slice())?; + + Ok(Self { + internal_txn_hash, + internal_evidence, + data_hash, + path: path.ok_or(ReceiptVerifyError::MissingProof)?, + }) + } +} + +/// Parse a CCF proof leaf (array) into its components. +pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), ReceiptVerifyError> { + let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); + let _arr_len = d + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let internal_txn_hash_slice = d + .decode_bstr() + .map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) + })?; + let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_internal_txn_hash_len: {}", + internal_txn_hash_slice.len() + )) + })?; + + let internal_evidence = d + .decode_tstr() + .map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_evidence: {}", e)) + })? + .to_string(); + + let data_hash_slice = d + .decode_bstr() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; + let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_data_hash_len: {}", + data_hash_slice.len() + )) + })?; + + Ok((internal_txn_hash, internal_evidence, data_hash)) +} + +/// Parse a CCF proof path value into a sequence of (direction, sibling_hash) pairs. +pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyError> { + let mut d = cose_sign1_primitives::provider::decoder(bytes); + let arr_len = d + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let mut out = Vec::new(); + for _ in 0..arr_len.unwrap_or(usize::MAX) { + let item_raw = d + .decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(); + let mut vd = cose_sign1_primitives::provider::decoder(&item_raw); + let _pair_len = vd + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let is_left = vd + .decode_bool() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e)))?; + + let bytes_item = vd + .decode_bstr() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; + let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_path_hash_len: {}", + bytes_item.len() + )) + })?; + + out.push((is_left, hash)); + } + + Ok(out) +} + +/// Extract proof blobs from the parsed VDP header value (unprotected header 396). +/// +/// The MST receipt places an array of proof blobs under label `-1` in the VDP map. +/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. +pub fn extract_proof_blobs( + vdp_value: &CoseHeaderValue, +) -> Result, ReceiptVerifyError> { + let pairs = match vdp_value { + CoseHeaderValue::Map(pairs) => pairs, + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "vdp_not_a_map".to_string(), + )) + } + }; + + for (label, value) in pairs { + if *label != CoseHeaderLabel::Int(PROOF_LABEL) { + continue; + } + + let arr = match value { + CoseHeaderValue::Array(arr) => arr, + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "proof_not_array".to_string(), + )) + } + }; + + let mut out = Vec::new(); + for item in arr { + match item { + CoseHeaderValue::Bytes(b) => out.push(b.clone()), + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "proof_item_not_bstr".to_string(), + )) + } + } + } + if out.is_empty() { + return Err(ReceiptVerifyError::MissingProof); + } + return Ok(out); + } + + Err(ReceiptVerifyError::MissingProof) +} + +/// Validate that the COSE alg value is a supported ECDSA algorithm. +pub fn validate_cose_alg_supported(alg: i64) -> Result<(), ReceiptVerifyError> { + match alg { + COSE_ALG_ES256 | COSE_ALG_ES384 => Ok(()), + _ => Err(ReceiptVerifyError::UnsupportedAlg(alg)), + } +} + +/// Validate that the receipt `alg` is compatible with the JWK curve. +pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), ReceiptVerifyError> { + let Some(crv) = jwk.crv.as_deref() else { + return Err(ReceiptVerifyError::JwkUnsupported( + "missing_crv".to_string(), + )); + }; + + let ok = matches!( + (crv, alg), + ("P-256", COSE_ALG_ES256) | ("P-384", COSE_ALG_ES384) + ); + + if !ok { + return Err(ReceiptVerifyError::JwkUnsupported(format!( + "alg_curve_mismatch: alg={alg} crv={crv}" + ))); + } + Ok(()) +} + +/// Compute the CCF accumulator (leaf hash) for an inclusion proof. +/// +/// Checks that the proof's `data_hash` matches the statement digest, and then +/// hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. +/// Hash field sizes are guaranteed at parse time via `[u8; 32]` types. +pub fn ccf_accumulator_sha256( + proof: &MstCcfInclusionProof, + expected_data_hash: [u8; 32], +) -> Result<[u8; 32], ReceiptVerifyError> { + if proof.data_hash != expected_data_hash { + return Err(ReceiptVerifyError::DataHashMismatch); + } + + let internal_evidence_hash = sha256(proof.internal_evidence.as_bytes()); + + let mut h = Sha256::new(); + h.update(proof.internal_txn_hash); + h.update(internal_evidence_hash); + h.update(expected_data_hash); + let out = h.finalize(); + Ok(out.into()) +} + +#[derive(Debug, Deserialize)] +struct Jwks { + keys: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Jwk { + pub kty: String, + pub crv: Option, + pub kid: Option, + pub x: Option, + pub y: Option, +} + +pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result { + let jwks: Jwks = serde_json::from_str(jwks_json) + .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string()))?; + + for k in jwks.keys { + if k.kid.as_deref() == Some(kid) { + return Ok(k); + } + } + + Err(ReceiptVerifyError::JwkNotFound(kid.to_string())) +} + +/// Convert a local (serde-parsed) JWK to a `crypto_primitives::EcJwk`. +/// +/// The local `Jwk` struct comes from JSON JWKS parsing. This function extracts +/// the EC fields needed for the backend-agnostic `JwkVerifierFactory` trait. +pub fn local_jwk_to_ec_jwk<'a>(jwk: &'a Jwk) -> Result, ReceiptVerifyError> { + if jwk.kty != "EC" { + return Err(ReceiptVerifyError::JwkUnsupported(format!( + "kty={}", + jwk.kty + ))); + } + + let crv = jwk + .crv + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_crv".to_string()))?; + + let x = jwk + .x + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_x".to_string()))?; + let y = jwk + .y + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_y".to_string()))?; + + Ok(EcJwk { + kty: Cow::Borrowed(&jwk.kty), + crv: Cow::Borrowed(crv), + x: Cow::Borrowed(x), + y: Cow::Borrowed(y), + kid: jwk.kid.as_deref().map(Cow::Borrowed), + }) +} + +/// Extract the CWT issuer hostname from a protected header's CWT claims map. +/// +/// CWT claims (label `cwt_claims_label`) is a nested CBOR map containing the +/// issuer (label `iss_label`) as a text string. +pub fn get_cwt_issuer_host( + protected: &CoseHeaderMap, + cwt_claims_label: i64, + iss_label: i64, +) -> Option { + let cwt_value = protected.get(&CoseHeaderLabel::Int(cwt_claims_label))?; + match cwt_value { + CoseHeaderValue::Map(pairs) => { + for (label, value) in pairs { + if *label == CoseHeaderLabel::Int(iss_label) { + return value.as_str().map(|s| s.to_string()); + } + } + None + } + _ => None, + } +} diff --git a/native/rust/extension_packs/mst/tests/facts_properties.rs b/native/rust/extension_packs/mst/tests/facts_properties.rs index bc4f3e9a..521d0faa 100644 --- a/native/rust/extension_packs/mst/tests/facts_properties.rs +++ b/native/rust/extension_packs/mst/tests/facts_properties.rs @@ -7,6 +7,7 @@ use cose_sign1_transparent_mst::validation::facts::{ MstReceiptStatementSha256Fact, MstReceiptTrustedFact, }; use cose_sign1_validation_primitives::fact_properties::FactProperties; +use std::sync::Arc; #[test] fn mst_fact_properties_unknown_fields_return_none() { @@ -22,25 +23,25 @@ fn mst_fact_properties_unknown_fields_return_none() { .is_none()); assert!(MstReceiptIssuerFact { - issuer: "example.com".to_string(), + issuer: Arc::from("example.com"), } .get_property("unknown") .is_none()); assert!(MstReceiptKidFact { - kid: "kid".to_string(), + kid: Arc::from("kid"), } .get_property("unknown") .is_none()); assert!(MstReceiptStatementSha256Fact { - sha256_hex: "00".repeat(32), + sha256_hex: Arc::from("00".repeat(32).as_str()), } .get_property("unknown") .is_none()); assert!(MstReceiptStatementCoverageFact { - coverage: "coverage".to_string(), + coverage: "coverage", } .get_property("unknown") .is_none()); diff --git a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs index cb4a4e09..0e773b16 100644 --- a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs @@ -13,6 +13,7 @@ extern crate cbor_primitives_everparse; use cbor_primitives::CborEncoder; use cose_sign1_transparent_mst::validation::receipt_verify::*; +use std::borrow::Cow; use crypto_primitives::EcJwk; // ============================================================================ @@ -443,7 +444,7 @@ fn test_local_jwk_to_ec_jwk_p384_valid() { ec_jwk.y, "mLgl1xH0TKP0VFl_0umg0Q6HBEUL0umg0Q6HBEUL0umg0Q6HBEUL0umg0Q6HBEUL" ); - assert_eq!(ec_jwk.kid, Some("my-p384-key".to_string())); + assert_eq!(ec_jwk.kid, Some(Cow::Borrowed("my-p384-key"))); } #[test] diff --git a/native/rust/extension_packs/mst/tests/fluent_ext_coverage.rs b/native/rust/extension_packs/mst/tests/fluent_ext_coverage.rs index 9a8f8392..a1864bdf 100644 --- a/native/rust/extension_packs/mst/tests/fluent_ext_coverage.rs +++ b/native/rust/extension_packs/mst/tests/fluent_ext_coverage.rs @@ -66,22 +66,22 @@ fn mst_facts_expose_declarative_properties() { assert!(present.get_property("no_such_field").is_none()); let issuer = MstReceiptIssuerFact { - issuer: "issuer".to_string(), + issuer: Arc::from("issuer"), }; assert!(issuer.get_property("issuer").is_some()); let kid = MstReceiptKidFact { - kid: "kid".to_string(), + kid: Arc::from("kid"), }; assert!(kid.get_property("kid").is_some()); let sha = MstReceiptStatementSha256Fact { - sha256_hex: "00".to_string(), + sha256_hex: Arc::from("00"), }; assert!(sha.get_property("sha256_hex").is_some()); let coverage = MstReceiptStatementCoverageFact { - coverage: "coverage".to_string(), + coverage: "coverage", }; assert!(coverage.get_property("coverage").is_some()); @@ -90,7 +90,7 @@ fn mst_facts_expose_declarative_properties() { let trusted = MstReceiptTrustedFact { trusted: true, - details: Some("ok".to_string()), + details: Some(Arc::from("ok")), }; assert!(trusted.get_property("trusted").is_some()); } diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs b/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs index 0fc4cc25..09449c44 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs @@ -7,6 +7,7 @@ use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; use cose_sign1_transparent_mst::validation::receipt_verify::{ verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, ReceiptVerifyOutput, }; +use std::sync::Arc; #[test] fn test_verify_mst_receipt_invalid_cbor() { @@ -202,16 +203,16 @@ fn test_receipt_verify_input_construction() { fn test_receipt_verify_output_construction() { let output = ReceiptVerifyOutput { trusted: true, - details: Some("verification successful".to_string()), - issuer: "example.com".to_string(), - kid: "key123".to_string(), + details: Some(Arc::from("verification successful")), + issuer: Arc::from("example.com"), + kid: Arc::from("key123"), statement_sha256: [0u8; 32], }; assert_eq!(output.trusted, true); - assert_eq!(output.details, Some("verification successful".to_string())); - assert_eq!(output.issuer, "example.com"); - assert_eq!(output.kid, "key123"); + assert_eq!(output.details.as_deref(), Some("verification successful")); + assert_eq!(&*output.issuer, "example.com"); + assert_eq!(&*output.kid, "key123"); assert_eq!(output.statement_sha256, [0u8; 32]); } diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs b/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs index bea22d7a..e536be2f 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs @@ -5,6 +5,8 @@ use cbor_primitives::CborEncoder; +use std::borrow::Cow; + use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; use cose_sign1_transparent_mst::validation::receipt_verify::{ base64url_decode, find_jwk_for_kid, is_cose_sign1_tagged_18, local_jwk_to_ec_jwk, sha256, @@ -134,7 +136,7 @@ fn test_local_jwk_to_ec_jwk_p384_valid() { assert_eq!(ec.crv, "P-384"); assert_eq!(ec.x, x_b64); assert_eq!(ec.y, y_b64); - assert_eq!(ec.kid, Some("test-key".to_string())); + assert_eq!(ec.kid, Some(Cow::Borrowed("test-key"))); } #[test] diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_helpers.rs b/native/rust/extension_packs/mst/tests/receipt_verify_helpers.rs index 8875a638..00b1b53c 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_helpers.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_helpers.rs @@ -8,6 +8,7 @@ use cose_sign1_transparent_mst::validation::receipt_verify::{ sha256_concat_slices, validate_receipt_alg_against_jwk, Jwk, ReceiptVerifyError, }; use crypto_primitives::EcJwk; +use std::borrow::Cow; #[test] fn test_sha256_basic() { @@ -211,7 +212,7 @@ fn test_local_jwk_to_ec_jwk_p256() { assert_eq!(ec_jwk.crv, "P-256"); assert_eq!(ec_jwk.x, x_b64); assert_eq!(ec_jwk.y, y_b64); - assert_eq!(ec_jwk.kid, Some("test-key".to_string())); + assert_eq!(ec_jwk.kid, Some(Cow::Borrowed("test-key"))); } #[test] @@ -235,7 +236,7 @@ fn test_local_jwk_to_ec_jwk_p384() { assert_eq!(ec_jwk.crv, "P-384"); assert_eq!(ec_jwk.x, x_b64); assert_eq!(ec_jwk.y, y_b64); - assert_eq!(ec_jwk.kid, Some("test-key-384".to_string())); + assert_eq!(ec_jwk.kid, Some(Cow::Borrowed("test-key-384"))); } #[test] diff --git a/native/rust/primitives/crypto/openssl/ffi/src/lib.rs b/native/rust/primitives/crypto/openssl/ffi/src/lib.rs index 84b9fdfd..e78231af 100644 --- a/native/rust/primitives/crypto/openssl/ffi/src/lib.rs +++ b/native/rust/primitives/crypto/openssl/ffi/src/lib.rs @@ -617,14 +617,14 @@ pub unsafe extern "C" fn cose_crypto_openssl_jwk_verifier_from_ec( } let ec_jwk = EcJwk { - kty: "EC".to_string(), - crv: cstr_to_string(crv, "crv")?, - x: cstr_to_string(x, "x")?, - y: cstr_to_string(y, "y")?, + kty: "EC".into(), + crv: cstr_to_string(crv, "crv")?.into(), + x: cstr_to_string(x, "x")?.into(), + y: cstr_to_string(y, "y")?.into(), kid: if kid.is_null() { None } else { - Some(cstr_to_string(kid, "kid")?) + Some(cstr_to_string(kid, "kid")?.into()) }, }; diff --git a/native/rust/primitives/crypto/openssl/src/jwk_verifier.rs b/native/rust/primitives/crypto/openssl/src/jwk_verifier.rs index 93cedc20..7fc7f41d 100644 --- a/native/rust/primitives/crypto/openssl/src/jwk_verifier.rs +++ b/native/rust/primitives/crypto/openssl/src/jwk_verifier.rs @@ -55,7 +55,7 @@ pub struct OpenSslJwkVerifierFactory; impl JwkVerifierFactory for OpenSslJwkVerifierFactory { fn verifier_from_ec_jwk( &self, - jwk: &EcJwk, + jwk: &EcJwk<'_>, cose_algorithm: i64, ) -> Result, CryptoError> { if jwk.kty != "EC" { @@ -65,7 +65,7 @@ impl JwkVerifierFactory for OpenSslJwkVerifierFactory { ))); } - let expected_len = match jwk.crv.as_str() { + let expected_len = match &*jwk.crv { "P-256" => 32, "P-384" => 48, "P-521" => 66, diff --git a/native/rust/primitives/crypto/openssl/tests/jwk_verifier_tests.rs b/native/rust/primitives/crypto/openssl/tests/jwk_verifier_tests.rs index 06224ca0..49d7f5ec 100644 --- a/native/rust/primitives/crypto/openssl/tests/jwk_verifier_tests.rs +++ b/native/rust/primitives/crypto/openssl/tests/jwk_verifier_tests.rs @@ -1,377 +1,377 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Tests for JWK → CryptoVerifier conversion via OpenSslJwkVerifierFactory. -//! -//! Covers: -//! - EC JWK (P-256, P-384) → verifier creation and signature verification -//! - RSA JWK → verifier creation -//! - Invalid JWK handling (wrong kty, bad coordinates, unsupported curves) -//! - Key conversion (ec_point_to_spki_der) -//! - Base64url decoding - -use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; -use cose_sign1_crypto_openssl::key_conversion::ec_point_to_spki_der; -use crypto_primitives::{CryptoVerifier, EcJwk, Jwk, JwkVerifierFactory, RsaJwk}; - -use base64::Engine; -use openssl::ec::{EcGroup, EcKey}; -use openssl::nid::Nid; -use openssl::pkey::PKey; - -fn b64url(data: &[u8]) -> String { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data) -} - -/// Generate a real P-256 key pair and return (private_pkey, EcJwk). -fn generate_p256_jwk() -> (PKey, EcJwk) { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key.clone()).unwrap(); - - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let mut x = openssl::bn::BigNum::new().unwrap(); - let mut y = openssl::bn::BigNum::new().unwrap(); - ec_key - .public_key() - .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx) - .unwrap(); - - let x_bytes = x.to_vec(); - let y_bytes = y.to_vec(); - // Pad to 32 bytes for P-256 - let mut x_padded = vec![0u8; 32 - x_bytes.len()]; - x_padded.extend_from_slice(&x_bytes); - let mut y_padded = vec![0u8; 32 - y_bytes.len()]; - y_padded.extend_from_slice(&y_bytes); - - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: b64url(&x_padded), - y: b64url(&y_padded), - kid: Some("test-p256".to_string()), - }; - - (pkey, jwk) -} - -/// Generate a real P-384 key pair and return (private_pkey, EcJwk). -fn generate_p384_jwk() -> (PKey, EcJwk) { - let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key.clone()).unwrap(); - - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let mut x = openssl::bn::BigNum::new().unwrap(); - let mut y = openssl::bn::BigNum::new().unwrap(); - ec_key - .public_key() - .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx) - .unwrap(); - - let x_bytes = x.to_vec(); - let y_bytes = y.to_vec(); - let mut x_padded = vec![0u8; 48 - x_bytes.len()]; - x_padded.extend_from_slice(&x_bytes); - let mut y_padded = vec![0u8; 48 - y_bytes.len()]; - y_padded.extend_from_slice(&y_bytes); - - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-384".to_string(), - x: b64url(&x_padded), - y: b64url(&y_padded), - kid: Some("test-p384".to_string()), - }; - - (pkey, jwk) -} - -/// Generate an RSA key pair and return (private_pkey, RsaJwk). -fn generate_rsa_jwk() -> (PKey, RsaJwk) { - let rsa = openssl::rsa::Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa.clone()).unwrap(); - - let n = rsa.n().to_vec(); - let e = rsa.e().to_vec(); - - let jwk = RsaJwk { - kty: "RSA".to_string(), - n: b64url(&n), - e: b64url(&e), - kid: Some("test-rsa".to_string()), - }; - - (pkey, jwk) -} - -// ==================== EC JWK Tests ==================== - -#[test] -fn ec_p256_jwk_creates_verifier() { - let factory = OpenSslJwkVerifierFactory; - let (_pkey, jwk) = generate_p256_jwk(); - - let verifier = factory.verifier_from_ec_jwk(&jwk, -7); // ES256 - assert!( - verifier.is_ok(), - "P-256 JWK should create verifier: {:?}", - verifier.err() - ); - assert_eq!(verifier.unwrap().algorithm(), -7); -} - -#[test] -fn ec_p384_jwk_creates_verifier() { - let factory = OpenSslJwkVerifierFactory; - let (_pkey, jwk) = generate_p384_jwk(); - - let verifier = factory.verifier_from_ec_jwk(&jwk, -35); // ES384 - assert!( - verifier.is_ok(), - "P-384 JWK should create verifier: {:?}", - verifier.err() - ); - assert_eq!(verifier.unwrap().algorithm(), -35); -} - -#[test] -fn ec_p256_jwk_verifies_signature() { - let factory = OpenSslJwkVerifierFactory; - let (pkey, jwk) = generate_p256_jwk(); - - // Sign some data with the private key - let data = b"test data for ES256 signature verification"; - let mut signer = - openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); - let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); - // Convert DER → fixed r||s format (COSE uses fixed-length) - let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 64).unwrap(); - - // Create verifier from JWK and verify - let verifier = factory.verifier_from_ec_jwk(&jwk, -7).unwrap(); - let result = verifier.verify(data, &fixed_sig); - assert!(result.is_ok()); - assert!(result.unwrap(), "Signature should verify with matching key"); -} - -#[test] -fn ec_p384_jwk_verifies_signature() { - let factory = OpenSslJwkVerifierFactory; - let (pkey, jwk) = generate_p384_jwk(); - - let data = b"test data for ES384 signature verification"; - let mut signer = - openssl::sign::Signer::new(openssl::hash::MessageDigest::sha384(), &pkey).unwrap(); - let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); - let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 96).unwrap(); - - let verifier = factory.verifier_from_ec_jwk(&jwk, -35).unwrap(); - let result = verifier.verify(data, &fixed_sig); - assert!(result.is_ok()); - assert!(result.unwrap(), "ES384 signature should verify"); -} - -#[test] -fn ec_jwk_wrong_key_rejects_signature() { - let factory = OpenSslJwkVerifierFactory; - let (pkey, _jwk1) = generate_p256_jwk(); - let (_pkey2, jwk2) = generate_p256_jwk(); // different key - - let data = b"signed with key 1"; - let mut signer = - openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); - let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); - let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 64).unwrap(); - - // Verify with DIFFERENT key should fail - let verifier = factory.verifier_from_ec_jwk(&jwk2, -7).unwrap(); - let result = verifier.verify(data, &fixed_sig); - assert!(result.is_ok()); - assert!(!result.unwrap(), "Wrong key should reject signature"); -} - -// ==================== EC JWK Error Cases ==================== - -#[test] -fn ec_jwk_wrong_kty_rejected() { - let factory = OpenSslJwkVerifierFactory; - let jwk = EcJwk { - kty: "RSA".to_string(), // wrong type - crv: "P-256".to_string(), - x: b64url(&[1u8; 32]), - y: b64url(&[2u8; 32]), - kid: None, - }; - assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); -} - -#[test] -fn ec_jwk_unsupported_curve_rejected() { - let factory = OpenSslJwkVerifierFactory; - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "secp256k1".to_string(), // not supported - x: b64url(&[1u8; 32]), - y: b64url(&[2u8; 32]), - kid: None, - }; - assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); -} - -#[test] -fn ec_jwk_wrong_coordinate_length_rejected() { - let factory = OpenSslJwkVerifierFactory; - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: b64url(&[1u8; 16]), // too short for P-256 - y: b64url(&[2u8; 32]), - kid: None, - }; - assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); -} - -#[test] -fn ec_jwk_invalid_point_rejected() { - let factory = OpenSslJwkVerifierFactory; - // All-zeros is not a valid point on P-256 - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: b64url(&[0u8; 32]), - y: b64url(&[0u8; 32]), - kid: None, - }; - assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); -} - -// ==================== RSA JWK Tests ==================== - -#[test] -fn rsa_jwk_creates_verifier() { - let factory = OpenSslJwkVerifierFactory; - let (_pkey, jwk) = generate_rsa_jwk(); - - let verifier = factory.verifier_from_rsa_jwk(&jwk, -37); // PS256 - assert!( - verifier.is_ok(), - "RSA JWK should create verifier: {:?}", - verifier.err() - ); -} - -#[test] -fn rsa_jwk_wrong_kty_rejected() { - let factory = OpenSslJwkVerifierFactory; - let jwk = RsaJwk { - kty: "EC".to_string(), // wrong - n: b64url(&[1u8; 256]), - e: b64url(&[1, 0, 1]), - kid: None, - }; - assert!(factory.verifier_from_rsa_jwk(&jwk, -37).is_err()); -} - -#[test] -fn rsa_jwk_verifies_signature() { - let factory = OpenSslJwkVerifierFactory; - let (pkey, jwk) = generate_rsa_jwk(); - - let data = b"test data for RSA-PSS signature"; - let mut signer = - openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); - signer - .set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS) - .unwrap(); - signer - .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::DIGEST_LENGTH) - .unwrap(); - let sig = signer.sign_oneshot_to_vec(data).unwrap(); - - let verifier = factory.verifier_from_rsa_jwk(&jwk, -37).unwrap(); // PS256 - let result = verifier.verify(data, &sig); - assert!(result.is_ok()); - assert!(result.unwrap(), "RSA-PSS signature should verify"); -} - -// ==================== Jwk Enum Dispatch ==================== - -#[test] -fn jwk_enum_dispatches_to_ec() { - let factory = OpenSslJwkVerifierFactory; - let (_pkey, ec_jwk) = generate_p256_jwk(); - let jwk = Jwk::Ec(ec_jwk); - - let verifier = factory.verifier_from_jwk(&jwk, -7); - assert!(verifier.is_ok()); -} - -#[test] -fn jwk_enum_dispatches_to_rsa() { - let factory = OpenSslJwkVerifierFactory; - let (_pkey, rsa_jwk) = generate_rsa_jwk(); - let jwk = Jwk::Rsa(rsa_jwk); - - let verifier = factory.verifier_from_jwk(&jwk, -37); - assert!(verifier.is_ok()); -} - -// ==================== key_conversion tests ==================== - -#[test] -fn ec_point_to_spki_der_p256() { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let point_bytes = ec_key - .public_key() - .to_bytes( - &group, - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut ctx, - ) - .unwrap(); - - let spki = ec_point_to_spki_der(&point_bytes, "P-256"); - assert!(spki.is_ok()); - let spki = spki.unwrap(); - assert_eq!(spki[0], 0x30, "SPKI DER starts with SEQUENCE"); - assert!(spki.len() > 65); -} - -#[test] -fn ec_point_to_spki_der_p384() { - let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let point_bytes = ec_key - .public_key() - .to_bytes( - &group, - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut ctx, - ) - .unwrap(); - - let spki = ec_point_to_spki_der(&point_bytes, "P-384"); - assert!(spki.is_ok()); -} - -#[test] -fn ec_point_to_spki_der_invalid_prefix() { - let bad_point = vec![0x00; 65]; // missing 0x04 prefix - assert!(ec_point_to_spki_der(&bad_point, "P-256").is_err()); -} - -#[test] -fn ec_point_to_spki_der_empty() { - assert!(ec_point_to_spki_der(&[], "P-256").is_err()); -} - -#[test] -fn ec_point_to_spki_der_unsupported_curve() { - let point = vec![0x04; 65]; - assert!(ec_point_to_spki_der(&point, "secp256k1").is_err()); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for JWK → CryptoVerifier conversion via OpenSslJwkVerifierFactory. +//! +//! Covers: +//! - EC JWK (P-256, P-384) → verifier creation and signature verification +//! - RSA JWK → verifier creation +//! - Invalid JWK handling (wrong kty, bad coordinates, unsupported curves) +//! - Key conversion (ec_point_to_spki_der) +//! - Base64url decoding + +use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; +use cose_sign1_crypto_openssl::key_conversion::ec_point_to_spki_der; +use crypto_primitives::{CryptoVerifier, EcJwk, Jwk, JwkVerifierFactory, RsaJwk}; + +use base64::Engine; +use openssl::ec::{EcGroup, EcKey}; +use openssl::nid::Nid; +use openssl::pkey::PKey; + +fn b64url(data: &[u8]) -> String { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data) +} + +/// Generate a real P-256 key pair and return (private_pkey, EcJwk). +fn generate_p256_jwk() -> (PKey, EcJwk<'static>) { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key.clone()).unwrap(); + + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let mut x = openssl::bn::BigNum::new().unwrap(); + let mut y = openssl::bn::BigNum::new().unwrap(); + ec_key + .public_key() + .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx) + .unwrap(); + + let x_bytes = x.to_vec(); + let y_bytes = y.to_vec(); + // Pad to 32 bytes for P-256 + let mut x_padded = vec![0u8; 32 - x_bytes.len()]; + x_padded.extend_from_slice(&x_bytes); + let mut y_padded = vec![0u8; 32 - y_bytes.len()]; + y_padded.extend_from_slice(&y_bytes); + + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: b64url(&x_padded).into(), + y: b64url(&y_padded).into(), + kid: Some("test-p256".into()), + }; + + (pkey, jwk) +} + +/// Generate a real P-384 key pair and return (private_pkey, EcJwk). +fn generate_p384_jwk() -> (PKey, EcJwk<'static>) { + let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key.clone()).unwrap(); + + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let mut x = openssl::bn::BigNum::new().unwrap(); + let mut y = openssl::bn::BigNum::new().unwrap(); + ec_key + .public_key() + .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx) + .unwrap(); + + let x_bytes = x.to_vec(); + let y_bytes = y.to_vec(); + let mut x_padded = vec![0u8; 48 - x_bytes.len()]; + x_padded.extend_from_slice(&x_bytes); + let mut y_padded = vec![0u8; 48 - y_bytes.len()]; + y_padded.extend_from_slice(&y_bytes); + + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-384".into(), + x: b64url(&x_padded).into(), + y: b64url(&y_padded).into(), + kid: Some("test-p384".into()), + }; + + (pkey, jwk) +} + +/// Generate an RSA key pair and return (private_pkey, RsaJwk). +fn generate_rsa_jwk() -> (PKey, RsaJwk) { + let rsa = openssl::rsa::Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa.clone()).unwrap(); + + let n = rsa.n().to_vec(); + let e = rsa.e().to_vec(); + + let jwk = RsaJwk { + kty: "RSA".into(), + n: b64url(&n), + e: b64url(&e), + kid: Some("test-rsa".into()), + }; + + (pkey, jwk) +} + +// ==================== EC JWK Tests ==================== + +#[test] +fn ec_p256_jwk_creates_verifier() { + let factory = OpenSslJwkVerifierFactory; + let (_pkey, jwk) = generate_p256_jwk(); + + let verifier = factory.verifier_from_ec_jwk(&jwk, -7); // ES256 + assert!( + verifier.is_ok(), + "P-256 JWK should create verifier: {:?}", + verifier.err() + ); + assert_eq!(verifier.unwrap().algorithm(), -7); +} + +#[test] +fn ec_p384_jwk_creates_verifier() { + let factory = OpenSslJwkVerifierFactory; + let (_pkey, jwk) = generate_p384_jwk(); + + let verifier = factory.verifier_from_ec_jwk(&jwk, -35); // ES384 + assert!( + verifier.is_ok(), + "P-384 JWK should create verifier: {:?}", + verifier.err() + ); + assert_eq!(verifier.unwrap().algorithm(), -35); +} + +#[test] +fn ec_p256_jwk_verifies_signature() { + let factory = OpenSslJwkVerifierFactory; + let (pkey, jwk) = generate_p256_jwk(); + + // Sign some data with the private key + let data = b"test data for ES256 signature verification"; + let mut signer = + openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); + let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); + // Convert DER → fixed r||s format (COSE uses fixed-length) + let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 64).unwrap(); + + // Create verifier from JWK and verify + let verifier = factory.verifier_from_ec_jwk(&jwk, -7).unwrap(); + let result = verifier.verify(data, &fixed_sig); + assert!(result.is_ok()); + assert!(result.unwrap(), "Signature should verify with matching key"); +} + +#[test] +fn ec_p384_jwk_verifies_signature() { + let factory = OpenSslJwkVerifierFactory; + let (pkey, jwk) = generate_p384_jwk(); + + let data = b"test data for ES384 signature verification"; + let mut signer = + openssl::sign::Signer::new(openssl::hash::MessageDigest::sha384(), &pkey).unwrap(); + let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); + let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 96).unwrap(); + + let verifier = factory.verifier_from_ec_jwk(&jwk, -35).unwrap(); + let result = verifier.verify(data, &fixed_sig); + assert!(result.is_ok()); + assert!(result.unwrap(), "ES384 signature should verify"); +} + +#[test] +fn ec_jwk_wrong_key_rejects_signature() { + let factory = OpenSslJwkVerifierFactory; + let (pkey, _jwk1) = generate_p256_jwk(); + let (_pkey2, jwk2) = generate_p256_jwk(); // different key + + let data = b"signed with key 1"; + let mut signer = + openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); + let der_sig = signer.sign_oneshot_to_vec(data).unwrap(); + let fixed_sig = cose_sign1_crypto_openssl::ecdsa_format::der_to_fixed(&der_sig, 64).unwrap(); + + // Verify with DIFFERENT key should fail + let verifier = factory.verifier_from_ec_jwk(&jwk2, -7).unwrap(); + let result = verifier.verify(data, &fixed_sig); + assert!(result.is_ok()); + assert!(!result.unwrap(), "Wrong key should reject signature"); +} + +// ==================== EC JWK Error Cases ==================== + +#[test] +fn ec_jwk_wrong_kty_rejected() { + let factory = OpenSslJwkVerifierFactory; + let jwk = EcJwk { + kty: "RSA".into(), // wrong type + crv: "P-256".into(), + x: b64url(&[1u8; 32]).into(), + y: b64url(&[2u8; 32]).into(), + kid: None, + }; + assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); +} + +#[test] +fn ec_jwk_unsupported_curve_rejected() { + let factory = OpenSslJwkVerifierFactory; + let jwk = EcJwk { + kty: "EC".into(), + crv: "secp256k1".into(), // not supported + x: b64url(&[1u8; 32]).into(), + y: b64url(&[2u8; 32]).into(), + kid: None, + }; + assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); +} + +#[test] +fn ec_jwk_wrong_coordinate_length_rejected() { + let factory = OpenSslJwkVerifierFactory; + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: b64url(&[1u8; 16]).into(), // too short for P-256 + y: b64url(&[2u8; 32]).into(), + kid: None, + }; + assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); +} + +#[test] +fn ec_jwk_invalid_point_rejected() { + let factory = OpenSslJwkVerifierFactory; + // All-zeros is not a valid point on P-256 + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: b64url(&[0u8; 32]).into(), + y: b64url(&[0u8; 32]).into(), + kid: None, + }; + assert!(factory.verifier_from_ec_jwk(&jwk, -7).is_err()); +} + +// ==================== RSA JWK Tests ==================== + +#[test] +fn rsa_jwk_creates_verifier() { + let factory = OpenSslJwkVerifierFactory; + let (_pkey, jwk) = generate_rsa_jwk(); + + let verifier = factory.verifier_from_rsa_jwk(&jwk, -37); // PS256 + assert!( + verifier.is_ok(), + "RSA JWK should create verifier: {:?}", + verifier.err() + ); +} + +#[test] +fn rsa_jwk_wrong_kty_rejected() { + let factory = OpenSslJwkVerifierFactory; + let jwk = RsaJwk { + kty: "EC".into(), // wrong + n: b64url(&[1u8; 256]), + e: b64url(&[1, 0, 1]), + kid: None, + }; + assert!(factory.verifier_from_rsa_jwk(&jwk, -37).is_err()); +} + +#[test] +fn rsa_jwk_verifies_signature() { + let factory = OpenSslJwkVerifierFactory; + let (pkey, jwk) = generate_rsa_jwk(); + + let data = b"test data for RSA-PSS signature"; + let mut signer = + openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey).unwrap(); + signer + .set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS) + .unwrap(); + signer + .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::DIGEST_LENGTH) + .unwrap(); + let sig = signer.sign_oneshot_to_vec(data).unwrap(); + + let verifier = factory.verifier_from_rsa_jwk(&jwk, -37).unwrap(); // PS256 + let result = verifier.verify(data, &sig); + assert!(result.is_ok()); + assert!(result.unwrap(), "RSA-PSS signature should verify"); +} + +// ==================== Jwk Enum Dispatch ==================== + +#[test] +fn jwk_enum_dispatches_to_ec() { + let factory = OpenSslJwkVerifierFactory; + let (_pkey, ec_jwk) = generate_p256_jwk(); + let jwk = Jwk::Ec(ec_jwk); + + let verifier = factory.verifier_from_jwk(&jwk, -7); + assert!(verifier.is_ok()); +} + +#[test] +fn jwk_enum_dispatches_to_rsa() { + let factory = OpenSslJwkVerifierFactory; + let (_pkey, rsa_jwk) = generate_rsa_jwk(); + let jwk = Jwk::Rsa(rsa_jwk); + + let verifier = factory.verifier_from_jwk(&jwk, -37); + assert!(verifier.is_ok()); +} + +// ==================== key_conversion tests ==================== + +#[test] +fn ec_point_to_spki_der_p256() { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let point_bytes = ec_key + .public_key() + .to_bytes( + &group, + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .unwrap(); + + let spki = ec_point_to_spki_der(&point_bytes, "P-256"); + assert!(spki.is_ok()); + let spki = spki.unwrap(); + assert_eq!(spki[0], 0x30, "SPKI DER starts with SEQUENCE"); + assert!(spki.len() > 65); +} + +#[test] +fn ec_point_to_spki_der_p384() { + let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let point_bytes = ec_key + .public_key() + .to_bytes( + &group, + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .unwrap(); + + let spki = ec_point_to_spki_der(&point_bytes, "P-384"); + assert!(spki.is_ok()); +} + +#[test] +fn ec_point_to_spki_der_invalid_prefix() { + let bad_point = vec![0x00; 65]; // missing 0x04 prefix + assert!(ec_point_to_spki_der(&bad_point, "P-256").is_err()); +} + +#[test] +fn ec_point_to_spki_der_empty() { + assert!(ec_point_to_spki_der(&[], "P-256").is_err()); +} + +#[test] +fn ec_point_to_spki_der_unsupported_curve() { + let point = vec![0x04; 65]; + assert!(ec_point_to_spki_der(&point, "secp256k1").is_err()); +} diff --git a/native/rust/primitives/crypto/src/jwk.rs b/native/rust/primitives/crypto/src/jwk.rs index cb736afc..c0716c93 100644 --- a/native/rust/primitives/crypto/src/jwk.rs +++ b/native/rust/primitives/crypto/src/jwk.rs @@ -11,6 +11,7 @@ use crate::error::CryptoError; use crate::verifier::CryptoVerifier; +use std::borrow::Cow; // ============================================================================ // JWK Key Representations @@ -20,17 +21,17 @@ use crate::verifier::CryptoVerifier; /// /// Used for ECDSA verification with P-256, P-384, and P-521 curves. #[derive(Debug, Clone)] -pub struct EcJwk { +pub struct EcJwk<'a> { /// Key type — must be "EC". - pub kty: String, + pub kty: Cow<'a, str>, /// Curve name: "P-256", "P-384", or "P-521". - pub crv: String, + pub crv: Cow<'a, str>, /// Base64url-encoded x-coordinate. - pub x: String, + pub x: Cow<'a, str>, /// Base64url-encoded y-coordinate. - pub y: String, + pub y: Cow<'a, str>, /// Key ID (optional). - pub kid: Option, + pub kid: Option>, } /// RSA JWK public key (kty = "RSA"). @@ -69,9 +70,9 @@ pub struct PqcJwk { /// Use this enum when accepting keys of unknown type at runtime /// (e.g., from a JWKS document that may contain mixed key types). #[derive(Debug, Clone)] -pub enum Jwk { +pub enum Jwk<'a> { /// Elliptic Curve key (P-256, P-384, P-521). - Ec(EcJwk), + Ec(EcJwk<'a>), /// RSA key. Rsa(RsaJwk), /// Post-Quantum key (ML-DSA). Feature-gated at usage sites. @@ -103,7 +104,7 @@ pub trait JwkVerifierFactory: Send + Sync { /// Create a `CryptoVerifier` from an EC JWK and a COSE algorithm identifier. fn verifier_from_ec_jwk( &self, - jwk: &EcJwk, + jwk: &EcJwk<'_>, cose_algorithm: i64, ) -> Result, CryptoError>; @@ -140,7 +141,7 @@ pub trait JwkVerifierFactory: Send + Sync { /// Dispatches to the appropriate typed method based on `Jwk` variant. fn verifier_from_jwk( &self, - jwk: &Jwk, + jwk: &Jwk<'_>, cose_algorithm: i64, ) -> Result, CryptoError> { match jwk { diff --git a/native/rust/primitives/crypto/tests/comprehensive_trait_tests.rs b/native/rust/primitives/crypto/tests/comprehensive_trait_tests.rs index 07671a62..ce5254d1 100644 --- a/native/rust/primitives/crypto/tests/comprehensive_trait_tests.rs +++ b/native/rust/primitives/crypto/tests/comprehensive_trait_tests.rs @@ -1,424 +1,424 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive tests for crypto_primitives: JWK types, trait defaults, -//! JwkVerifierFactory dispatch, CryptoError Display/Debug. - -use crypto_primitives::{ - CryptoError, CryptoSigner, CryptoVerifier, EcJwk, Jwk, JwkVerifierFactory, PqcJwk, RsaJwk, -}; - -// ============================================================================ -// JWK type construction and accessors -// ============================================================================ - -#[test] -fn ec_jwk_creation_and_debug() { - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: "base64url_x".to_string(), - y: "base64url_y".to_string(), - kid: Some("key-1".to_string()), - }; - assert_eq!(jwk.kty, "EC"); - assert_eq!(jwk.crv, "P-256"); - assert_eq!(jwk.x, "base64url_x"); - assert_eq!(jwk.y, "base64url_y"); - assert_eq!(jwk.kid.as_deref(), Some("key-1")); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("EC")); -} - -#[test] -fn ec_jwk_without_kid() { - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-384".to_string(), - x: "x384".to_string(), - y: "y384".to_string(), - kid: None, - }; - assert!(jwk.kid.is_none()); -} - -#[test] -fn ec_jwk_clone() { - let jwk = EcJwk { - kty: "EC".to_string(), - crv: "P-521".to_string(), - x: "x521".to_string(), - y: "y521".to_string(), - kid: Some("cloned-key".to_string()), - }; - let cloned = jwk.clone(); - assert_eq!(cloned.crv, "P-521"); - assert_eq!(cloned.kid, Some("cloned-key".to_string())); -} - -#[test] -fn rsa_jwk_creation_and_debug() { - let jwk = RsaJwk { - kty: "RSA".to_string(), - n: "modulus".to_string(), - e: "AQAB".to_string(), - kid: Some("rsa-key".to_string()), - }; - assert_eq!(jwk.kty, "RSA"); - assert_eq!(jwk.n, "modulus"); - assert_eq!(jwk.e, "AQAB"); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("RSA")); -} - -#[test] -fn rsa_jwk_without_kid() { - let jwk = RsaJwk { - kty: "RSA".to_string(), - n: "n".to_string(), - e: "e".to_string(), - kid: None, - }; - assert!(jwk.kid.is_none()); -} - -#[test] -fn rsa_jwk_clone() { - let jwk = RsaJwk { - kty: "RSA".to_string(), - n: "big-modulus".to_string(), - e: "AQAB".to_string(), - kid: None, - }; - let cloned = jwk.clone(); - assert_eq!(cloned.n, "big-modulus"); -} - -#[test] -fn pqc_jwk_creation_and_debug() { - let jwk = PqcJwk { - kty: "ML-DSA".to_string(), - alg: "ML-DSA-44".to_string(), - pub_key: "base64_pub".to_string(), - kid: Some("pqc-1".to_string()), - }; - assert_eq!(jwk.kty, "ML-DSA"); - assert_eq!(jwk.alg, "ML-DSA-44"); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("ML-DSA")); -} - -#[test] -fn pqc_jwk_clone() { - let jwk = PqcJwk { - kty: "ML-DSA".to_string(), - alg: "ML-DSA-87".to_string(), - pub_key: "key".to_string(), - kid: None, - }; - let cloned = jwk.clone(); - assert_eq!(cloned.alg, "ML-DSA-87"); -} - -// ============================================================================ -// Jwk enum -// ============================================================================ - -#[test] -fn jwk_ec_variant() { - let ec = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: "x".to_string(), - y: "y".to_string(), - kid: None, - }; - let jwk = Jwk::Ec(ec); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("Ec")); -} - -#[test] -fn jwk_rsa_variant() { - let rsa = RsaJwk { - kty: "RSA".to_string(), - n: "n".to_string(), - e: "e".to_string(), - kid: None, - }; - let jwk = Jwk::Rsa(rsa); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("Rsa")); -} - -#[test] -fn jwk_pqc_variant() { - let pqc = PqcJwk { - kty: "ML-DSA".to_string(), - alg: "ML-DSA-65".to_string(), - pub_key: "key".to_string(), - kid: None, - }; - let jwk = Jwk::Pqc(pqc); - let dbg = format!("{:?}", jwk); - assert!(dbg.contains("Pqc")); -} - -#[test] -fn jwk_clone() { - let ec = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: "x".to_string(), - y: "y".to_string(), - kid: None, - }; - let jwk = Jwk::Ec(ec); - let cloned = jwk.clone(); - match cloned { - Jwk::Ec(e) => assert_eq!(e.crv, "P-256"), - _ => panic!("expected Ec variant"), - } -} - -// ============================================================================ -// JwkVerifierFactory default implementations -// ============================================================================ - -/// Minimal implementation only providing EC JWK. -struct MinimalJwkFactory; - -impl JwkVerifierFactory for MinimalJwkFactory { - fn verifier_from_ec_jwk( - &self, - _jwk: &EcJwk, - _cose_algorithm: i64, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedOperation("test: not real".into())) - } -} - -#[test] -fn jwk_factory_rsa_default_returns_unsupported() { - let factory = MinimalJwkFactory; - let rsa = RsaJwk { - kty: "RSA".to_string(), - n: "n".to_string(), - e: "e".to_string(), - kid: None, - }; - let result = factory.verifier_from_rsa_jwk(&rsa, -257); - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("RSA JWK")); - } - other => panic!("expected UnsupportedOperation, got: {:?}", other), - } -} - -#[test] -fn jwk_factory_pqc_default_returns_unsupported() { - let factory = MinimalJwkFactory; - let pqc = PqcJwk { - kty: "ML-DSA".to_string(), - alg: "ML-DSA-44".to_string(), - pub_key: "key".to_string(), - kid: None, - }; - let result = factory.verifier_from_pqc_jwk(&pqc, -48); - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("PQC JWK")); - } - other => panic!("expected UnsupportedOperation, got: {:?}", other), - } -} - -#[test] -fn jwk_factory_verifier_from_jwk_dispatches_ec() { - let factory = MinimalJwkFactory; - let ec = EcJwk { - kty: "EC".to_string(), - crv: "P-256".to_string(), - x: "x".to_string(), - y: "y".to_string(), - kid: None, - }; - let jwk = Jwk::Ec(ec); - let result = factory.verifier_from_jwk(&jwk, -7); - // Should dispatch to verifier_from_ec_jwk which returns our test error - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("test: not real")); - } - other => panic!("expected our test error, got: {:?}", other), - } -} - -#[test] -fn jwk_factory_verifier_from_jwk_dispatches_rsa() { - let factory = MinimalJwkFactory; - let rsa = RsaJwk { - kty: "RSA".to_string(), - n: "n".to_string(), - e: "e".to_string(), - kid: None, - }; - let jwk = Jwk::Rsa(rsa); - let result = factory.verifier_from_jwk(&jwk, -257); - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("RSA JWK")); - } - other => panic!("expected RSA unsupported, got: {:?}", other), - } -} - -#[test] -fn jwk_factory_verifier_from_jwk_dispatches_pqc() { - let factory = MinimalJwkFactory; - let pqc = PqcJwk { - kty: "ML-DSA".to_string(), - alg: "ML-DSA-65".to_string(), - pub_key: "key".to_string(), - kid: None, - }; - let jwk = Jwk::Pqc(pqc); - let result = factory.verifier_from_jwk(&jwk, -49); - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("PQC JWK")); - } - other => panic!("expected PQC unsupported, got: {:?}", other), - } -} - -// ============================================================================ -// CryptoError Debug -// ============================================================================ - -#[test] -fn crypto_error_debug_signing_failed() { - let err = CryptoError::SigningFailed("test".to_string()); - let dbg = format!("{:?}", err); - assert!(dbg.contains("SigningFailed")); - assert!(dbg.contains("test")); -} - -#[test] -fn crypto_error_debug_verification_failed() { - let err = CryptoError::VerificationFailed("bad".to_string()); - let dbg = format!("{:?}", err); - assert!(dbg.contains("VerificationFailed")); -} - -#[test] -fn crypto_error_debug_invalid_key() { - let err = CryptoError::InvalidKey("corrupt".to_string()); - let dbg = format!("{:?}", err); - assert!(dbg.contains("InvalidKey")); -} - -#[test] -fn crypto_error_debug_unsupported_algorithm() { - let err = CryptoError::UnsupportedAlgorithm(-999); - let dbg = format!("{:?}", err); - assert!(dbg.contains("UnsupportedAlgorithm")); - assert!(dbg.contains("-999")); -} - -#[test] -fn crypto_error_debug_unsupported_operation() { - let err = CryptoError::UnsupportedOperation("nope".to_string()); - let dbg = format!("{:?}", err); - assert!(dbg.contains("UnsupportedOperation")); -} - -#[test] -fn crypto_error_is_std_error() { - let err = CryptoError::SigningFailed("test".to_string()); - let std_err: &dyn std::error::Error = &err; - assert!(!std_err.to_string().is_empty()); -} - -// ============================================================================ -// CryptoSigner trait default: key_id() returns None -// ============================================================================ - -struct MinimalSigner; - -impl CryptoSigner for MinimalSigner { - fn sign(&self, _data: &[u8]) -> Result, CryptoError> { - Ok(vec![0]) - } - fn algorithm(&self) -> i64 { - -7 - } - fn key_type(&self) -> &str { - "Test" - } -} - -#[test] -fn signer_default_key_id_is_none() { - let signer = MinimalSigner; - assert_eq!(signer.key_id(), None); -} - -#[test] -fn signer_default_supports_streaming_is_false() { - let signer = MinimalSigner; - assert!(!signer.supports_streaming()); -} - -#[test] -fn signer_default_sign_init_returns_error() { - let signer = MinimalSigner; - let result = signer.sign_init(); - assert!(result.is_err()); -} - -// ============================================================================ -// CryptoVerifier trait defaults -// ============================================================================ - -struct MinimalVerifier; - -impl CryptoVerifier for MinimalVerifier { - fn verify(&self, _data: &[u8], _signature: &[u8]) -> Result { - Ok(true) - } - fn algorithm(&self) -> i64 { - -7 - } -} - -#[test] -fn verifier_default_supports_streaming_is_false() { - let verifier = MinimalVerifier; - assert!(!verifier.supports_streaming()); -} - -#[test] -fn verifier_default_verify_init_returns_error() { - let verifier = MinimalVerifier; - let result = verifier.verify_init(b"sig"); - assert!(result.is_err()); - let err = result.err().unwrap(); - match err { - CryptoError::UnsupportedOperation(msg) => { - assert!(msg.contains("streaming not supported")); - } - other => panic!("expected UnsupportedOperation, got: {:?}", other), - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive tests for crypto_primitives: JWK types, trait defaults, +//! JwkVerifierFactory dispatch, CryptoError Display/Debug. + +use crypto_primitives::{ + CryptoError, CryptoSigner, CryptoVerifier, EcJwk, Jwk, JwkVerifierFactory, PqcJwk, RsaJwk, +}; + +// ============================================================================ +// JWK type construction and accessors +// ============================================================================ + +#[test] +fn ec_jwk_creation_and_debug() { + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: "base64url_x".into(), + y: "base64url_y".into(), + kid: Some("key-1".into()), + }; + assert_eq!(jwk.kty, "EC"); + assert_eq!(jwk.crv, "P-256"); + assert_eq!(jwk.x, "base64url_x"); + assert_eq!(jwk.y, "base64url_y"); + assert_eq!(jwk.kid.as_deref(), Some("key-1")); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("EC")); +} + +#[test] +fn ec_jwk_without_kid() { + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-384".into(), + x: "x384".into(), + y: "y384".into(), + kid: None, + }; + assert!(jwk.kid.is_none()); +} + +#[test] +fn ec_jwk_clone() { + let jwk = EcJwk { + kty: "EC".into(), + crv: "P-521".into(), + x: "x521".into(), + y: "y521".into(), + kid: Some("cloned-key".into()), + }; + let cloned = jwk.clone(); + assert_eq!(cloned.crv, "P-521"); + assert_eq!(cloned.kid.as_deref(), Some("cloned-key")); +} + +#[test] +fn rsa_jwk_creation_and_debug() { + let jwk = RsaJwk { + kty: "RSA".into(), + n: "modulus".to_string(), + e: "AQAB".to_string(), + kid: Some("rsa-key".into()), + }; + assert_eq!(jwk.kty, "RSA"); + assert_eq!(jwk.n, "modulus"); + assert_eq!(jwk.e, "AQAB"); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("RSA")); +} + +#[test] +fn rsa_jwk_without_kid() { + let jwk = RsaJwk { + kty: "RSA".into(), + n: "n".to_string(), + e: "e".to_string(), + kid: None, + }; + assert!(jwk.kid.is_none()); +} + +#[test] +fn rsa_jwk_clone() { + let jwk = RsaJwk { + kty: "RSA".into(), + n: "big-modulus".to_string(), + e: "AQAB".to_string(), + kid: None, + }; + let cloned = jwk.clone(); + assert_eq!(cloned.n, "big-modulus"); +} + +#[test] +fn pqc_jwk_creation_and_debug() { + let jwk = PqcJwk { + kty: "ML-DSA".into(), + alg: "ML-DSA-44".to_string(), + pub_key: "base64_pub".into(), + kid: Some("pqc-1".into()), + }; + assert_eq!(jwk.kty, "ML-DSA"); + assert_eq!(jwk.alg, "ML-DSA-44"); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("ML-DSA")); +} + +#[test] +fn pqc_jwk_clone() { + let jwk = PqcJwk { + kty: "ML-DSA".into(), + alg: "ML-DSA-87".to_string(), + pub_key: "key".into(), + kid: None, + }; + let cloned = jwk.clone(); + assert_eq!(cloned.alg, "ML-DSA-87"); +} + +// ============================================================================ +// Jwk enum +// ============================================================================ + +#[test] +fn jwk_ec_variant() { + let ec = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: "x".into(), + y: "y".into(), + kid: None, + }; + let jwk = Jwk::Ec(ec); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("Ec")); +} + +#[test] +fn jwk_rsa_variant() { + let rsa = RsaJwk { + kty: "RSA".into(), + n: "n".to_string(), + e: "e".to_string(), + kid: None, + }; + let jwk = Jwk::Rsa(rsa); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("Rsa")); +} + +#[test] +fn jwk_pqc_variant() { + let pqc = PqcJwk { + kty: "ML-DSA".into(), + alg: "ML-DSA-65".to_string(), + pub_key: "key".into(), + kid: None, + }; + let jwk = Jwk::Pqc(pqc); + let dbg = format!("{:?}", jwk); + assert!(dbg.contains("Pqc")); +} + +#[test] +fn jwk_clone() { + let ec = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: "x".into(), + y: "y".into(), + kid: None, + }; + let jwk = Jwk::Ec(ec); + let cloned = jwk.clone(); + match cloned { + Jwk::Ec(e) => assert_eq!(e.crv, "P-256"), + _ => panic!("expected Ec variant"), + } +} + +// ============================================================================ +// JwkVerifierFactory default implementations +// ============================================================================ + +/// Minimal implementation only providing EC JWK. +struct MinimalJwkFactory; + +impl JwkVerifierFactory for MinimalJwkFactory { + fn verifier_from_ec_jwk( + &self, + _jwk: &EcJwk<'_>, + _cose_algorithm: i64, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedOperation("test: not real".into())) + } +} + +#[test] +fn jwk_factory_rsa_default_returns_unsupported() { + let factory = MinimalJwkFactory; + let rsa = RsaJwk { + kty: "RSA".into(), + n: "n".to_string(), + e: "e".to_string(), + kid: None, + }; + let result = factory.verifier_from_rsa_jwk(&rsa, -257); + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("RSA JWK")); + } + other => panic!("expected UnsupportedOperation, got: {:?}", other), + } +} + +#[test] +fn jwk_factory_pqc_default_returns_unsupported() { + let factory = MinimalJwkFactory; + let pqc = PqcJwk { + kty: "ML-DSA".into(), + alg: "ML-DSA-44".to_string(), + pub_key: "key".into(), + kid: None, + }; + let result = factory.verifier_from_pqc_jwk(&pqc, -48); + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("PQC JWK")); + } + other => panic!("expected UnsupportedOperation, got: {:?}", other), + } +} + +#[test] +fn jwk_factory_verifier_from_jwk_dispatches_ec() { + let factory = MinimalJwkFactory; + let ec = EcJwk { + kty: "EC".into(), + crv: "P-256".into(), + x: "x".into(), + y: "y".into(), + kid: None, + }; + let jwk = Jwk::Ec(ec); + let result = factory.verifier_from_jwk(&jwk, -7); + // Should dispatch to verifier_from_ec_jwk which returns our test error + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("test: not real")); + } + other => panic!("expected our test error, got: {:?}", other), + } +} + +#[test] +fn jwk_factory_verifier_from_jwk_dispatches_rsa() { + let factory = MinimalJwkFactory; + let rsa = RsaJwk { + kty: "RSA".into(), + n: "n".to_string(), + e: "e".to_string(), + kid: None, + }; + let jwk = Jwk::Rsa(rsa); + let result = factory.verifier_from_jwk(&jwk, -257); + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("RSA JWK")); + } + other => panic!("expected RSA unsupported, got: {:?}", other), + } +} + +#[test] +fn jwk_factory_verifier_from_jwk_dispatches_pqc() { + let factory = MinimalJwkFactory; + let pqc = PqcJwk { + kty: "ML-DSA".into(), + alg: "ML-DSA-65".to_string(), + pub_key: "key".into(), + kid: None, + }; + let jwk = Jwk::Pqc(pqc); + let result = factory.verifier_from_jwk(&jwk, -49); + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("PQC JWK")); + } + other => panic!("expected PQC unsupported, got: {:?}", other), + } +} + +// ============================================================================ +// CryptoError Debug +// ============================================================================ + +#[test] +fn crypto_error_debug_signing_failed() { + let err = CryptoError::SigningFailed("test".to_string()); + let dbg = format!("{:?}", err); + assert!(dbg.contains("SigningFailed")); + assert!(dbg.contains("test")); +} + +#[test] +fn crypto_error_debug_verification_failed() { + let err = CryptoError::VerificationFailed("bad".to_string()); + let dbg = format!("{:?}", err); + assert!(dbg.contains("VerificationFailed")); +} + +#[test] +fn crypto_error_debug_invalid_key() { + let err = CryptoError::InvalidKey("corrupt".to_string()); + let dbg = format!("{:?}", err); + assert!(dbg.contains("InvalidKey")); +} + +#[test] +fn crypto_error_debug_unsupported_algorithm() { + let err = CryptoError::UnsupportedAlgorithm(-999); + let dbg = format!("{:?}", err); + assert!(dbg.contains("UnsupportedAlgorithm")); + assert!(dbg.contains("-999")); +} + +#[test] +fn crypto_error_debug_unsupported_operation() { + let err = CryptoError::UnsupportedOperation("nope".to_string()); + let dbg = format!("{:?}", err); + assert!(dbg.contains("UnsupportedOperation")); +} + +#[test] +fn crypto_error_is_std_error() { + let err = CryptoError::SigningFailed("test".to_string()); + let std_err: &dyn std::error::Error = &err; + assert!(!std_err.to_string().is_empty()); +} + +// ============================================================================ +// CryptoSigner trait default: key_id() returns None +// ============================================================================ + +struct MinimalSigner; + +impl CryptoSigner for MinimalSigner { + fn sign(&self, _data: &[u8]) -> Result, CryptoError> { + Ok(vec![0]) + } + fn algorithm(&self) -> i64 { + -7 + } + fn key_type(&self) -> &str { + "Test" + } +} + +#[test] +fn signer_default_key_id_is_none() { + let signer = MinimalSigner; + assert_eq!(signer.key_id(), None); +} + +#[test] +fn signer_default_supports_streaming_is_false() { + let signer = MinimalSigner; + assert!(!signer.supports_streaming()); +} + +#[test] +fn signer_default_sign_init_returns_error() { + let signer = MinimalSigner; + let result = signer.sign_init(); + assert!(result.is_err()); +} + +// ============================================================================ +// CryptoVerifier trait defaults +// ============================================================================ + +struct MinimalVerifier; + +impl CryptoVerifier for MinimalVerifier { + fn verify(&self, _data: &[u8], _signature: &[u8]) -> Result { + Ok(true) + } + fn algorithm(&self) -> i64 { + -7 + } +} + +#[test] +fn verifier_default_supports_streaming_is_false() { + let verifier = MinimalVerifier; + assert!(!verifier.supports_streaming()); +} + +#[test] +fn verifier_default_verify_init_returns_error() { + let verifier = MinimalVerifier; + let result = verifier.verify_init(b"sig"); + assert!(result.is_err()); + let err = result.err().unwrap(); + match err { + CryptoError::UnsupportedOperation(msg) => { + assert!(msg.contains("streaming not supported")); + } + other => panic!("expected UnsupportedOperation, got: {:?}", other), + } +} diff --git a/native/rust/validation/core/src/message_fact_producer.rs b/native/rust/validation/core/src/message_fact_producer.rs index 9213473d..1c6954be 100644 --- a/native/rust/validation/core/src/message_fact_producer.rs +++ b/native/rust/validation/core/src/message_fact_producer.rs @@ -91,10 +91,8 @@ impl TrustFactProducer for CoseSign1MessageFactProducer { })?; // Parse or use already-parsed message - let msg: Arc = if let Some(m) = ctx.cose_sign1_message() { - // Clone the Arc from the context - // We trust the engine to have stored it as Arc - Arc::new(m.clone()) + let msg: Arc = if let Some(m) = ctx.cose_sign1_message_arc() { + m } else { // Message should always be available from the validator ctx.mark_error::("no parsed message in context".to_string()); @@ -203,11 +201,11 @@ fn produce_cwt_claims_from_map( ) -> Result<(), TrustError> { let mut scalar_claims: BTreeMap = BTreeMap::new(); let mut raw_claims: BTreeMap> = BTreeMap::new(); - let mut raw_claims_text: BTreeMap> = BTreeMap::new(); + let mut raw_claims_text: BTreeMap, Arc<[u8]>> = BTreeMap::new(); - let mut iss: Option = None; - let mut sub: Option = None; - let mut aud: Option = None; + let mut iss: Option> = None; + let mut sub: Option> = None; + let mut aud: Option> = None; let mut exp: Option = None; let mut nbf: Option = None; let mut iat: Option = None; @@ -247,7 +245,7 @@ fn produce_cwt_claims_from_map( } CoseHeaderLabel::Text(k) => { if let Some(bytes) = value_bytes { - raw_claims_text.insert(k.clone(), Arc::from(bytes.into_boxed_slice())); + raw_claims_text.insert(Arc::from(k.as_str()), Arc::from(bytes.into_boxed_slice())); } match (k.as_str(), &value_str, value_i64) { @@ -279,10 +277,10 @@ fn produce_cwt_claims_from_map( } /// Extract a string from a CoseHeaderValue. -fn extract_string(value: &CoseHeaderValue) -> Option { +fn extract_string(value: &CoseHeaderValue) -> Option> { match value { - CoseHeaderValue::Text(s) => Some(s.to_string()), - CoseHeaderValue::Bytes(b) => std::str::from_utf8(b.as_ref()).ok().map(String::from), + CoseHeaderValue::Text(s) => Some(Arc::from(&**s)), + CoseHeaderValue::Bytes(b) => std::str::from_utf8(b.as_ref()).ok().map(Arc::from), _ => None, } } @@ -371,11 +369,11 @@ fn produce_cwt_claims_from_bytes( let mut scalar_claims: BTreeMap = BTreeMap::new(); let mut raw_claims: BTreeMap> = BTreeMap::new(); - let mut raw_claims_text: BTreeMap> = BTreeMap::new(); + let mut raw_claims_text: BTreeMap, Arc<[u8]>> = BTreeMap::new(); - let mut iss: Option = None; - let mut sub: Option = None; - let mut aud: Option = None; + let mut iss: Option> = None; + let mut sub: Option> = None; + let mut aud: Option> = None; let mut exp: Option = None; let mut nbf: Option = None; let mut iat: Option = None; @@ -399,18 +397,16 @@ fn produce_cwt_claims_from_bytes( let key_i64 = cbor_primitives::RawCbor::new(&key_bytes).try_as_i64(); let key_text = cbor_primitives::RawCbor::new(&key_bytes) .try_as_str() - .map(String::from); + .map(Arc::::from); let value_raw = cbor_primitives::RawCbor::new(&value_bytes); - let value_str = value_raw.try_as_str().map(String::from); + let value_str = value_raw.try_as_str().map(Arc::::from); let value_i64 = value_raw.try_as_i64(); let value_bool = value_raw.try_as_bool(); if let Some(k) = key_i64 { - raw_claims.insert(k, Arc::from(value_bytes.clone().into_boxed_slice())); - if let Some(s) = &value_str { - scalar_claims.insert(k, CwtClaimScalar::Str(s.clone())); + scalar_claims.insert(k, CwtClaimScalar::Str(Arc::clone(s))); } else if let Some(n) = value_i64 { scalar_claims.insert(k, CwtClaimScalar::I64(n)); } else if let Some(b) = value_bool { @@ -418,33 +414,49 @@ fn produce_cwt_claims_from_bytes( } match (k, &value_str, value_i64) { - (1, Some(s), _) => iss = Some(s.clone()), - (2, Some(s), _) => sub = Some(s.clone()), - (3, Some(s), _) => aud = Some(s.clone()), + (1, Some(s), _) => iss = Some(Arc::clone(s)), + (2, Some(s), _) => sub = Some(Arc::clone(s)), + (3, Some(s), _) => aud = Some(Arc::clone(s)), (4, _, Some(n)) => exp = Some(n), (5, _, Some(n)) => nbf = Some(n), (6, _, Some(n)) => iat = Some(n), _ => {} } + raw_claims.insert(k, Arc::from(value_bytes.into_boxed_slice())); continue; } - if let Some(k) = key_text.as_deref() { - raw_claims_text.insert( - k.to_string(), - Arc::from(value_bytes.to_vec().into_boxed_slice()), - ); - - match (k, &value_str, value_i64) { - ("iss", Some(s), _) => iss = Some(s.clone()), - ("sub", Some(s), _) => sub = Some(s.clone()), - ("aud", Some(s), _) => aud = Some(s.clone()), - ("exp", _, Some(n)) => exp = Some(n), - ("nbf", _, Some(n)) => nbf = Some(n), - ("iat", _, Some(n)) => iat = Some(n), - _ => {} + if let Some(k) = key_text { + if let Some(s) = &value_str { + match &*k { + "iss" => iss = Some(Arc::clone(s)), + "sub" => sub = Some(Arc::clone(s)), + "aud" => aud = Some(Arc::clone(s)), + _ => {} + } + } else { + match &*k { + "exp" => { + if let Some(n) = value_i64 { + exp = Some(n); + } + } + "nbf" => { + if let Some(n) = value_i64 { + nbf = Some(n); + } + } + "iat" => { + if let Some(n) = value_i64 { + iat = Some(n); + } + } + _ => {} + } } + + raw_claims_text.insert(k, Arc::from(value_bytes.into_boxed_slice())); } } @@ -544,7 +556,7 @@ impl CoseSign1MessageFactProducer { } /// Resolve content type from COSE headers. -fn resolve_content_type(msg: &CoseSign1Message) -> Option { +fn resolve_content_type(msg: &CoseSign1Message) -> Option> { const CONTENT_TYPE: i64 = 3; const PAYLOAD_HASH_ALG: i64 = 258; const PREIMAGE_CONTENT_TYPE: i64 = 259; @@ -571,7 +583,7 @@ fn resolve_content_type(msg: &CoseSign1Message) -> Option { if let Some(i) = get_header_int(protected, &preimage_ct_label) .or_else(|| get_header_int(unprotected, &preimage_ct_label)) { - return Some(format!("coap/{i}")); + return Some(Arc::from(format!("coap/{i}").as_str())); } return None; @@ -584,25 +596,25 @@ fn resolve_content_type(msg: &CoseSign1Message) -> Option { if lower.contains("+cose-hash-v") { let pos = lower.find("+cose-hash-v").unwrap(); let stripped = ct[..pos].trim(); - return (!stripped.is_empty()).then(|| stripped.to_string()); + return (!stripped.is_empty()).then(|| Arc::from(stripped)); } // Check for +hash- suffix (case-insensitive) and strip it if let Some(pos) = lower.find("+hash-") { let stripped = ct[..pos].trim(); - return (!stripped.is_empty()).then(|| stripped.to_string()); + return (!stripped.is_empty()).then(|| Arc::from(stripped)); } Some(ct) } /// Get a text value from headers. -fn get_header_text(map: &CoseHeaderMap, label: &CoseHeaderLabel) -> Option { +fn get_header_text(map: &CoseHeaderMap, label: &CoseHeaderLabel) -> Option> { match map.get(label)? { - CoseHeaderValue::Text(s) if !s.trim().is_empty() => Some(s.to_string()), + CoseHeaderValue::Text(s) if !s.trim().is_empty() => Some(Arc::from(&**s)), CoseHeaderValue::Bytes(b) => { let s = std::str::from_utf8(b.as_ref()).ok()?; - (!s.trim().is_empty()).then(|| s.to_string()) + (!s.trim().is_empty()).then(|| Arc::from(s)) } _ => None, } diff --git a/native/rust/validation/core/src/message_facts.rs b/native/rust/validation/core/src/message_facts.rs index 9710ee2b..62419a7b 100644 --- a/native/rust/validation/core/src/message_facts.rs +++ b/native/rust/validation/core/src/message_facts.rs @@ -71,7 +71,7 @@ pub struct DetachedPayloadPresentFact { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContentTypeFact { - pub content_type: String, + pub content_type: Arc, } /// Indicates whether the COSE header parameter for CWT Claims (label 15) is present. @@ -92,11 +92,11 @@ pub struct CwtClaimsFact { pub raw_claims: BTreeMap>, /// Raw CBOR bytes for each text claim key. - pub raw_claims_text: BTreeMap>, + pub raw_claims_text: BTreeMap, Arc<[u8]>>, - pub iss: Option, - pub sub: Option, - pub aud: Option, + pub iss: Option>, + pub sub: Option>, + pub aud: Option>, pub exp: Option, pub nbf: Option, pub iat: Option, @@ -104,7 +104,7 @@ pub struct CwtClaimsFact { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CwtClaimScalar { - Str(String), + Str(Arc), I64(i64), Bool(bool), } @@ -458,7 +458,7 @@ pub struct UnknownCounterSignatureBytesFact { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CounterSignatureEnvelopeIntegrityFact { pub sig_structure_intact: bool, - pub details: Option, + pub details: Option>, } impl FactProperties for DetachedPayloadPresentFact { @@ -475,7 +475,7 @@ impl FactProperties for ContentTypeFact { /// Return the property value for declarative trust policies. fn get_property<'a>(&'a self, name: &str) -> Option> { match name { - "content_type" => Some(FactValue::Str(Cow::Borrowed(self.content_type.as_str()))), + "content_type" => Some(FactValue::Str(Cow::Borrowed(&self.content_type))), _ => None, } } @@ -517,7 +517,7 @@ impl FactProperties for CwtClaimsFact { if let Some(rest) = name.strip_prefix(fields::cwt_claims::CLAIM_PREFIX) { if let Ok(label) = rest.parse::() { return self.scalar_claims.get(&label).map(|v| match v { - CwtClaimScalar::Str(s) => FactValue::Str(Cow::Borrowed(s.as_str())), + CwtClaimScalar::Str(s) => FactValue::Str(Cow::Borrowed(s)), CwtClaimScalar::I64(n) => FactValue::I64(*n), CwtClaimScalar::Bool(b) => FactValue::Bool(*b), }); diff --git a/native/rust/validation/core/tests/final_targeted_coverage.rs b/native/rust/validation/core/tests/final_targeted_coverage.rs index b3ca2fdf..3070352b 100644 --- a/native/rust/validation/core/tests/final_targeted_coverage.rs +++ b/native/rust/validation/core/tests/final_targeted_coverage.rs @@ -842,7 +842,7 @@ fn message_fact_producer_extracts_content_type() { let ct = engine.get_fact_set::(&subject).unwrap(); match ct { TrustFactSet::Available(v) => { - assert_eq!(v[0].content_type, "application/json"); + assert_eq!(&*v[0].content_type, "application/json"); } _ => panic!("Expected ContentTypeFact Available"), } @@ -866,7 +866,7 @@ fn message_fact_producer_strips_cose_hash_v_suffix() { let ct = engine.get_fact_set::(&subject).unwrap(); match ct { TrustFactSet::Available(v) => { - assert_eq!(v[0].content_type, "application/json"); + assert_eq!(&*v[0].content_type, "application/json"); } _ => panic!("Expected ContentTypeFact Available"), } @@ -890,7 +890,7 @@ fn message_fact_producer_strips_hash_alg_suffix() { let ct = engine.get_fact_set::(&subject).unwrap(); match ct { TrustFactSet::Available(v) => { - assert_eq!(v[0].content_type, "text/plain"); + assert_eq!(&*v[0].content_type, "text/plain"); } _ => panic!("Expected ContentTypeFact Available"), } diff --git a/native/rust/validation/core/tests/message_fact_coverage.rs b/native/rust/validation/core/tests/message_fact_coverage.rs index e56ff261..a00abf84 100644 --- a/native/rust/validation/core/tests/message_fact_coverage.rs +++ b/native/rust/validation/core/tests/message_fact_coverage.rs @@ -152,7 +152,7 @@ fn cwt_claims_map_extracts_wellknown_int_keyed_claims() { // Scalar claims should contain the same values. assert!(matches!( fact.scalar_claims.get(&1), - Some(CwtClaimScalar::Str(s)) if s == "issuer" + Some(CwtClaimScalar::Str(s)) if &**s == "issuer" )); assert!(matches!( fact.scalar_claims.get(&4), @@ -360,7 +360,7 @@ fn cwt_claims_map_stores_unknown_int_and_text_keys() { // Int-keyed unknown claim. assert!(matches!( fact.scalar_claims.get(&999), - Some(CwtClaimScalar::Str(s)) if s == "val999" + Some(CwtClaimScalar::Str(s)) if &**s == "val999" )); assert!(fact.raw_claims.contains_key(&999)); @@ -404,7 +404,7 @@ fn content_type_returns_plain_value_without_hash_suffix() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("application/octet-stream", ct[0].content_type); + assert_eq!("application/octet-stream", &*ct[0].content_type); } // --------------------------------------------------------------------------- @@ -423,7 +423,7 @@ fn content_type_falls_back_to_unprotected_header() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("text/xml", ct[0].content_type); + assert_eq!("text/xml", &*ct[0].content_type); } // --------------------------------------------------------------------------- @@ -443,7 +443,7 @@ fn content_type_reads_preimage_from_unprotected_when_envelope_marker_in_protecte let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("image/png", ct[0].content_type); + assert_eq!("image/png", &*ct[0].content_type); } // --------------------------------------------------------------------------- @@ -463,7 +463,7 @@ fn content_type_reads_integer_preimage_from_unprotected() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("coap/50", ct[0].content_type); + assert_eq!("coap/50", &*ct[0].content_type); } // --------------------------------------------------------------------------- @@ -563,7 +563,7 @@ fn cwt_claims_map_extracts_string_from_utf8_bytes_value() { assert_eq!(fact.iss.as_deref(), Some("issuer_b")); assert!(matches!( fact.scalar_claims.get(&1), - Some(CwtClaimScalar::Str(s)) if s == "issuer_b" + Some(CwtClaimScalar::Str(s)) if &**s == "issuer_b" )); } diff --git a/native/rust/validation/core/tests/message_fact_producer_counter_sig.rs b/native/rust/validation/core/tests/message_fact_producer_counter_sig.rs index 165988db..50efe36a 100644 --- a/native/rust/validation/core/tests/message_fact_producer_counter_sig.rs +++ b/native/rust/validation/core/tests/message_fact_producer_counter_sig.rs @@ -400,7 +400,7 @@ fn content_type_bytes_header_valid_utf8_is_used() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("application/cbor", ct[0].content_type); + assert_eq!("application/cbor", &*ct[0].content_type); } #[test] @@ -480,7 +480,7 @@ fn content_type_preimage_from_unprotected_when_envelope_marker_in_protected() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("image/png", ct[0].content_type); + assert_eq!("image/png", &*ct[0].content_type); } #[test] @@ -495,7 +495,7 @@ fn content_type_integer_preimage_from_unprotected() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("coap/99", ct[0].content_type); + assert_eq!("coap/99", &*ct[0].content_type); } #[test] @@ -513,7 +513,7 @@ fn content_type_cose_hash_v_case_insensitive_strip() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("application/xml", ct[0].content_type); + assert_eq!("application/xml", &*ct[0].content_type); } #[test] @@ -531,7 +531,7 @@ fn content_type_hash_legacy_case_insensitive_strip() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("application/xml", ct[0].content_type); + assert_eq!("application/xml", &*ct[0].content_type); } #[test] @@ -825,7 +825,7 @@ fn get_header_int_returns_integer_preimage_content_type() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("coap/0", ct[0].content_type); + assert_eq!("coap/0", &*ct[0].content_type); } #[test] @@ -841,5 +841,5 @@ fn content_type_from_unprotected_bytes_utf8() { let ct = engine.get_facts::(&subject).unwrap(); assert_eq!(1, ct.len()); - assert_eq!("text/html", ct[0].content_type); + assert_eq!("text/html", &*ct[0].content_type); } diff --git a/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs b/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs index 6b548118..a0c8d6e1 100644 --- a/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs +++ b/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs @@ -110,7 +110,7 @@ fn raw_cwt_all_wellknown_int_claims() { assert_eq!(fact.nbf, Some(1_600_000_000)); assert_eq!(fact.iat, Some(1_650_000_000)); - assert!(matches!(fact.scalar_claims.get(&1), Some(CwtClaimScalar::Str(s)) if s == "issuer")); + assert!(matches!(fact.scalar_claims.get(&1), Some(CwtClaimScalar::Str(s)) if &**s == "issuer")); assert!(matches!( fact.scalar_claims.get(&4), Some(CwtClaimScalar::I64(1_700_000_000)) @@ -208,7 +208,7 @@ fn raw_cwt_nonstandard_int_keys() { assert!(fact.nbf.is_none()); assert!(fact.iat.is_none()); - assert!(matches!(fact.scalar_claims.get(&999), Some(CwtClaimScalar::Str(s)) if s == "val999")); + assert!(matches!(fact.scalar_claims.get(&999), Some(CwtClaimScalar::Str(s)) if &**s == "val999")); assert!(matches!( fact.scalar_claims.get(&1000), Some(CwtClaimScalar::I64(42)) diff --git a/native/rust/validation/core/tests/message_facts_claim_properties.rs b/native/rust/validation/core/tests/message_facts_claim_properties.rs index 35ceb902..27ff08ff 100644 --- a/native/rust/validation/core/tests/message_facts_claim_properties.rs +++ b/native/rust/validation/core/tests/message_facts_claim_properties.rs @@ -32,9 +32,9 @@ fn cwt_claims_get_property_all_some() { scalar_claims: BTreeMap::new(), raw_claims: BTreeMap::new(), raw_claims_text: BTreeMap::new(), - iss: Some("my-issuer".to_string()), - sub: Some("my-subject".to_string()), - aud: Some("my-audience".to_string()), + iss: Some("my-issuer".into()), + sub: Some("my-subject".into()), + aud: Some("my-audience".into()), exp: Some(1_700_000_000), nbf: Some(1_600_000_000), iat: Some(1_650_000_000), @@ -93,7 +93,7 @@ fn cwt_claims_get_property_all_none() { #[test] fn cwt_claims_get_property_claim_prefix_all_variants() { let mut scalar_claims = BTreeMap::new(); - scalar_claims.insert(10, CwtClaimScalar::Str("text-value".to_string())); + scalar_claims.insert(10, CwtClaimScalar::Str("text-value".into())); scalar_claims.insert(20, CwtClaimScalar::I64(42)); scalar_claims.insert(30, CwtClaimScalar::Bool(false)); @@ -198,7 +198,7 @@ fn cwt_claims_claim_value_text_missing_key() { #[test] fn cwt_claims_claim_value_text_present_key() { let mut raw_claims_text = BTreeMap::new(); - raw_claims_text.insert("mykey".to_string(), encode_cbor_text("myval")); + raw_claims_text.insert("mykey".into(), encode_cbor_text("myval")); let fact = CwtClaimsFact { scalar_claims: BTreeMap::new(), @@ -228,7 +228,7 @@ fn cwt_claims_claim_value_text_present_key() { #[test] fn content_type_fact_get_property() { let fact = ContentTypeFact { - content_type: "application/json".to_string(), + content_type: "application/json".into(), }; assert!(matches!( diff --git a/native/rust/validation/core/tests/message_facts_more_coverage.rs b/native/rust/validation/core/tests/message_facts_more_coverage.rs index 89ad840a..3e1571e6 100644 --- a/native/rust/validation/core/tests/message_facts_more_coverage.rs +++ b/native/rust/validation/core/tests/message_facts_more_coverage.rs @@ -9,6 +9,7 @@ use cose_sign1_validation::fluent::{ TrustPlanBuilder, }; use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; +use std::borrow::Cow; use std::collections::BTreeMap; use std::sync::Arc; @@ -66,7 +67,7 @@ fn message_fact_properties_cover_expected_branches() { assert_eq!(detached.get_property("nope"), None); let ct = ContentTypeFact { - content_type: "text/plain".to_string(), + content_type: "text/plain".into(), }; assert!(matches!( ct.get_property("content_type"), @@ -88,7 +89,7 @@ fn message_fact_properties_cover_expected_branches() { scalar_claims, raw_claims: BTreeMap::new(), raw_claims_text: BTreeMap::new(), - iss: Some("issuer".to_string()), + iss: Some("issuer".into()), sub: None, aud: None, exp: None, @@ -106,7 +107,7 @@ fn message_fact_properties_cover_expected_branches() { let integrity = CounterSignatureEnvelopeIntegrityFact { sig_structure_intact: true, - details: Some("x".to_string()), + details: Some(Cow::Borrowed("x")), }; assert_eq!( integrity.get_property("sig_structure_intact"), diff --git a/native/rust/validation/core/tests/message_facts_properties.rs b/native/rust/validation/core/tests/message_facts_properties.rs index 1167949e..263b2699 100644 --- a/native/rust/validation/core/tests/message_facts_properties.rs +++ b/native/rust/validation/core/tests/message_facts_properties.rs @@ -38,21 +38,21 @@ fn cwt_claims_fact_property_accessors_cover_standard_and_scalar_claims() { let mut scalar_claims = BTreeMap::new(); scalar_claims.insert(42, CwtClaimScalar::I64(7)); scalar_claims.insert(99, CwtClaimScalar::Bool(true)); - scalar_claims.insert(100, CwtClaimScalar::Str("hello".to_string())); + scalar_claims.insert(100, CwtClaimScalar::Str("hello".into())); let mut raw_claims = BTreeMap::new(); raw_claims.insert(6, encode_cbor_i64(555)); let mut raw_claims_text = BTreeMap::new(); - raw_claims_text.insert("custom".to_string(), encode_cbor_text("v")); + raw_claims_text.insert("custom".into(), encode_cbor_text("v")); let fact = CwtClaimsFact { scalar_claims, raw_claims, raw_claims_text, - iss: Some("issuer".to_string()), + iss: Some("issuer".into()), sub: None, - aud: Some("aud".to_string()), + aud: Some("aud".into()), exp: Some(1), nbf: None, iat: Some(2), diff --git a/native/rust/validation/core/tests/message_fluent_ext_more.rs b/native/rust/validation/core/tests/message_fluent_ext_more.rs index b9c32bba..0a6d4eb3 100644 --- a/native/rust/validation/core/tests/message_fluent_ext_more.rs +++ b/native/rust/validation/core/tests/message_fluent_ext_more.rs @@ -47,7 +47,7 @@ impl TrustFactProducer for MessageFactsProducer { } ctx.observe(ContentTypeFact { - content_type: "application/json".to_string(), + content_type: "application/json".into(), })?; ctx.observe(DetachedPayloadPresentFact { present: false })?; ctx.observe(CwtClaimsPresentFact { present: true })?; @@ -57,13 +57,13 @@ impl TrustFactProducer for MessageFactsProducer { raw_claims.insert(6, encode_cbor_i64(123)); // iat (label 6) let mut raw_claims_text = BTreeMap::new(); - raw_claims_text.insert("custom".to_string(), encode_cbor_text("ok")); + raw_claims_text.insert("custom".into(), encode_cbor_text("ok")); ctx.observe(CwtClaimsFact { scalar_claims: BTreeMap::new(), raw_claims, raw_claims_text, - iss: Some("issuer.example".to_string()), + iss: Some("issuer.example".into()), sub: None, aud: None, exp: None, diff --git a/native/rust/validation/core/tests/message_parts_accessors.rs b/native/rust/validation/core/tests/message_parts_accessors.rs index dd14cb48..b056d9e3 100644 --- a/native/rust/validation/core/tests/message_parts_accessors.rs +++ b/native/rust/validation/core/tests/message_parts_accessors.rs @@ -126,7 +126,7 @@ fn claim_value_text_returns_some_for_existing_key() { let raw_bytes: Arc<[u8]> = Arc::from(enc.into_bytes().into_boxed_slice()); let mut raw_claims_text = BTreeMap::new(); - raw_claims_text.insert("my_claim".to_string(), raw_bytes); + raw_claims_text.insert("my_claim".into(), raw_bytes); let fact = CwtClaimsFact { scalar_claims: BTreeMap::new(), diff --git a/native/rust/validation/core/tests/targeted_coverage_gaps.rs b/native/rust/validation/core/tests/targeted_coverage_gaps.rs index f71df913..e23b3ab8 100644 --- a/native/rust/validation/core/tests/targeted_coverage_gaps.rs +++ b/native/rust/validation/core/tests/targeted_coverage_gaps.rs @@ -13,6 +13,7 @@ use cbor_primitives_everparse::EverParseCborProvider; use cose_sign1_primitives::payload::Payload; use cose_sign1_primitives::CoseSign1Message; use cose_sign1_validation::fluent::*; +use std::borrow::Cow; use cose_sign1_validation_primitives::facts::{TrustFactEngine, TrustFactSet}; use cose_sign1_validation_primitives::subject::TrustSubject; use cose_sign1_validation_test_utils::SimpleTrustPack; @@ -699,16 +700,16 @@ fn cwt_claim_scalar_via_get_property() { let fact = CwtClaimsFact { scalar_claims: { let mut m = BTreeMap::new(); - m.insert(42, CwtClaimScalar::Str("hello".to_string())); + m.insert(42, CwtClaimScalar::Str("hello".into())); m.insert(43, CwtClaimScalar::I64(999)); m.insert(44, CwtClaimScalar::Bool(true)); m }, raw_claims: BTreeMap::new(), raw_claims_text: BTreeMap::new(), - iss: Some("issuer".to_string()), + iss: Some("issuer".into()), sub: None, - aud: Some("audience".to_string()), + aud: Some("audience".into()), exp: Some(100), nbf: Some(50), iat: Some(75), @@ -774,7 +775,7 @@ fn cwt_claims_fact_claim_value_accessors() { let mut bool_enc = p.encoder(); bool_enc.encode_bool(true).unwrap(); m.insert( - "flag".to_string(), + "flag".into(), Arc::from(bool_enc.into_bytes().into_boxed_slice()), ); m @@ -808,7 +809,7 @@ fn content_type_fact_get_property() { use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; let fact = ContentTypeFact { - content_type: "application/json".to_string(), + content_type: "application/json".into(), }; assert!(matches!( fact.get_property("content_type"), @@ -853,7 +854,7 @@ fn counter_signature_envelope_integrity_get_property() { let fact = CounterSignatureEnvelopeIntegrityFact { sig_structure_intact: true, - details: Some("verified".to_string()), + details: Some(Cow::Borrowed("verified")), }; assert!(matches!( fact.get_property("sig_structure_intact"), @@ -1055,7 +1056,7 @@ fn indirect_signature_content_type_stripping_cose_hash_v() { }; assert!(!ct_facts.is_empty()); // The +cose-hash-v suffix should be stripped - assert_eq!(ct_facts[0].content_type, "application/test"); + assert_eq!(&*ct_facts[0].content_type, "application/test"); } #[test] @@ -1079,7 +1080,7 @@ fn indirect_signature_content_type_stripping_legacy_hash() { other => panic!("expected Available, got {other:?}"), }; assert!(!ct_facts.is_empty()); - assert_eq!(ct_facts[0].content_type, "application/vnd.example"); + assert_eq!(&*ct_facts[0].content_type, "application/vnd.example"); } #[test] @@ -1310,7 +1311,7 @@ fn content_type_from_preimage_content_type_header() { other => panic!("expected Available, got {other:?}"), }; assert!(!ct_facts.is_empty()); - assert_eq!(ct_facts[0].content_type, "application/original"); + assert_eq!(&*ct_facts[0].content_type, "application/original"); } #[test] @@ -1344,7 +1345,7 @@ fn content_type_from_preimage_int_content_type() { other => panic!("expected Available, got {other:?}"), }; assert!(!ct_facts.is_empty()); - assert_eq!(ct_facts[0].content_type, "coap/50"); + assert_eq!(&*ct_facts[0].content_type, "coap/50"); } #[test] diff --git a/native/rust/validation/core/tests/validator_deep_coverage.rs b/native/rust/validation/core/tests/validator_deep_coverage.rs index 8987369e..0e71bcbe 100644 --- a/native/rust/validation/core/tests/validator_deep_coverage.rs +++ b/native/rust/validation/core/tests/validator_deep_coverage.rs @@ -28,6 +28,7 @@ use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFa use cose_sign1_validation_primitives::plan::CompiledTrustPlan; use cose_sign1_validation_primitives::policy::TrustPolicyBuilder; use cose_sign1_validation_test_utils::SimpleTrustPack; +use std::borrow::Cow; use std::future::Future; use std::io::{Cursor, Read}; use std::pin::Pin; @@ -488,7 +489,7 @@ impl CounterSignatureResolver for FakeCounterSigResolver { struct IntegrityFactProducer { sig_structure_intact: bool, - details: Option, + details: Option>, } impl TrustFactProducer for IntegrityFactProducer { @@ -1274,7 +1275,7 @@ fn counter_sig_bypass_sync_resolution_failed_with_integrity_fact() { let counter_sig_resolver: Arc = Arc::new(FakeCounterSigResolver); let integrity_producer: Arc = Arc::new(IntegrityFactProducer { sig_structure_intact: true, - details: Some("MST receipt verified".to_string()), + details: Some(Cow::Borrowed("MST receipt verified")), }); let message_producer = CoseSign1MessageFactProducer::new() @@ -1371,7 +1372,7 @@ fn counter_sig_bypass_sync_resolution_succeeded_with_integrity_fact() { let counter_sig_resolver: Arc = Arc::new(FakeCounterSigResolver); let integrity_producer: Arc = Arc::new(IntegrityFactProducer { sig_structure_intact: true, - details: Some("envelope verified".to_string()), + details: Some(Cow::Borrowed("envelope verified")), }); let message_producer = CoseSign1MessageFactProducer::new() @@ -1473,7 +1474,7 @@ fn async_counter_sig_bypass_resolution_failed_success() { let counter_sig_resolver: Arc = Arc::new(FakeCounterSigResolver); let integrity_producer: Arc = Arc::new(IntegrityFactProducer { sig_structure_intact: true, - details: Some("async bypass".to_string()), + details: Some(Cow::Borrowed("async bypass")), }); let message_producer = CoseSign1MessageFactProducer::new() @@ -1555,7 +1556,7 @@ fn async_counter_sig_bypass_resolution_succeeded_success() { let counter_sig_resolver: Arc = Arc::new(FakeCounterSigResolver); let integrity_producer: Arc = Arc::new(IntegrityFactProducer { sig_structure_intact: true, - details: Some("async resolved bypass".to_string()), + details: Some(Cow::Borrowed("async resolved bypass")), }); let message_producer = CoseSign1MessageFactProducer::new() @@ -1649,7 +1650,7 @@ fn counter_sig_bypass_includes_details_metadata() { let counter_sig_resolver: Arc = Arc::new(FakeCounterSigResolver); let integrity_producer: Arc = Arc::new(IntegrityFactProducer { sig_structure_intact: true, - details: Some("sha256 verified".to_string()), + details: Some(Cow::Borrowed("sha256 verified")), }); let message_producer = CoseSign1MessageFactProducer::new() diff --git a/native/rust/validation/primitives/src/facts.rs b/native/rust/validation/primitives/src/facts.rs index 24e45880..ea46457b 100644 --- a/native/rust/validation/primitives/src/facts.rs +++ b/native/rust/validation/primitives/src/facts.rs @@ -113,6 +113,12 @@ impl<'a> TrustFactContext<'a> { self.engine.cose_sign1_message.as_deref() } + /// Parsed COSE message as an `Arc`, avoiding a deep clone when the caller + /// already needs shared ownership. + pub fn cose_sign1_message_arc(&self) -> Option> { + self.engine.cose_sign1_message.as_ref().map(Arc::clone) + } + /// Which COSE header location rules should consult. pub fn cose_header_location(&self) -> CoseHeaderLocation { self.engine.cose_header_location From 52afd8700c95a80f0b7c66aa5443bcc22c748ba9 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Tue, 7 Apr 2026 08:21:07 -0700 Subject: [PATCH 5/8] Fix rustfmt formatting for CI: wrap long lines, sort imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../x509/ffi/tests/additional_ffi_coverage.rs | 2 +- .../did/x509/ffi/tests/ffi_rsa_coverage.rs | 1938 ++++++------ .../ffi/tests/resolve_validate_coverage.rs | 614 ++-- native/rust/did/x509/src/policy_validators.rs | 5 +- native/rust/did/x509/src/resolver.rs | 4 +- .../x509/tests/additional_coverage_tests.rs | 700 ++--- native/rust/did/x509/tests/builder_tests.rs | 710 ++--- .../rust/did/x509/tests/did_document_tests.rs | 172 +- .../did/x509/tests/policy_validator_tests.rs | 773 ++--- .../x509/tests/policy_validators_coverage.rs | 744 ++--- .../did/x509/tests/surgical_did_coverage.rs | 2798 +++++++++-------- .../did/x509/tests/x509_extensions_rcgen.rs | 474 +-- .../did/x509/tests/x509_extensions_tests.rs | 305 +- .../certificates/src/validation/facts.rs | 18 +- .../mst/src/validation/facts.rs | 426 ++- .../mst/src/validation/receipt_verify.rs | 1472 +++++---- .../mst/tests/final_targeted_mst_coverage.rs | 2 +- .../core/src/message_fact_producer.rs | 3 +- .../tests/message_fact_producer_raw_cwt.rs | 4 +- .../core/tests/targeted_coverage_gaps.rs | 2 +- 20 files changed, 5604 insertions(+), 5562 deletions(-) diff --git a/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs b/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs index 97d97d44..08efe680 100644 --- a/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs +++ b/native/rust/did/x509/ffi/tests/additional_ffi_coverage.rs @@ -5,12 +5,12 @@ //! //! These tests focus on uncovered paths in the FFI layer. -use std::borrow::Cow; use did_x509::builder::DidX509Builder; use did_x509::models::policy::DidX509Policy; use did_x509_ffi::*; use rcgen::string::Ia5String; use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; +use std::borrow::Cow; use std::ffi::{CStr, CString}; use std::ptr; diff --git a/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs b/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs index 1e2647ca..4a518788 100644 --- a/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs +++ b/native/rust/did/x509/ffi/tests/ffi_rsa_coverage.rs @@ -1,969 +1,969 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional FFI coverage tests to improve coverage on resolve, validate, and build paths. - -use std::borrow::Cow; -use did_x509::builder::DidX509Builder; -use did_x509::models::policy::DidX509Policy; -use did_x509_ffi::*; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::hash::MessageDigest; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::{X509Builder, X509NameBuilder}; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; -use std::ffi::{CStr, CString}; -use std::ptr; - -/// Helper to get error message from an error handle. -fn error_message(err: *const DidX509ErrorHandle) -> Option { - if err.is_null() { - return None; - } - let msg = unsafe { did_x509_error_message(err) }; - if msg.is_null() { - return None; - } - let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); - unsafe { did_x509_string_free(msg) }; - Some(s) -} - -/// Generate an RSA certificate using openssl. -fn generate_rsa_cert() -> Vec { - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - - let serial = BigNum::from_u32(1).unwrap(); - builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - let mut name_builder = X509NameBuilder::new().unwrap(); - name_builder - .append_entry_by_text("CN", "RSA Test Certificate") - .unwrap(); - let name = name_builder.build(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - - let not_before = Asn1Time::days_from_now(0).unwrap(); - let not_after = Asn1Time::days_from_now(365).unwrap(); - builder.set_not_before(¬_before).unwrap(); - builder.set_not_after(¬_after).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - - let eku = openssl::x509::extension::ExtendedKeyUsage::new() - .code_signing() - .build() - .unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Generate an EC certificate using rcgen. -fn generate_ec_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "EC Test Certificate"); - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -#[test] -fn test_ffi_resolve_rsa_certificate() { - let cert_der = generate_rsa_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_json.is_null()); - - // Verify RSA key type in result - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - assert!(json_str.contains("RSA"), "Should contain RSA key type"); - - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_rsa_certificate() { - let cert_der = generate_rsa_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert_eq!(is_valid, 1, "RSA certificate should be valid"); - - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_ec_certificate() { - let cert_der = generate_ec_cert(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_build_from_chain( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_did.is_null()); - - let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); - assert!( - did_str.starts_with("did:x509:"), - "Should be a valid DID:x509" - ); - - unsafe { - did_x509_string_free(result_did); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_ec_certificate() { - let cert_der = generate_ec_cert(); - - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_build_with_eku( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_did.is_null()); - - let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); - assert!( - did_str.starts_with("did:x509:"), - "Should be a valid DID:x509" - ); - assert!(did_str.contains("eku"), "Should contain EKU policy"); - - unsafe { - did_x509_string_free(result_did); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parse_and_get_fields() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Parse - let status = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - assert_eq!(status, DID_X509_OK, "Parse should succeed"); - assert!(!handle.is_null()); - - // Get fingerprint - let mut fingerprint: *mut libc::c_char = ptr::null_mut(); - let status = impl_parsed_get_fingerprint_inner(handle, &mut fingerprint, &mut error); - assert_eq!(status, DID_X509_OK, "Get fingerprint should succeed"); - assert!(!fingerprint.is_null()); - - let fp_str = unsafe { CStr::from_ptr(fingerprint) }.to_str().unwrap(); - assert_eq!( - fp_str.len(), - 64, - "SHA256 fingerprint should be 64 hex chars" - ); - - // Get hash algorithm - let mut algorithm: *mut libc::c_char = ptr::null_mut(); - let status = impl_parsed_get_hash_algorithm_inner(handle, &mut algorithm, &mut error); - assert_eq!(status, DID_X509_OK, "Get algorithm should succeed"); - assert!(!algorithm.is_null()); - - let alg_str = unsafe { CStr::from_ptr(algorithm) }.to_str().unwrap(); - assert_eq!(alg_str, "sha256", "Should be sha256"); - - // Get policy count - let mut count: u32 = 0; - let status = impl_parsed_get_policy_count_inner(handle, &mut count); - assert_eq!(status, DID_X509_OK, "Get policy count should succeed"); - assert_eq!(count, 1, "Should have 1 policy"); - - // Clean up - unsafe { - did_x509_string_free(fingerprint); - did_x509_string_free(algorithm); - did_x509_parsed_free(handle); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_ec_verify_document_structure() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - assert_eq!(status, DID_X509_OK); - assert!(!result_json.is_null()); - - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - - // Verify EC key in result - assert!(json_str.contains("EC"), "Should contain EC key type"); - assert!(json_str.contains("P-256"), "Should contain P-256 curve"); - assert!( - json_str.contains("JsonWebKey2020"), - "Should contain JsonWebKey2020" - ); - - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_error_code_accessor() { - // Create an error by passing invalid arguments - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Parse with null string should create an error - let status = impl_parse_inner(ptr::null(), &mut handle, &mut error); - - assert_ne!(status, DID_X509_OK); - assert!(!error.is_null()); - - // Test error code accessor - let code = unsafe { did_x509_error_code(error) }; - assert!(code != 0, "Error code should be non-zero"); - - // Clean up - unsafe { - did_x509_error_free(error); - } -} - -#[test] -fn test_ffi_build_with_eku_null_output_pointer() { - let cert_der = generate_ec_cert(); - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null for out_did_string - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - ptr::null_mut(), - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_cert() { - let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); - let eku_oids = [eku_oid.as_ptr()]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null cert with non-zero len - let status = impl_build_with_eku_inner( - ptr::null(), - 10, // non-zero length but null pointer - eku_oids.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_oids() { - let cert_der = generate_ec_cert(); - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Pass null eku_oids with non-zero count - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - ptr::null(), - 1, // non-zero count but null pointer - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_with_eku_null_oid_entry() { - let cert_der = generate_ec_cert(); - let eku_oids: [*const libc::c_char; 1] = [ptr::null()]; // Null entry - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_with_eku_inner( - cert_der.as_ptr(), - cert_der.len() as u32, - eku_oids.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_output() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_certs() { - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - ptr::null(), // null certs - ptr::null(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_zero_count() { - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - - let status = impl_build_from_chain_inner( - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_build_from_chain_null_cert_entry() { - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; // non-zero len but null pointer - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_build_from_chain_inner( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_is_valid() { - let cert_der = generate_ec_cert(); - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null out_is_valid - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_did() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - ptr::null(), // null DID - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_chain() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - ptr::null(), // null chain - ptr::null(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_zero_chain_count() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_validate_null_chain_entry() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_validate_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_output() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_did() { - let cert_der = generate_ec_cert(); - let chain_certs = [cert_der.as_ptr()]; - let chain_cert_lens = [cert_der.len() as u32]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - ptr::null(), // null DID - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_chain() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - ptr::null(), // null chain - ptr::null(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_zero_chain_count() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let certs: [*const u8; 0] = []; - let lens: [u32; 0] = []; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - certs.as_ptr(), - lens.as_ptr(), - 0, // zero count - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_resolve_null_chain_entry() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let chain_certs: [*const u8; 1] = [ptr::null()]; - let chain_cert_lens: [u32; 1] = [10]; - let mut result: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_resolve_inner( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_fingerprint_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - // Test null output - let status = impl_parsed_get_fingerprint_inner(handle, ptr::null_mut(), &mut error); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_fingerprint_null_handle() { - let mut fingerprint: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parsed_get_fingerprint_inner( - ptr::null(), // null handle - &mut fingerprint, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_algorithm_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - let status = impl_parsed_get_hash_algorithm_inner( - handle, - ptr::null_mut(), // null output - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_algorithm_null_handle() { - let mut algorithm: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parsed_get_hash_algorithm_inner( - ptr::null(), // null handle - &mut algorithm, - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_policy_count_null_output() { - let cert_der = generate_ec_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); - - let status = impl_parsed_get_policy_count_inner( - handle, - ptr::null_mut(), // null output - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - - unsafe { - if !handle.is_null() { - did_x509_parsed_free(handle); - } - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_ffi_parsed_get_policy_count_null_handle() { - let mut count: u32 = 0; - - let status = impl_parsed_get_policy_count_inner( - ptr::null(), // null handle - &mut count, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); -} - -#[test] -fn test_ffi_parse_null_output_handle() { - let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - let status = impl_parse_inner( - did_cstring.as_ptr(), - ptr::null_mut(), // null output handle - &mut error, - ); - - assert_eq!(status, DID_X509_ERR_NULL_POINTER); - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} \ No newline at end of file +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional FFI coverage tests to improve coverage on resolve, validate, and build paths. + +use did_x509::builder::DidX509Builder; +use did_x509::models::policy::DidX509Policy; +use did_x509_ffi::*; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::{X509Builder, X509NameBuilder}; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair}; +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::ptr; + +/// Helper to get error message from an error handle. +fn error_message(err: *const DidX509ErrorHandle) -> Option { + if err.is_null() { + return None; + } + let msg = unsafe { did_x509_error_message(err) }; + if msg.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); + unsafe { did_x509_string_free(msg) }; + Some(s) +} + +/// Generate an RSA certificate using openssl. +fn generate_rsa_cert() -> Vec { + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + + let serial = BigNum::from_u32(1).unwrap(); + builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + let mut name_builder = X509NameBuilder::new().unwrap(); + name_builder + .append_entry_by_text("CN", "RSA Test Certificate") + .unwrap(); + let name = name_builder.build(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + builder.set_not_before(¬_before).unwrap(); + builder.set_not_after(¬_after).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + + let eku = openssl::x509::extension::ExtendedKeyUsage::new() + .code_signing() + .build() + .unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Generate an EC certificate using rcgen. +fn generate_ec_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "EC Test Certificate"); + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +#[test] +fn test_ffi_resolve_rsa_certificate() { + let cert_der = generate_rsa_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_json.is_null()); + + // Verify RSA key type in result + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + assert!(json_str.contains("RSA"), "Should contain RSA key type"); + + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_rsa_certificate() { + let cert_der = generate_rsa_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert_eq!(is_valid, 1, "RSA certificate should be valid"); + + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_ec_certificate() { + let cert_der = generate_ec_cert(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_build_from_chain( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_did.is_null()); + + let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); + assert!( + did_str.starts_with("did:x509:"), + "Should be a valid DID:x509" + ); + + unsafe { + did_x509_string_free(result_did); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_ec_certificate() { + let cert_der = generate_ec_cert(); + + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_build_with_eku( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_did.is_null()); + + let did_str = unsafe { CStr::from_ptr(result_did) }.to_str().unwrap(); + assert!( + did_str.starts_with("did:x509:"), + "Should be a valid DID:x509" + ); + assert!(did_str.contains("eku"), "Should contain EKU policy"); + + unsafe { + did_x509_string_free(result_did); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parse_and_get_fields() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Parse + let status = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + assert_eq!(status, DID_X509_OK, "Parse should succeed"); + assert!(!handle.is_null()); + + // Get fingerprint + let mut fingerprint: *mut libc::c_char = ptr::null_mut(); + let status = impl_parsed_get_fingerprint_inner(handle, &mut fingerprint, &mut error); + assert_eq!(status, DID_X509_OK, "Get fingerprint should succeed"); + assert!(!fingerprint.is_null()); + + let fp_str = unsafe { CStr::from_ptr(fingerprint) }.to_str().unwrap(); + assert_eq!( + fp_str.len(), + 64, + "SHA256 fingerprint should be 64 hex chars" + ); + + // Get hash algorithm + let mut algorithm: *mut libc::c_char = ptr::null_mut(); + let status = impl_parsed_get_hash_algorithm_inner(handle, &mut algorithm, &mut error); + assert_eq!(status, DID_X509_OK, "Get algorithm should succeed"); + assert!(!algorithm.is_null()); + + let alg_str = unsafe { CStr::from_ptr(algorithm) }.to_str().unwrap(); + assert_eq!(alg_str, "sha256", "Should be sha256"); + + // Get policy count + let mut count: u32 = 0; + let status = impl_parsed_get_policy_count_inner(handle, &mut count); + assert_eq!(status, DID_X509_OK, "Get policy count should succeed"); + assert_eq!(count, 1, "Should have 1 policy"); + + // Clean up + unsafe { + did_x509_string_free(fingerprint); + did_x509_string_free(algorithm); + did_x509_parsed_free(handle); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_ec_verify_document_structure() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + assert_eq!(status, DID_X509_OK); + assert!(!result_json.is_null()); + + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + + // Verify EC key in result + assert!(json_str.contains("EC"), "Should contain EC key type"); + assert!(json_str.contains("P-256"), "Should contain P-256 curve"); + assert!( + json_str.contains("JsonWebKey2020"), + "Should contain JsonWebKey2020" + ); + + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_error_code_accessor() { + // Create an error by passing invalid arguments + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Parse with null string should create an error + let status = impl_parse_inner(ptr::null(), &mut handle, &mut error); + + assert_ne!(status, DID_X509_OK); + assert!(!error.is_null()); + + // Test error code accessor + let code = unsafe { did_x509_error_code(error) }; + assert!(code != 0, "Error code should be non-zero"); + + // Clean up + unsafe { + did_x509_error_free(error); + } +} + +#[test] +fn test_ffi_build_with_eku_null_output_pointer() { + let cert_der = generate_ec_cert(); + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null for out_did_string + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + ptr::null_mut(), + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_cert() { + let eku_oid = CString::new("1.3.6.1.5.5.7.3.3").unwrap(); + let eku_oids = [eku_oid.as_ptr()]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null cert with non-zero len + let status = impl_build_with_eku_inner( + ptr::null(), + 10, // non-zero length but null pointer + eku_oids.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_oids() { + let cert_der = generate_ec_cert(); + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Pass null eku_oids with non-zero count + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + ptr::null(), + 1, // non-zero count but null pointer + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_with_eku_null_oid_entry() { + let cert_der = generate_ec_cert(); + let eku_oids: [*const libc::c_char; 1] = [ptr::null()]; // Null entry + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_with_eku_inner( + cert_der.as_ptr(), + cert_der.len() as u32, + eku_oids.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_output() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_certs() { + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + ptr::null(), // null certs + ptr::null(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_zero_count() { + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + + let status = impl_build_from_chain_inner( + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_build_from_chain_null_cert_entry() { + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; // non-zero len but null pointer + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_build_from_chain_inner( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_is_valid() { + let cert_der = generate_ec_cert(); + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null out_is_valid + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_did() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + ptr::null(), // null DID + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_chain() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + ptr::null(), // null chain + ptr::null(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_zero_chain_count() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_validate_null_chain_entry() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_validate_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_output() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_did() { + let cert_der = generate_ec_cert(); + let chain_certs = [cert_der.as_ptr()]; + let chain_cert_lens = [cert_der.len() as u32]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + ptr::null(), // null DID + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_chain() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + ptr::null(), // null chain + ptr::null(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_zero_chain_count() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let certs: [*const u8; 0] = []; + let lens: [u32; 0] = []; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + certs.as_ptr(), + lens.as_ptr(), + 0, // zero count + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_INVALID_ARGUMENT); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_resolve_null_chain_entry() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let chain_certs: [*const u8; 1] = [ptr::null()]; + let chain_cert_lens: [u32; 1] = [10]; + let mut result: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_resolve_inner( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_fingerprint_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + // Test null output + let status = impl_parsed_get_fingerprint_inner(handle, ptr::null_mut(), &mut error); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_fingerprint_null_handle() { + let mut fingerprint: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parsed_get_fingerprint_inner( + ptr::null(), // null handle + &mut fingerprint, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_algorithm_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + let status = impl_parsed_get_hash_algorithm_inner( + handle, + ptr::null_mut(), // null output + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_algorithm_null_handle() { + let mut algorithm: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parsed_get_hash_algorithm_inner( + ptr::null(), // null handle + &mut algorithm, + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_policy_count_null_output() { + let cert_der = generate_ec_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + let mut handle: *mut DidX509ParsedHandle = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let _ = impl_parse_inner(did_cstring.as_ptr(), &mut handle, &mut error); + + let status = impl_parsed_get_policy_count_inner( + handle, + ptr::null_mut(), // null output + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + + unsafe { + if !handle.is_null() { + did_x509_parsed_free(handle); + } + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_ffi_parsed_get_policy_count_null_handle() { + let mut count: u32 = 0; + + let status = impl_parsed_get_policy_count_inner( + ptr::null(), // null handle + &mut count, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); +} + +#[test] +fn test_ffi_parse_null_output_handle() { + let did_cstring = CString::new("did:x509:0:sha256::eku:1.3.6.1.5.5.7.3.3").unwrap(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + let status = impl_parse_inner( + did_cstring.as_ptr(), + ptr::null_mut(), // null output handle + &mut error, + ); + + assert_eq!(status, DID_X509_ERR_NULL_POINTER); + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} diff --git a/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs b/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs index 2a3a7ea7..363e2ad1 100644 --- a/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs +++ b/native/rust/did/x509/ffi/tests/resolve_validate_coverage.rs @@ -1,307 +1,307 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive coverage tests for DID:x509 FFI resolve, validate, and build functions. -//! -//! These tests target uncovered paths in impl_*_inner functions to achieve full coverage. - -use std::borrow::Cow; -use did_x509::builder::DidX509Builder; -use did_x509::models::policy::DidX509Policy; -use did_x509_ffi::*; -use rcgen::string::Ia5String; -use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; -use serde_json::Value; -use std::ffi::{CStr, CString}; -use std::ptr; - -/// Helper to get error message from an error handle. -fn error_message(err: *const DidX509ErrorHandle) -> Option { - if err.is_null() { - return None; - } - let msg = unsafe { did_x509_error_message(err) }; - if msg.is_null() { - return None; - } - let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); - unsafe { did_x509_string_free(msg) }; - Some(s) -} - -/// Generate a self-signed X.509 certificate with code signing EKU using rcgen. -fn generate_code_signing_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Certificate"); - - // Add Extended Key Usage for Code Signing - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; - - // Add Subject Alternative Name - params.subject_alt_names = vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate invalid certificate data (garbage bytes). -fn generate_invalid_cert() -> Vec { - vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER -} - -#[test] -fn test_resolve_inner_happy_path() { - // Generate a valid certificate and build proper DID - let cert_der = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the resolve function - let status = unsafe { - did_x509_resolve( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - // Verify success - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert!(!result_json.is_null()); - - // Parse the JSON result - let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); - let doc: Value = serde_json::from_str(json_str).unwrap(); - - // Verify the DID document structure - assert_eq!(doc["id"], did_string); - assert!(doc["verificationMethod"].is_array()); - assert_eq!(doc["verificationMethod"][0]["type"], "JsonWebKey2020"); - - // Clean up - unsafe { - did_x509_string_free(result_json); - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_resolve_inner_invalid_did() { - // Generate a valid certificate - let cert_der = generate_code_signing_cert(); - - // Use an invalid DID string (completely malformed) - let invalid_did = CString::new("not-a-did-at-all").unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_json: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the resolve function - let status = unsafe { - did_x509_resolve( - invalid_did.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_json, - &mut error, - ) - }; - - // Verify failure - assert_ne!(status, DID_X509_OK); - assert!(result_json.is_null()); - assert!(!error.is_null()); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("must start with 'did:x509'"), - "Error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_validate_inner_matching_chain() { - // Generate a valid certificate and build proper DID - let cert_der = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain - let cert_ptr = cert_der.as_ptr(); - let cert_len = cert_der.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = 0; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the validate function - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - // Verify success and validity - assert_eq!( - status, - DID_X509_OK, - "Expected success, got error: {:?}", - error_message(error) - ); - assert_eq!(is_valid, 1, "Certificate should be valid for the DID"); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_validate_inner_wrong_chain() { - // Generate one certificate - let cert_der1 = generate_code_signing_cert(); - - // Calculate fingerprint for a different certificate - let cert_der2 = generate_code_signing_cert(); - let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); - let did_string = DidX509Builder::build_sha256(&cert_der2, &[policy]).expect("Should build DID"); - - // Build DID for cert2 but validate against cert1 - let did_cstring = CString::new(did_string.as_str()).unwrap(); - - // Prepare certificate chain with cert1 (doesn't match DID fingerprint) - let cert_ptr = cert_der1.as_ptr(); - let cert_len = cert_der1.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut is_valid: i32 = -1; - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the validate function - let status = unsafe { - did_x509_validate( - did_cstring.as_ptr(), - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut is_valid, - &mut error, - ) - }; - - // Verify the operation should fail because the fingerprint doesn't match - assert_ne!(status, DID_X509_OK); - assert_ne!( - is_valid, 1, - "Certificate should not be valid for the mismatched DID" - ); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("fingerprint"), - "Should be a fingerprint mismatch error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} - -#[test] -fn test_build_from_chain_invalid_cert() { - // Use invalid certificate data (garbage bytes) - let invalid_cert = generate_invalid_cert(); - - // Prepare certificate chain with invalid cert - let cert_ptr = invalid_cert.as_ptr(); - let cert_len = invalid_cert.len() as u32; - let chain_certs = [cert_ptr]; - let chain_cert_lens = [cert_len]; - - let mut result_did: *mut libc::c_char = ptr::null_mut(); - let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); - - // Call the build_from_chain function - let status = unsafe { - did_x509_build_from_chain( - chain_certs.as_ptr(), - chain_cert_lens.as_ptr(), - 1, - &mut result_did, - &mut error, - ) - }; - - // Verify failure - assert_ne!(status, DID_X509_OK); - assert!(result_did.is_null()); - assert!(!error.is_null()); - - let err_msg = error_message(error).unwrap(); - assert!( - err_msg.contains("parse") || err_msg.contains("build") || err_msg.contains("invalid"), - "Error: {}", - err_msg - ); - - // Clean up - unsafe { - if !error.is_null() { - did_x509_error_free(error); - } - } -} \ No newline at end of file +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive coverage tests for DID:x509 FFI resolve, validate, and build functions. +//! +//! These tests target uncovered paths in impl_*_inner functions to achieve full coverage. + +use did_x509::builder::DidX509Builder; +use did_x509::models::policy::DidX509Policy; +use did_x509_ffi::*; +use rcgen::string::Ia5String; +use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType as RcgenSanType}; +use serde_json::Value; +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::ptr; + +/// Helper to get error message from an error handle. +fn error_message(err: *const DidX509ErrorHandle) -> Option { + if err.is_null() { + return None; + } + let msg = unsafe { did_x509_error_message(err) }; + if msg.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(msg) }.to_string_lossy().to_string(); + unsafe { did_x509_string_free(msg) }; + Some(s) +} + +/// Generate a self-signed X.509 certificate with code signing EKU using rcgen. +fn generate_code_signing_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Certificate"); + + // Add Extended Key Usage for Code Signing + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + + // Add Subject Alternative Name + params.subject_alt_names = vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate invalid certificate data (garbage bytes). +fn generate_invalid_cert() -> Vec { + vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF] // Invalid DER +} + +#[test] +fn test_resolve_inner_happy_path() { + // Generate a valid certificate and build proper DID + let cert_der = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the resolve function + let status = unsafe { + did_x509_resolve( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + // Verify success + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert!(!result_json.is_null()); + + // Parse the JSON result + let json_str = unsafe { CStr::from_ptr(result_json) }.to_str().unwrap(); + let doc: Value = serde_json::from_str(json_str).unwrap(); + + // Verify the DID document structure + assert_eq!(doc["id"], did_string); + assert!(doc["verificationMethod"].is_array()); + assert_eq!(doc["verificationMethod"][0]["type"], "JsonWebKey2020"); + + // Clean up + unsafe { + did_x509_string_free(result_json); + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_resolve_inner_invalid_did() { + // Generate a valid certificate + let cert_der = generate_code_signing_cert(); + + // Use an invalid DID string (completely malformed) + let invalid_did = CString::new("not-a-did-at-all").unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_json: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the resolve function + let status = unsafe { + did_x509_resolve( + invalid_did.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_json, + &mut error, + ) + }; + + // Verify failure + assert_ne!(status, DID_X509_OK); + assert!(result_json.is_null()); + assert!(!error.is_null()); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("must start with 'did:x509'"), + "Error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_validate_inner_matching_chain() { + // Generate a valid certificate and build proper DID + let cert_der = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der, &[policy]).expect("Should build DID"); + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain + let cert_ptr = cert_der.as_ptr(); + let cert_len = cert_der.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = 0; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the validate function + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + // Verify success and validity + assert_eq!( + status, + DID_X509_OK, + "Expected success, got error: {:?}", + error_message(error) + ); + assert_eq!(is_valid, 1, "Certificate should be valid for the DID"); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_validate_inner_wrong_chain() { + // Generate one certificate + let cert_der1 = generate_code_signing_cert(); + + // Calculate fingerprint for a different certificate + let cert_der2 = generate_code_signing_cert(); + let policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]); + let did_string = DidX509Builder::build_sha256(&cert_der2, &[policy]).expect("Should build DID"); + + // Build DID for cert2 but validate against cert1 + let did_cstring = CString::new(did_string.as_str()).unwrap(); + + // Prepare certificate chain with cert1 (doesn't match DID fingerprint) + let cert_ptr = cert_der1.as_ptr(); + let cert_len = cert_der1.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut is_valid: i32 = -1; + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the validate function + let status = unsafe { + did_x509_validate( + did_cstring.as_ptr(), + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut is_valid, + &mut error, + ) + }; + + // Verify the operation should fail because the fingerprint doesn't match + assert_ne!(status, DID_X509_OK); + assert_ne!( + is_valid, 1, + "Certificate should not be valid for the mismatched DID" + ); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("fingerprint"), + "Should be a fingerprint mismatch error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} + +#[test] +fn test_build_from_chain_invalid_cert() { + // Use invalid certificate data (garbage bytes) + let invalid_cert = generate_invalid_cert(); + + // Prepare certificate chain with invalid cert + let cert_ptr = invalid_cert.as_ptr(); + let cert_len = invalid_cert.len() as u32; + let chain_certs = [cert_ptr]; + let chain_cert_lens = [cert_len]; + + let mut result_did: *mut libc::c_char = ptr::null_mut(); + let mut error: *mut DidX509ErrorHandle = ptr::null_mut(); + + // Call the build_from_chain function + let status = unsafe { + did_x509_build_from_chain( + chain_certs.as_ptr(), + chain_cert_lens.as_ptr(), + 1, + &mut result_did, + &mut error, + ) + }; + + // Verify failure + assert_ne!(status, DID_X509_OK); + assert!(result_did.is_null()); + assert!(!error.is_null()); + + let err_msg = error_message(error).unwrap(); + assert!( + err_msg.contains("parse") || err_msg.contains("build") || err_msg.contains("invalid"), + "Error: {}", + err_msg + ); + + // Clean up + unsafe { + if !error.is_null() { + did_x509_error_free(error); + } + } +} diff --git a/native/rust/did/x509/src/policy_validators.rs b/native/rust/did/x509/src/policy_validators.rs index e69d848b..41118dc8 100644 --- a/native/rust/did/x509/src/policy_validators.rs +++ b/native/rust/did/x509/src/policy_validators.rs @@ -10,7 +10,10 @@ use std::borrow::Cow; use x509_parser::prelude::*; /// Validate Extended Key Usage (EKU) policy -pub fn validate_eku(cert: &X509Certificate, expected_oids: &[Cow<'static, str>]) -> Result<(), DidX509Error> { +pub fn validate_eku( + cert: &X509Certificate, + expected_oids: &[Cow<'static, str>], +) -> Result<(), DidX509Error> { let ekus = x509_extensions::extract_extended_key_usage(cert); if ekus.is_empty() { diff --git a/native/rust/did/x509/src/resolver.rs b/native/rust/did/x509/src/resolver.rs index 05c7f90a..51ad474d 100644 --- a/native/rust/did/x509/src/resolver.rs +++ b/native/rust/did/x509/src/resolver.rs @@ -107,7 +107,9 @@ impl DidX509Resolver { } /// Convert X.509 certificate public key to JWK format - fn public_key_to_jwk(cert: &X509Certificate) -> Result, String>, DidX509Error> { + fn public_key_to_jwk( + cert: &X509Certificate, + ) -> Result, String>, DidX509Error> { let public_key = cert.public_key(); match public_key.parsed() { diff --git a/native/rust/did/x509/tests/additional_coverage_tests.rs b/native/rust/did/x509/tests/additional_coverage_tests.rs index 00ce54d3..3d418599 100644 --- a/native/rust/did/x509/tests/additional_coverage_tests.rs +++ b/native/rust/did/x509/tests/additional_coverage_tests.rs @@ -1,350 +1,350 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional coverage tests for DID:x509 library to achieve 90% line coverage. -//! -//! These tests focus on: -//! 1. resolver.rs - EC JWK conversion paths, edge cases -//! 2. x509_extensions.rs - EKU extraction, CA detection -//! 3. Base64 encoding edge cases - -use did_x509::builder::DidX509Builder; -use did_x509::error::DidX509Error; -use did_x509::models::policy::DidX509Policy; -use did_x509::resolver::DidX509Resolver; -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use rcgen::string::Ia5String; -use rcgen::{ - BasicConstraints as RcgenBasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, - IsCa, KeyPair, SanType as RcgenSanType, -}; -use x509_parser::prelude::*; -use std::borrow::Cow; - -/// Generate an EC certificate with code signing EKU -fn generate_ec_cert_with_eku(ekus: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Certificate"); - params.extended_key_usages = ekus; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a CA certificate with BasicConstraints(CA:true) -fn generate_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test CA Certificate"); - params.is_ca = IsCa::Ca(RcgenBasicConstraints::Unconstrained); - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a non-CA certificate -fn generate_non_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Non-CA Certificate"); - params.is_ca = IsCa::NoCa; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate a certificate with multiple EKU extensions -fn generate_multi_eku_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Multi EKU Certificate"); - params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::EmailProtection, - ExtendedKeyUsagePurpose::TimeStamping, - ExtendedKeyUsagePurpose::OcspSigning, - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Generate certificate with no extensions -fn generate_plain_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Plain Certificate"); - // No extended_key_usages, no is_ca, no SAN - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -// ============================================================================ -// Resolver tests - covering EC JWK conversion and base64url encoding -// ============================================================================ - -#[test] -fn test_resolver_ec_p256_jwk() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]); - assert!( - result.is_ok(), - "Should resolve EC P-256 cert: {:?}", - result.err() - ); - - let doc = result.unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - - // Verify EC JWK structure - assert_eq!(jwk.get("kty").unwrap(), "EC"); - assert_eq!(jwk.get("crv").unwrap(), "P-256"); - assert!(jwk.contains_key("x")); - assert!(jwk.contains_key("y")); -} - -#[test] -fn test_resolver_did_document_structure() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); - - // Verify DID Document structure - assert_eq!(result.id, did); - assert!(!result.context.is_empty()); - assert!(result - .context - .contains(&"https://www.w3.org/ns/did/v1".to_string())); - assert_eq!(result.verification_method.len(), 1); - assert_eq!(result.assertion_method.len(), 1); - - // Verify verification method structure - let vm = &result.verification_method[0]; - assert!(vm.id.starts_with(&did)); - assert!(vm.id.ends_with("#key-1")); - assert_eq!(vm.type_, "JsonWebKey2020"); - assert_eq!(vm.controller, did); -} - -#[test] -fn test_resolver_validation_failure() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - // Create DID requiring Code Signing EKU, but cert only has Server Auth - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); // Code Signing - - // Use a correct fingerprint but wrong policy - use sha2::{Digest, Sha256}; - let fingerprint = Sha256::digest(&cert_der); - let fingerprint_hex = hex::encode(fingerprint); - let did = format!( - "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", - fingerprint_hex - ); - - let result = DidX509Resolver::resolve(&did, &[&cert_der]); - assert!( - result.is_err(), - "Should fail - cert doesn't have required EKU" - ); -} - -// ============================================================================ -// x509_extensions tests - covering all standard EKU OIDs -// ============================================================================ - -#[test] -fn test_extract_all_standard_ekus() { - let cert_der = generate_multi_eku_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - - // Should contain all 6 standard EKU OIDs - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), - "Missing ServerAuth" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), - "Missing ClientAuth" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), - "Missing CodeSigning" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), - "Missing EmailProtection" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), - "Missing TimeStamping" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), - "Missing OcspSigning" - ); -} - -#[test] -fn test_extract_single_eku_code_signing() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert_eq!(ekus.len(), 1); - assert_eq!(ekus[0], "1.3.6.1.5.5.7.3.3"); -} - -#[test] -fn test_extract_eku_oids_wrapper_success() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - - let oids = result.unwrap(); - assert!(oids.iter().any(|x| x == "1.3.6.1.5.5.7.3.1")); -} - -#[test] -fn test_extract_eku_no_extension() { - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.is_empty(), - "Cert without EKU extension should return empty vec" - ); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - assert!(result.unwrap().is_empty()); -} - -// ============================================================================ -// CA certificate detection tests -// ============================================================================ - -#[test] -fn test_is_ca_certificate_true() { - let cert_der = generate_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(is_ca, "CA certificate should be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_false() { - let cert_der = generate_non_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Non-CA certificate should not be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_no_basic_constraints() { - // Plain cert has no basic constraints extension at all - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Cert without BasicConstraints should not be CA"); -} - -// ============================================================================ -// Fulcio issuer extraction tests -// ============================================================================ - -#[test] -fn test_extract_fulcio_issuer_none() { - let cert_der = generate_plain_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!( - issuer.is_none(), - "Regular cert should not have Fulcio issuer" - ); -} - -#[test] -fn test_extract_fulcio_issuer_not_present() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!(issuer.is_none()); -} - -// ============================================================================ -// Base64url encoding edge cases (via resolver) -// ============================================================================ - -#[test] -fn test_base64url_no_padding() { - let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); - - let doc = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - - // base64url encoding should NOT have padding characters - let x = jwk.get("x").unwrap(); - let y = jwk.get("y").unwrap(); - - assert!(!x.contains('='), "x should not have padding"); - assert!(!y.contains('='), "y should not have padding"); - assert!(!x.contains('+'), "x should use URL-safe alphabet"); - assert!(!y.contains('+'), "y should use URL-safe alphabet"); - assert!(!x.contains('/'), "x should use URL-safe alphabet"); - assert!(!y.contains('/'), "y should use URL-safe alphabet"); -} - -// ============================================================================ -// Error path coverage -// ============================================================================ - -#[test] -fn test_resolver_empty_chain() { - let did = - "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.3.6.1.5.5.7.3.3"; - - let result = DidX509Resolver::resolve(did, &[]); - assert!(result.is_err(), "Should fail with empty chain"); -} - -#[test] -fn test_resolver_invalid_did_format() { - let cert_der = generate_plain_cert(); - let invalid_did = "not:a:valid:did"; - - let result = DidX509Resolver::resolve(invalid_did, &[&cert_der]); - assert!(result.is_err(), "Should fail with invalid DID format"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional coverage tests for DID:x509 library to achieve 90% line coverage. +//! +//! These tests focus on: +//! 1. resolver.rs - EC JWK conversion paths, edge cases +//! 2. x509_extensions.rs - EKU extraction, CA detection +//! 3. Base64 encoding edge cases + +use did_x509::builder::DidX509Builder; +use did_x509::error::DidX509Error; +use did_x509::models::policy::DidX509Policy; +use did_x509::resolver::DidX509Resolver; +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use rcgen::string::Ia5String; +use rcgen::{ + BasicConstraints as RcgenBasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, + IsCa, KeyPair, SanType as RcgenSanType, +}; +use std::borrow::Cow; +use x509_parser::prelude::*; + +/// Generate an EC certificate with code signing EKU +fn generate_ec_cert_with_eku(ekus: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Certificate"); + params.extended_key_usages = ekus; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a CA certificate with BasicConstraints(CA:true) +fn generate_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test CA Certificate"); + params.is_ca = IsCa::Ca(RcgenBasicConstraints::Unconstrained); + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a non-CA certificate +fn generate_non_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Non-CA Certificate"); + params.is_ca = IsCa::NoCa; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate a certificate with multiple EKU extensions +fn generate_multi_eku_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Multi EKU Certificate"); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ExtendedKeyUsagePurpose::TimeStamping, + ExtendedKeyUsagePurpose::OcspSigning, + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Generate certificate with no extensions +fn generate_plain_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Plain Certificate"); + // No extended_key_usages, no is_ca, no SAN + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +// ============================================================================ +// Resolver tests - covering EC JWK conversion and base64url encoding +// ============================================================================ + +#[test] +fn test_resolver_ec_p256_jwk() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]); + assert!( + result.is_ok(), + "Should resolve EC P-256 cert: {:?}", + result.err() + ); + + let doc = result.unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + + // Verify EC JWK structure + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-256"); + assert!(jwk.contains_key("x")); + assert!(jwk.contains_key("y")); +} + +#[test] +fn test_resolver_did_document_structure() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); + + // Verify DID Document structure + assert_eq!(result.id, did); + assert!(!result.context.is_empty()); + assert!(result + .context + .contains(&"https://www.w3.org/ns/did/v1".to_string())); + assert_eq!(result.verification_method.len(), 1); + assert_eq!(result.assertion_method.len(), 1); + + // Verify verification method structure + let vm = &result.verification_method[0]; + assert!(vm.id.starts_with(&did)); + assert!(vm.id.ends_with("#key-1")); + assert_eq!(vm.type_, "JsonWebKey2020"); + assert_eq!(vm.controller, did); +} + +#[test] +fn test_resolver_validation_failure() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + // Create DID requiring Code Signing EKU, but cert only has Server Auth + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); // Code Signing + + // Use a correct fingerprint but wrong policy + use sha2::{Digest, Sha256}; + let fingerprint = Sha256::digest(&cert_der); + let fingerprint_hex = hex::encode(fingerprint); + let did = format!( + "did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", + fingerprint_hex + ); + + let result = DidX509Resolver::resolve(&did, &[&cert_der]); + assert!( + result.is_err(), + "Should fail - cert doesn't have required EKU" + ); +} + +// ============================================================================ +// x509_extensions tests - covering all standard EKU OIDs +// ============================================================================ + +#[test] +fn test_extract_all_standard_ekus() { + let cert_der = generate_multi_eku_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + + // Should contain all 6 standard EKU OIDs + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Missing ServerAuth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Missing ClientAuth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Missing CodeSigning" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Missing EmailProtection" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Missing TimeStamping" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Missing OcspSigning" + ); +} + +#[test] +fn test_extract_single_eku_code_signing() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert_eq!(ekus.len(), 1); + assert_eq!(ekus[0], "1.3.6.1.5.5.7.3.3"); +} + +#[test] +fn test_extract_eku_oids_wrapper_success() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + + let oids = result.unwrap(); + assert!(oids.iter().any(|x| x == "1.3.6.1.5.5.7.3.1")); +} + +#[test] +fn test_extract_eku_no_extension() { + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.is_empty(), + "Cert without EKU extension should return empty vec" + ); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); +} + +// ============================================================================ +// CA certificate detection tests +// ============================================================================ + +#[test] +fn test_is_ca_certificate_true() { + let cert_der = generate_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(is_ca, "CA certificate should be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_false() { + let cert_der = generate_non_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Non-CA certificate should not be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_no_basic_constraints() { + // Plain cert has no basic constraints extension at all + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Cert without BasicConstraints should not be CA"); +} + +// ============================================================================ +// Fulcio issuer extraction tests +// ============================================================================ + +#[test] +fn test_extract_fulcio_issuer_none() { + let cert_der = generate_plain_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!( + issuer.is_none(), + "Regular cert should not have Fulcio issuer" + ); +} + +#[test] +fn test_extract_fulcio_issuer_not_present() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!(issuer.is_none()); +} + +// ============================================================================ +// Base64url encoding edge cases (via resolver) +// ============================================================================ + +#[test] +fn test_base64url_no_padding() { + let cert_der = generate_ec_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert_der, &[policy]).unwrap(); + + let doc = DidX509Resolver::resolve(&did, &[&cert_der]).unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + + // base64url encoding should NOT have padding characters + let x = jwk.get("x").unwrap(); + let y = jwk.get("y").unwrap(); + + assert!(!x.contains('='), "x should not have padding"); + assert!(!y.contains('='), "y should not have padding"); + assert!(!x.contains('+'), "x should use URL-safe alphabet"); + assert!(!y.contains('+'), "y should use URL-safe alphabet"); + assert!(!x.contains('/'), "x should use URL-safe alphabet"); + assert!(!y.contains('/'), "y should use URL-safe alphabet"); +} + +// ============================================================================ +// Error path coverage +// ============================================================================ + +#[test] +fn test_resolver_empty_chain() { + let did = + "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::eku:1.3.6.1.5.5.7.3.3"; + + let result = DidX509Resolver::resolve(did, &[]); + assert!(result.is_err(), "Should fail with empty chain"); +} + +#[test] +fn test_resolver_invalid_did_format() { + let cert_der = generate_plain_cert(); + let invalid_did = "not:a:valid:did"; + + let result = DidX509Resolver::resolve(invalid_did, &[&cert_der]); + assert!(result.is_err(), "Should fail with invalid DID format"); +} diff --git a/native/rust/did/x509/tests/builder_tests.rs b/native/rust/did/x509/tests/builder_tests.rs index aa77f0fe..d0ced26e 100644 --- a/native/rust/did/x509/tests/builder_tests.rs +++ b/native/rust/did/x509/tests/builder_tests.rs @@ -1,355 +1,355 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use std::borrow::Cow; -use did_x509::{ - builder::DidX509Builder, - constants::*, - models::policy::{DidX509Policy, SanType}, - parsing::DidX509Parser, - DidX509Error, -}; - -// Inline base64 utilities for tests -const BASE64_STANDARD: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { - let mut lookup = [0xFFu8; 256]; - for (i, &c) in alphabet.iter().enumerate() { - lookup[c as usize] = i as u8; - } - - let input = input.trim_end_matches('='); - let mut out = Vec::with_capacity(input.len() * 3 / 4); - let mut buf: u32 = 0; - let mut bits: u32 = 0; - - for &b in input.as_bytes() { - let val = lookup[b as usize]; - if val == 0xFF { - return Err(format!("invalid base64 byte: 0x{:02x}", b)); - } - buf = (buf << 6) | val as u32; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((buf >> bits) as u8); - buf &= (1 << bits) - 1; - } - } - Ok(out) -} - -fn base64_standard_decode(input: &str) -> Result, String> { - base64_decode(input, BASE64_STANDARD) -} - -/// Create a simple self-signed test certificate in DER format -/// This is a minimal test certificate for unit testing purposes -fn create_test_cert_der() -> Vec { - // This is a minimal self-signed certificate encoded in DER format - // Subject: CN=Test CA, O=Test Org - // Validity: Not critical for fingerprint testing - // This is a real DER-encoded certificate for testing - let cert_pem = r#"-----BEGIN CERTIFICATE----- -MIICpDCCAYwCCQDU7T7JbtQhxTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU -ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD -VQQDDAlUZXN0IFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO -8vH0PqH3m3KkjvFnqvqp8aIJYVIqW+aTvnW5VNvz6rQkX8d8VnNqPfGYQxJjMzTl -xJ3FxU7dI5C5PbF8qQqOkZ7lNxL+XH5LPnvZdF3zV8lJxVR5J3LWnE5eQqYHqOkT -yJNlM6xvF8kPqOB7hH5vFXrXxqPvLlQqQqZPvGqHqKFLvLZqQqPvKqQqPvLqQqPv -LqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQq -PvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLq -QqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqAgMBAAEwDQYJKoZIhvcNAQELBQADggEB -AKT3qxYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYq -KYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqK -YqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKY -qLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYq -LVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqL -VYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLV -YqKYqA== ------END CERTIFICATE-----"#; - - // Parse PEM and extract DER - let cert_lines: Vec<&str> = cert_pem - .lines() - .filter(|line| !line.contains("BEGIN") && !line.contains("END")) - .collect(); - let cert_base64 = cert_lines.join(""); - - // Decode base64 to DER - base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") -} - -/// Create a test leaf certificate with EKU extension -fn create_test_leaf_cert_with_eku() -> Vec { - // A test certificate with EKU extension - let cert_pem = r#"-----BEGIN CERTIFICATE----- -MIICrjCCAZYCCQCxvF8bFxMqFjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU -ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD -VQQDDAlUZXN0IExlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP -HqYxNKj5J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx -J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxAgMBAAGj -PDBOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH -AwEwDQYJKoZIhvcNAQELBQADggEBAA== ------END CERTIFICATE-----"#; - - let cert_lines: Vec<&str> = cert_pem - .lines() - .filter(|line| !line.contains("BEGIN") && !line.contains("END")) - .collect(); - let cert_base64 = cert_lines.join(""); - base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") -} - -#[test] -fn test_build_with_eku_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2")); -} - -#[test] -fn test_build_with_multiple_eku_oids() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec![ - "1.3.6.1.5.5.7.3.2".to_string().into(), - "1.3.6.1.5.5.7.3.3".to_string().into(), - ]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2:1.3.6.1.5.5.7.3.3")); -} - -#[test] -fn test_build_with_subject_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Subject(vec![ - ("CN".to_string(), "example.com".to_string()), - ("O".to_string(), "Example Org".to_string()), - ]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::subject:CN:example.com:O:Example%20Org")); -} - -#[test] -fn test_build_with_san_email_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Email, "test@example.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:email:test%40example.com")); -} - -#[test] -fn test_build_with_san_dns_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:dns:example.com")); -} - -#[test] -fn test_build_with_san_uri_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::San(SanType::Uri, "https://example.com/path".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::san:uri:https%3A%2F%2Fexample.com%2Fpath")); -} - -#[test] -fn test_build_with_fulcio_issuer_policy() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string()); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - assert!(did.contains("::fulcio-issuer:accounts.google.com")); -} - -#[test] -fn test_build_with_multiple_policies() { - let ca_cert = create_test_cert_der(); - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), - DidX509Policy::Subject(vec![("CN".to_string(), "test".to_string())]), - ]; - - let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); - - assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2::subject:CN:test")); -} - -#[test] -fn test_build_with_sha256() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA256).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); - // SHA-256 produces 32 bytes = 43 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 43); -} - -#[test] -fn test_build_with_sha384() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA384).unwrap(); - - assert!(did.starts_with("did:x509:0:sha384:")); - // SHA-384 produces 48 bytes = 64 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 64); -} - -#[test] -fn test_build_with_sha512() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - - let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA512).unwrap(); - - assert!(did.starts_with("did:x509:0:sha512:")); - // SHA-512 produces 64 bytes = 86 base64url chars (without padding) - let parts: Vec<&str> = did.split("::").collect(); - let fingerprint_part = parts[0].split(':').last().unwrap(); - assert_eq!(fingerprint_part.len(), 86); -} - -#[test] -fn test_build_with_invalid_hash_algorithm() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - - let result = DidX509Builder::build(&ca_cert, &[policy], "sha1"); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - DidX509Error::UnsupportedHashAlgorithm("sha1".to_string()) - ); -} - -#[test] -fn test_build_from_chain() { - let leaf_cert = create_test_leaf_cert_with_eku(); - let ca_cert = create_test_cert_der(); - let chain: Vec<&[u8]> = vec![&leaf_cert, &ca_cert]; - - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); - - // Should use the last cert (CA) for fingerprint - assert!(did.starts_with("did:x509:0:sha256:")); - assert!(did.contains("::eku:1.2.3.4")); -} - -#[test] -fn test_build_from_chain_empty() { - let chain: Vec<&[u8]> = vec![]; - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - - let result = DidX509Builder::build_from_chain(&chain, &[policy]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - DidX509Error::InvalidChain("Empty chain".to_string()) - ); -} - -#[test] -fn test_build_from_chain_single_cert() { - let ca_cert = create_test_cert_der(); - let chain: Vec<&[u8]> = vec![&ca_cert]; - - let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); - let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); - - assert!(did.starts_with("did:x509:0:sha256:")); -} - -#[test] -fn test_roundtrip_build_and_parse() { - let ca_cert = create_test_cert_der(); - let policies = vec![ - DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), - DidX509Policy::Subject(vec![ - ("CN".to_string(), "test.example.com".to_string()), - ("O".to_string(), "Test Org".to_string()), - ]), - DidX509Policy::San(SanType::Dns, "example.com".to_string()), - ]; - - let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); - - // Parse the built DID - let parsed = DidX509Parser::parse(&did).unwrap(); - - // Verify structure - assert_eq!(parsed.hash_algorithm, HASH_ALGORITHM_SHA256); - assert_eq!(parsed.policies.len(), 3); - - // Verify EKU policy - if let DidX509Policy::Eku(oids) = &parsed.policies[0] { - assert_eq!(oids, &vec!["1.3.6.1.5.5.7.3.2".to_string()]); - } else { - panic!("Expected EKU policy"); - } - - // Verify Subject policy - if let DidX509Policy::Subject(attrs) = &parsed.policies[1] { - assert_eq!(attrs.len(), 2); - assert_eq!(attrs[0], ("CN".to_string(), "test.example.com".to_string())); - assert_eq!(attrs[1], ("O".to_string(), "Test Org".to_string())); - } else { - panic!("Expected Subject policy"); - } - - // Verify SAN policy - if let DidX509Policy::San(san_type, value) = &parsed.policies[2] { - assert_eq!(*san_type, SanType::Dns); - assert_eq!(value, "example.com"); - } else { - panic!("Expected SAN policy"); - } -} - -#[test] -fn test_encode_policy_with_special_characters() { - let ca_cert = create_test_cert_der(); - let policy = DidX509Policy::Subject(vec![( - "CN".to_string(), - "Test: Value, With Special/Chars".to_string(), - )]); - - let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); - - // Special characters should be percent-encoded - assert!(did.contains("%3A")); // colon - assert!(did.contains("%2C")); // comma - assert!(did.contains("%2F")); // slash -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use did_x509::{ + builder::DidX509Builder, + constants::*, + models::policy::{DidX509Policy, SanType}, + parsing::DidX509Parser, + DidX509Error, +}; +use std::borrow::Cow; + +// Inline base64 utilities for tests +const BASE64_STANDARD: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { + let mut lookup = [0xFFu8; 256]; + for (i, &c) in alphabet.iter().enumerate() { + lookup[c as usize] = i as u8; + } + + let input = input.trim_end_matches('='); + let mut out = Vec::with_capacity(input.len() * 3 / 4); + let mut buf: u32 = 0; + let mut bits: u32 = 0; + + for &b in input.as_bytes() { + let val = lookup[b as usize]; + if val == 0xFF { + return Err(format!("invalid base64 byte: 0x{:02x}", b)); + } + buf = (buf << 6) | val as u32; + bits += 6; + if bits >= 8 { + bits -= 8; + out.push((buf >> bits) as u8); + buf &= (1 << bits) - 1; + } + } + Ok(out) +} + +fn base64_standard_decode(input: &str) -> Result, String> { + base64_decode(input, BASE64_STANDARD) +} + +/// Create a simple self-signed test certificate in DER format +/// This is a minimal test certificate for unit testing purposes +fn create_test_cert_der() -> Vec { + // This is a minimal self-signed certificate encoded in DER format + // Subject: CN=Test CA, O=Test Org + // Validity: Not critical for fingerprint testing + // This is a real DER-encoded certificate for testing + let cert_pem = r#"-----BEGIN CERTIFICATE----- +MIICpDCCAYwCCQDU7T7JbtQhxTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU +ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD +VQQDDAlUZXN0IFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO +8vH0PqH3m3KkjvFnqvqp8aIJYVIqW+aTvnW5VNvz6rQkX8d8VnNqPfGYQxJjMzTl +xJ3FxU7dI5C5PbF8qQqOkZ7lNxL+XH5LPnvZdF3zV8lJxVR5J3LWnE5eQqYHqOkT +yJNlM6xvF8kPqOB7hH5vFXrXxqPvLlQqQqZPvGqHqKFLvLZqQqPvKqQqPvLqQqPv +LqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQq +PvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqPvLq +QqPvLqQqPvLqQqPvLqQqPvLqQqPvLqQqAgMBAAEwDQYJKoZIhvcNAQELBQADggEB +AKT3qxYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYq +KYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqK +YqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKY +qLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYq +LVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqL +VYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLVYqKYqLV +YqKYqA== +-----END CERTIFICATE-----"#; + + // Parse PEM and extract DER + let cert_lines: Vec<&str> = cert_pem + .lines() + .filter(|line| !line.contains("BEGIN") && !line.contains("END")) + .collect(); + let cert_base64 = cert_lines.join(""); + + // Decode base64 to DER + base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") +} + +/// Create a test leaf certificate with EKU extension +fn create_test_leaf_cert_with_eku() -> Vec { + // A test certificate with EKU extension + let cert_pem = r#"-----BEGIN CERTIFICATE----- +MIICrjCCAZYCCQCxvF8bFxMqFjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlU +ZXN0IFJvb3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD +VQQDDAlUZXN0IExlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP +HqYxNKj5J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKx +J5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxJ5mH0pKxAgMBAAGj +PDBOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwEwDQYJKoZIhvcNAQELBQADggEBAA== +-----END CERTIFICATE-----"#; + + let cert_lines: Vec<&str> = cert_pem + .lines() + .filter(|line| !line.contains("BEGIN") && !line.contains("END")) + .collect(); + let cert_base64 = cert_lines.join(""); + base64_standard_decode(&cert_base64).expect("Failed to decode test certificate") +} + +#[test] +fn test_build_with_eku_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2")); +} + +#[test] +fn test_build_with_multiple_eku_oids() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec![ + "1.3.6.1.5.5.7.3.2".to_string().into(), + "1.3.6.1.5.5.7.3.3".to_string().into(), + ]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2:1.3.6.1.5.5.7.3.3")); +} + +#[test] +fn test_build_with_subject_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Subject(vec![ + ("CN".to_string(), "example.com".to_string()), + ("O".to_string(), "Example Org".to_string()), + ]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::subject:CN:example.com:O:Example%20Org")); +} + +#[test] +fn test_build_with_san_email_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Email, "test@example.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:email:test%40example.com")); +} + +#[test] +fn test_build_with_san_dns_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:dns:example.com")); +} + +#[test] +fn test_build_with_san_uri_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::San(SanType::Uri, "https://example.com/path".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::san:uri:https%3A%2F%2Fexample.com%2Fpath")); +} + +#[test] +fn test_build_with_fulcio_issuer_policy() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string()); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + assert!(did.contains("::fulcio-issuer:accounts.google.com")); +} + +#[test] +fn test_build_with_multiple_policies() { + let ca_cert = create_test_cert_der(); + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), + DidX509Policy::Subject(vec![("CN".to_string(), "test".to_string())]), + ]; + + let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); + + assert!(did.contains("::eku:1.3.6.1.5.5.7.3.2::subject:CN:test")); +} + +#[test] +fn test_build_with_sha256() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA256).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); + // SHA-256 produces 32 bytes = 43 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 43); +} + +#[test] +fn test_build_with_sha384() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA384).unwrap(); + + assert!(did.starts_with("did:x509:0:sha384:")); + // SHA-384 produces 48 bytes = 64 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 64); +} + +#[test] +fn test_build_with_sha512() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let did = DidX509Builder::build(&ca_cert, &[policy], HASH_ALGORITHM_SHA512).unwrap(); + + assert!(did.starts_with("did:x509:0:sha512:")); + // SHA-512 produces 64 bytes = 86 base64url chars (without padding) + let parts: Vec<&str> = did.split("::").collect(); + let fingerprint_part = parts[0].split(':').last().unwrap(); + assert_eq!(fingerprint_part.len(), 86); +} + +#[test] +fn test_build_with_invalid_hash_algorithm() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let result = DidX509Builder::build(&ca_cert, &[policy], "sha1"); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + DidX509Error::UnsupportedHashAlgorithm("sha1".to_string()) + ); +} + +#[test] +fn test_build_from_chain() { + let leaf_cert = create_test_leaf_cert_with_eku(); + let ca_cert = create_test_cert_der(); + let chain: Vec<&[u8]> = vec![&leaf_cert, &ca_cert]; + + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); + + // Should use the last cert (CA) for fingerprint + assert!(did.starts_with("did:x509:0:sha256:")); + assert!(did.contains("::eku:1.2.3.4")); +} + +#[test] +fn test_build_from_chain_empty() { + let chain: Vec<&[u8]> = vec![]; + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + + let result = DidX509Builder::build_from_chain(&chain, &[policy]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + DidX509Error::InvalidChain("Empty chain".to_string()) + ); +} + +#[test] +fn test_build_from_chain_single_cert() { + let ca_cert = create_test_cert_der(); + let chain: Vec<&[u8]> = vec![&ca_cert]; + + let policy = DidX509Policy::Eku(vec!["1.2.3.4".to_string().into()]); + let did = DidX509Builder::build_from_chain(&chain, &[policy]).unwrap(); + + assert!(did.starts_with("did:x509:0:sha256:")); +} + +#[test] +fn test_roundtrip_build_and_parse() { + let ca_cert = create_test_cert_der(); + let policies = vec![ + DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.2".to_string().into()]), + DidX509Policy::Subject(vec![ + ("CN".to_string(), "test.example.com".to_string()), + ("O".to_string(), "Test Org".to_string()), + ]), + DidX509Policy::San(SanType::Dns, "example.com".to_string()), + ]; + + let did = DidX509Builder::build_sha256(&ca_cert, &policies).unwrap(); + + // Parse the built DID + let parsed = DidX509Parser::parse(&did).unwrap(); + + // Verify structure + assert_eq!(parsed.hash_algorithm, HASH_ALGORITHM_SHA256); + assert_eq!(parsed.policies.len(), 3); + + // Verify EKU policy + if let DidX509Policy::Eku(oids) = &parsed.policies[0] { + assert_eq!(oids, &vec!["1.3.6.1.5.5.7.3.2".to_string()]); + } else { + panic!("Expected EKU policy"); + } + + // Verify Subject policy + if let DidX509Policy::Subject(attrs) = &parsed.policies[1] { + assert_eq!(attrs.len(), 2); + assert_eq!(attrs[0], ("CN".to_string(), "test.example.com".to_string())); + assert_eq!(attrs[1], ("O".to_string(), "Test Org".to_string())); + } else { + panic!("Expected Subject policy"); + } + + // Verify SAN policy + if let DidX509Policy::San(san_type, value) = &parsed.policies[2] { + assert_eq!(*san_type, SanType::Dns); + assert_eq!(value, "example.com"); + } else { + panic!("Expected SAN policy"); + } +} + +#[test] +fn test_encode_policy_with_special_characters() { + let ca_cert = create_test_cert_der(); + let policy = DidX509Policy::Subject(vec![( + "CN".to_string(), + "Test: Value, With Special/Chars".to_string(), + )]); + + let did = DidX509Builder::build_sha256(&ca_cert, &[policy]).unwrap(); + + // Special characters should be percent-encoded + assert!(did.contains("%3A")); // colon + assert!(did.contains("%2C")); // comma + assert!(did.contains("%2F")); // slash +} diff --git a/native/rust/did/x509/tests/did_document_tests.rs b/native/rust/did/x509/tests/did_document_tests.rs index 52c3c3ec..11082f1f 100644 --- a/native/rust/did/x509/tests/did_document_tests.rs +++ b/native/rust/did/x509/tests/did_document_tests.rs @@ -1,86 +1,86 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use did_x509::{DidDocument, VerificationMethod}; -use std::collections::HashMap; -use std::borrow::Cow; - -#[test] -fn test_did_document_to_json() { - let mut jwk = HashMap::new(); - jwk.insert(Cow::Borrowed("kty"), "RSA".to_string()); - jwk.insert(Cow::Borrowed("n"), "test".to_string()); - jwk.insert(Cow::Borrowed("e"), "AQAB".to_string()); - - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - public_key_jwk: jwk, - }], - assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], - }; - - let json = doc.to_json(false).unwrap(); - assert!(json.contains("@context")); - assert!(json.contains("did:x509:0:sha256:test::eku:1.2.3")); - assert!(json.contains("verificationMethod")); - assert!(json.contains("assertionMethod")); -} - -#[test] -fn test_did_document_to_json_indented() { - let mut jwk = HashMap::new(); - jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); - - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), - public_key_jwk: jwk, - }], - assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], - }; - - // Test indented output - let json_indented = doc.to_json(true).unwrap(); - assert!(json_indented.contains('\n')); // Should have newlines - assert!(json_indented.contains("@context")); -} - -#[test] -fn test_did_document_clone_partial_eq() { - let mut jwk = HashMap::new(); - jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); - - let doc1 = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), - verification_method: vec![VerificationMethod { - id: "did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string(), - type_: "JsonWebKey2020".to_string(), - controller: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), - public_key_jwk: jwk.clone(), - }], - assertion_method: vec!["did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string()], - }; - - // Clone and test equality - let doc2 = doc1.clone(); - assert_eq!(doc1, doc2); - - // Test inequality with different doc - let doc3 = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string()], - id: "did:x509:0:sha256:test2::eku:1.2.3".to_string(), - verification_method: vec![], - assertion_method: vec![], - }; - assert_ne!(doc1, doc3); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use did_x509::{DidDocument, VerificationMethod}; +use std::borrow::Cow; +use std::collections::HashMap; + +#[test] +fn test_did_document_to_json() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "RSA".to_string()); + jwk.insert(Cow::Borrowed("n"), "test".to_string()); + jwk.insert(Cow::Borrowed("e"), "AQAB".to_string()); + + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + public_key_jwk: jwk, + }], + assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], + }; + + let json = doc.to_json(false).unwrap(); + assert!(json.contains("@context")); + assert!(json.contains("did:x509:0:sha256:test::eku:1.2.3")); + assert!(json.contains("verificationMethod")); + assert!(json.contains("assertionMethod")); +} + +#[test] +fn test_did_document_to_json_indented() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); + + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test::eku:1.2.3".to_string(), + public_key_jwk: jwk, + }], + assertion_method: vec!["did:x509:0:sha256:test::eku:1.2.3#key-1".to_string()], + }; + + // Test indented output + let json_indented = doc.to_json(true).unwrap(); + assert!(json_indented.contains('\n')); // Should have newlines + assert!(json_indented.contains("@context")); +} + +#[test] +fn test_did_document_clone_partial_eq() { + let mut jwk = HashMap::new(); + jwk.insert(Cow::Borrowed("kty"), "EC".to_string()); + + let doc1 = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), + verification_method: vec![VerificationMethod { + id: "did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string(), + type_: "JsonWebKey2020".to_string(), + controller: "did:x509:0:sha256:test1::eku:1.2.3".to_string(), + public_key_jwk: jwk.clone(), + }], + assertion_method: vec!["did:x509:0:sha256:test1::eku:1.2.3#key-1".to_string()], + }; + + // Clone and test equality + let doc2 = doc1.clone(); + assert_eq!(doc1, doc2); + + // Test inequality with different doc + let doc3 = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string()], + id: "did:x509:0:sha256:test2::eku:1.2.3".to_string(), + verification_method: vec![], + assertion_method: vec![], + }; + assert_ne!(doc1, doc3); +} diff --git a/native/rust/did/x509/tests/policy_validator_tests.rs b/native/rust/did/x509/tests/policy_validator_tests.rs index 799bc66c..05e15058 100644 --- a/native/rust/did/x509/tests/policy_validator_tests.rs +++ b/native/rust/did/x509/tests/policy_validator_tests.rs @@ -1,375 +1,398 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive tests for policy validators with real X.509 certificates. -//! -//! Tests the policy_validators.rs functions with actual certificate generation -//! to ensure proper validation behavior for various policy types. - -use did_x509::error::DidX509Error; -use did_x509::models::SanType; -use did_x509::policy_validators::{ - validate_eku, validate_fulcio_issuer, validate_san, validate_subject, -}; -use rcgen::string::Ia5String; -use rcgen::ExtendedKeyUsagePurpose; -use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; -use x509_parser::prelude::*; - -/// Helper to generate a certificate with specific EKU OIDs. -fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test EKU Certificate"); - - if !eku_purposes.is_empty() { - params.extended_key_usages = eku_purposes; - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific subject attributes. -fn generate_cert_with_subject(attributes: Vec<(DnType, String)>) -> Vec { - let mut params = CertificateParams::default(); - - for (dn_type, value) in attributes { - params.distinguished_name.push(dn_type, value); - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific SAN entries. -fn generate_cert_with_san(san_entries: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test SAN Certificate"); - params.subject_alt_names = san_entries; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_eku_success_single_oid() { - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_eku_success_multiple_oids() { - let cert_der = generate_cert_with_eku(vec![ - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::ClientAuth, - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing - "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth - ], - ); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_eku_failure_missing_extension() { - let cert_der = generate_cert_with_eku(vec![]); // No EKU extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Extended Key Usage extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_eku_failure_wrong_oid() { - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); // Expect Code Signing - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required EKU OID '1.3.6.1.5.5.7.3.3' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_success_single_attribute() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("CN".to_string().into(), "Test Subject".to_string().into())]); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_subject_success_multiple_attributes() { - let cert_der = generate_cert_with_subject(vec![ - (DnType::CommonName, "Test Subject".to_string().into()), - (DnType::OrganizationName, "Test Org".to_string().into()), - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject( - &cert, - &[ - ("CN".to_string().into(), "Test Subject".to_string().into()), - ("O".to_string().into(), "Test Org".to_string().into()), - ], - ); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_subject_failure_empty_attributes() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Must contain at least one attribute")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_attribute_not_found() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("O".to_string().into(), "Missing Org".to_string().into())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required attribute 'O' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_attribute_value_mismatch() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("CN".to_string().into(), "Wrong Subject".to_string().into())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("value mismatch")); - assert!(msg.contains("expected 'Wrong Subject', got 'Test Subject'")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_subject_failure_unknown_attribute() { - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Test Subject".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_subject(&cert, &[("UNKNOWN".to_string().into(), "value".to_string().into())]); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Unknown attribute 'UNKNOWN'")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_success_dns() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( - Ia5String::try_from("example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_success_email() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Email, "test@example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_success_uri() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::URI( - Ia5String::try_from("https://example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Uri, "https://example.com"); - assert!(result.is_ok()); -} - -#[test] -fn test_validate_san_failure_no_extension() { - let cert_der = generate_cert_with_san(vec![]); // No SAN extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Subject Alternative Names")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_failure_wrong_value() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( - Ia5String::try_from("wrong.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required SAN 'dns:example.com' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_san_failure_wrong_type() { - let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( - Ia5String::try_from("test@example.com").unwrap(), - )]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "test@example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("Required SAN 'dns:test@example.com' not found")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_fulcio_issuer_success() { - // Generate a basic certificate - Fulcio issuer extension testing would - // require more complex certificate generation with custom extensions - let cert_der = - generate_cert_with_subject(vec![(DnType::CommonName, "Fulcio Test".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // This test will fail since the certificate doesn't have Fulcio extension - let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Fulcio issuer extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_validate_fulcio_issuer_failure_missing_extension() { - let cert_der = generate_cert_with_subject(vec![(DnType::CommonName, "Test Cert".to_string().into())]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); - assert!(result.is_err()); - match result { - Err(DidX509Error::PolicyValidationFailed(msg)) => { - assert!(msg.contains("no Fulcio issuer extension")); - } - _ => panic!("Expected PolicyValidationFailed error"), - } -} - -#[test] -fn test_error_display_coverage() { - // Test additional error paths to improve coverage - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test with multiple missing EKU OIDs - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing - "1.3.6.1.5.5.7.3.4".to_string().into(), // Email Protection - ], - ); - assert!(result.is_err()); - - // Test subject validation with duplicate checks - let result2 = validate_subject( - &cert, - &[ - ("CN".to_string().into(), "Test".to_string().into()), - ("O".to_string().into(), "Missing".to_string().into()), - ], - ); - assert!(result2.is_err()); -} - -#[test] -fn test_policy_validation_edge_cases() { - let cert_der = generate_cert_with_subject(vec![ - (DnType::CommonName, "Edge Case Test".to_string().into()), - (DnType::OrganizationName, "Test Corp".to_string().into()), - (DnType::CountryName, "US".to_string().into()), - ]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test with less common DN attributes - let result = validate_subject(&cert, &[("C".to_string().into(), "US".to_string().into())]); - assert!(result.is_ok()); - - // Test with case sensitivity - let result2 = validate_subject( - &cert, - &[ - ("CN".to_string().into(), "edge case test".to_string().into()), // Different case - ], - ); - assert!(result2.is_err()); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive tests for policy validators with real X.509 certificates. +//! +//! Tests the policy_validators.rs functions with actual certificate generation +//! to ensure proper validation behavior for various policy types. + +use did_x509::error::DidX509Error; +use did_x509::models::SanType; +use did_x509::policy_validators::{ + validate_eku, validate_fulcio_issuer, validate_san, validate_subject, +}; +use rcgen::string::Ia5String; +use rcgen::ExtendedKeyUsagePurpose; +use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; +use x509_parser::prelude::*; + +/// Helper to generate a certificate with specific EKU OIDs. +fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test EKU Certificate"); + + if !eku_purposes.is_empty() { + params.extended_key_usages = eku_purposes; + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific subject attributes. +fn generate_cert_with_subject(attributes: Vec<(DnType, String)>) -> Vec { + let mut params = CertificateParams::default(); + + for (dn_type, value) in attributes { + params.distinguished_name.push(dn_type, value); + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific SAN entries. +fn generate_cert_with_san(san_entries: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test SAN Certificate"); + params.subject_alt_names = san_entries; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_eku_success_single_oid() { + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_eku_success_multiple_oids() { + let cert_der = generate_cert_with_eku(vec![ + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::ClientAuth, + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing + "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth + ], + ); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_eku_failure_missing_extension() { + let cert_der = generate_cert_with_eku(vec![]); // No EKU extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Extended Key Usage extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_eku_failure_wrong_oid() { + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); // Expect Code Signing + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required EKU OID '1.3.6.1.5.5.7.3.3' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_success_single_attribute() { + let cert_der = generate_cert_with_subject(vec![( + DnType::CommonName, + "Test Subject".to_string().into(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[("CN".to_string().into(), "Test Subject".to_string().into())], + ); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_subject_success_multiple_attributes() { + let cert_der = generate_cert_with_subject(vec![ + (DnType::CommonName, "Test Subject".to_string().into()), + (DnType::OrganizationName, "Test Org".to_string().into()), + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test Subject".to_string().into()), + ("O".to_string().into(), "Test Org".to_string().into()), + ], + ); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_subject_failure_empty_attributes() { + let cert_der = generate_cert_with_subject(vec![( + DnType::CommonName, + "Test Subject".to_string().into(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject(&cert, &[]); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Must contain at least one attribute")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_attribute_not_found() { + let cert_der = generate_cert_with_subject(vec![( + DnType::CommonName, + "Test Subject".to_string().into(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[("O".to_string().into(), "Missing Org".to_string().into())], + ); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required attribute 'O' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_attribute_value_mismatch() { + let cert_der = generate_cert_with_subject(vec![( + DnType::CommonName, + "Test Subject".to_string().into(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[("CN".to_string().into(), "Wrong Subject".to_string().into())], + ); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("value mismatch")); + assert!(msg.contains("expected 'Wrong Subject', got 'Test Subject'")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_subject_failure_unknown_attribute() { + let cert_der = generate_cert_with_subject(vec![( + DnType::CommonName, + "Test Subject".to_string().into(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_subject( + &cert, + &[("UNKNOWN".to_string().into(), "value".to_string().into())], + ); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Unknown attribute 'UNKNOWN'")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_success_dns() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( + Ia5String::try_from("example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_success_email() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Email, "test@example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_success_uri() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::URI( + Ia5String::try_from("https://example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Uri, "https://example.com"); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_san_failure_no_extension() { + let cert_der = generate_cert_with_san(vec![]); // No SAN extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Subject Alternative Names")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_failure_wrong_value() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::DnsName( + Ia5String::try_from("wrong.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required SAN 'dns:example.com' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_san_failure_wrong_type() { + let cert_der = generate_cert_with_san(vec![RcgenSanType::Rfc822Name( + Ia5String::try_from("test@example.com").unwrap(), + )]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "test@example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("Required SAN 'dns:test@example.com' not found")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_fulcio_issuer_success() { + // Generate a basic certificate - Fulcio issuer extension testing would + // require more complex certificate generation with custom extensions + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Fulcio Test".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // This test will fail since the certificate doesn't have Fulcio extension + let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Fulcio issuer extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_validate_fulcio_issuer_failure_missing_extension() { + let cert_der = + generate_cert_with_subject(vec![(DnType::CommonName, "Test Cert".to_string().into())]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_fulcio_issuer(&cert, "https://fulcio.example.com"); + assert!(result.is_err()); + match result { + Err(DidX509Error::PolicyValidationFailed(msg)) => { + assert!(msg.contains("no Fulcio issuer extension")); + } + _ => panic!("Expected PolicyValidationFailed error"), + } +} + +#[test] +fn test_error_display_coverage() { + // Test additional error paths to improve coverage + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::ServerAuth]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test with multiple missing EKU OIDs + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing + "1.3.6.1.5.5.7.3.4".to_string().into(), // Email Protection + ], + ); + assert!(result.is_err()); + + // Test subject validation with duplicate checks + let result2 = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test".to_string().into()), + ("O".to_string().into(), "Missing".to_string().into()), + ], + ); + assert!(result2.is_err()); +} + +#[test] +fn test_policy_validation_edge_cases() { + let cert_der = generate_cert_with_subject(vec![ + (DnType::CommonName, "Edge Case Test".to_string().into()), + (DnType::OrganizationName, "Test Corp".to_string().into()), + (DnType::CountryName, "US".to_string().into()), + ]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test with less common DN attributes + let result = validate_subject(&cert, &[("C".to_string().into(), "US".to_string().into())]); + assert!(result.is_ok()); + + // Test with case sensitivity + let result2 = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "edge case test".to_string().into()), // Different case + ], + ); + assert!(result2.is_err()); +} diff --git a/native/rust/did/x509/tests/policy_validators_coverage.rs b/native/rust/did/x509/tests/policy_validators_coverage.rs index 6c5b5d8a..4a43cf5c 100644 --- a/native/rust/did/x509/tests/policy_validators_coverage.rs +++ b/native/rust/did/x509/tests/policy_validators_coverage.rs @@ -1,369 +1,375 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Additional coverage tests for policy validators to cover uncovered lines in policy_validators.rs. -//! -//! These tests target specific edge cases and error paths not covered by existing tests. - -use did_x509::error::DidX509Error; -use did_x509::models::SanType; -use did_x509::policy_validators::{ - validate_eku, validate_fulcio_issuer, validate_san, validate_subject, -}; -use rcgen::string::Ia5String; -use rcgen::ExtendedKeyUsagePurpose; -use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; -use x509_parser::prelude::*; - -/// Helper to generate a certificate with no EKU extension. -fn generate_cert_without_eku() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test No EKU Certificate"); - // Explicitly don't add extended_key_usages - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific subject attributes, including parsing edge cases. -fn generate_cert_with_subject_edge_cases() -> Vec { - let mut params = CertificateParams::default(); - // Add multiple types of subject attributes to test parsing - params - .distinguished_name - .push(DnType::CommonName, "Test Subject"); - params - .distinguished_name - .push(DnType::OrganizationName, "Test Org"); - params - .distinguished_name - .push(DnType::OrganizationalUnitName, "Test Unit"); - params.distinguished_name.push(DnType::CountryName, "US"); - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with no SAN extension. -fn generate_cert_without_san() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test No SAN Certificate"); - // Explicitly don't add subject_alt_names - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -/// Helper to generate a certificate with specific SAN entries for edge case testing. -fn generate_cert_with_multiple_sans() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Multi SAN Certificate"); - - // Add multiple types of SANs - params.subject_alt_names = vec![ - RcgenSanType::DnsName(Ia5String::try_from("test1.example.com").unwrap()), - RcgenSanType::DnsName(Ia5String::try_from("test2.example.com").unwrap()), - RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), - RcgenSanType::IpAddress("192.168.1.1".parse().unwrap()), - ]; - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_eku_no_extension() { - let cert_der = generate_cert_without_eku(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); - - // Should fail because certificate has no EKU extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("no Extended Key Usage"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_eku_missing_required_oid() { - // Generate cert with only code signing, but require both code signing and client auth - let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_eku( - &cert, - &[ - "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing (present) - "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth (missing) - ], - ); - - // Should fail because Client Auth EKU is missing - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("1.3.6.1.5.5.7.3.2") && msg.contains("not found"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -/// Helper to generate a certificate with specific EKU OIDs. -fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test EKU Certificate"); - - if !eku_purposes.is_empty() { - params.extended_key_usages = eku_purposes; - } - - let key = KeyPair::generate().unwrap(); - let cert = params.self_signed(&key).unwrap(); - cert.der().to_vec() -} - -#[test] -fn test_validate_subject_empty_attributes() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Empty expected attributes should fail - let result = validate_subject(&cert, &[]); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("at least one attribute"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_unknown_attribute() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Use an unknown attribute label - let result = validate_subject( - &cert, - &[("UnknownAttribute".to_string().into(), "SomeValue".to_string().into())], - ); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("Unknown attribute") && msg.contains("UnknownAttribute"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_missing_attribute() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request an attribute that doesn't exist in the certificate - let result = validate_subject( - &cert, - &[ - ("L".to_string().into(), "NonExistent".to_string().into()), // Locality - ], - ); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("L"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_value_mismatch() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request CommonName with wrong value - let result = validate_subject(&cert, &[("CN".to_string().into(), "Wrong Name".to_string().into())]); - - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("value mismatch") && msg.contains("CN"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_subject_success_multiple_attributes() { - let cert_der = generate_cert_with_subject_edge_cases(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Request multiple attributes that exist with correct values - let result = validate_subject( - &cert, - &[ - ("CN".to_string().into(), "Test Subject".to_string().into()), - ("O".to_string().into(), "Test Org".to_string().into()), - ("C".to_string().into(), "US".to_string().into()), - ], - ); - - assert!( - result.is_ok(), - "Multiple attribute validation should succeed" - ); -} - -#[test] -fn test_validate_san_no_extension() { - let cert_der = generate_cert_without_san(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "test.example.com"); - - // Should fail because certificate has no SAN extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("no Subject Alternative Names"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_not_found() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_san(&cert, &SanType::Dns, "nonexistent.example.com"); - - // Should fail because requested SAN doesn't exist - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("nonexistent.example.com"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_wrong_type() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Look for "test1.example.com" as an email instead of DNS name - let result = validate_san(&cert, &SanType::Email, "test1.example.com"); - - // Should fail because type doesn't match (it's a DNS name, not email) - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!( - msg.contains("not found") && msg.contains("email"), - "Error: {}", - msg - ); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -#[test] -fn test_validate_san_success_multiple_types() { - let cert_der = generate_cert_with_multiple_sans(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // Test each SAN type we added - assert!(validate_san(&cert, &SanType::Dns, "test1.example.com").is_ok()); - assert!(validate_san(&cert, &SanType::Dns, "test2.example.com").is_ok()); - assert!(validate_san(&cert, &SanType::Email, "test@example.com").is_ok()); -} - -#[test] -fn test_validate_fulcio_issuer_no_extension() { - let cert_der = generate_cert_without_san(); // Regular cert without Fulcio extension - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = validate_fulcio_issuer(&cert, "github.com"); - - // Should fail because certificate has no Fulcio issuer extension - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(msg) => { - assert!(msg.contains("no Fulcio issuer extension"), "Error: {}", msg); - } - _ => panic!("Expected PolicyValidationFailed"), - } -} - -// Note: Testing successful Fulcio validation is difficult without creating certificates -// with the specific Fulcio extension, which would require more complex certificate creation. -// The main coverage goal is to test the error paths which we've done above. - -#[test] -fn test_validate_fulcio_issuer_url_normalization() { - // This test would ideally check the URL normalization logic in validate_fulcio_issuer, - // but since we can't easily create certificates with Fulcio extensions using rcgen, - // we've focused on the error path testing above. - - // The URL normalization logic (adding https:// prefix) is covered when the extension - // exists but doesn't match, which we can't easily test without the extension. - - // Test case showing the expected behavior: - // If we had a cert with Fulcio issuer "https://github.com" and expected "github.com", - // it should normalize to "https://github.com" and match. - - let cert_der = generate_cert_without_san(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - // This will fail with "no extension" but shows the expected interface - let result = validate_fulcio_issuer(&cert, "github.com"); - assert!(result.is_err()); // Expected due to no extension -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Additional coverage tests for policy validators to cover uncovered lines in policy_validators.rs. +//! +//! These tests target specific edge cases and error paths not covered by existing tests. + +use did_x509::error::DidX509Error; +use did_x509::models::SanType; +use did_x509::policy_validators::{ + validate_eku, validate_fulcio_issuer, validate_san, validate_subject, +}; +use rcgen::string::Ia5String; +use rcgen::ExtendedKeyUsagePurpose; +use rcgen::{CertificateParams, DnType, KeyPair, SanType as RcgenSanType}; +use x509_parser::prelude::*; + +/// Helper to generate a certificate with no EKU extension. +fn generate_cert_without_eku() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test No EKU Certificate"); + // Explicitly don't add extended_key_usages + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific subject attributes, including parsing edge cases. +fn generate_cert_with_subject_edge_cases() -> Vec { + let mut params = CertificateParams::default(); + // Add multiple types of subject attributes to test parsing + params + .distinguished_name + .push(DnType::CommonName, "Test Subject"); + params + .distinguished_name + .push(DnType::OrganizationName, "Test Org"); + params + .distinguished_name + .push(DnType::OrganizationalUnitName, "Test Unit"); + params.distinguished_name.push(DnType::CountryName, "US"); + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with no SAN extension. +fn generate_cert_without_san() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test No SAN Certificate"); + // Explicitly don't add subject_alt_names + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +/// Helper to generate a certificate with specific SAN entries for edge case testing. +fn generate_cert_with_multiple_sans() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Multi SAN Certificate"); + + // Add multiple types of SANs + params.subject_alt_names = vec![ + RcgenSanType::DnsName(Ia5String::try_from("test1.example.com").unwrap()), + RcgenSanType::DnsName(Ia5String::try_from("test2.example.com").unwrap()), + RcgenSanType::Rfc822Name(Ia5String::try_from("test@example.com").unwrap()), + RcgenSanType::IpAddress("192.168.1.1".parse().unwrap()), + ]; + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_eku_no_extension() { + let cert_der = generate_cert_without_eku(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + + // Should fail because certificate has no EKU extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("no Extended Key Usage"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_eku_missing_required_oid() { + // Generate cert with only code signing, but require both code signing and client auth + let cert_der = generate_cert_with_eku(vec![ExtendedKeyUsagePurpose::CodeSigning]); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_eku( + &cert, + &[ + "1.3.6.1.5.5.7.3.3".to_string().into(), // Code Signing (present) + "1.3.6.1.5.5.7.3.2".to_string().into(), // Client Auth (missing) + ], + ); + + // Should fail because Client Auth EKU is missing + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("1.3.6.1.5.5.7.3.2") && msg.contains("not found"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +/// Helper to generate a certificate with specific EKU OIDs. +fn generate_cert_with_eku(eku_purposes: Vec) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test EKU Certificate"); + + if !eku_purposes.is_empty() { + params.extended_key_usages = eku_purposes; + } + + let key = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + cert.der().to_vec() +} + +#[test] +fn test_validate_subject_empty_attributes() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Empty expected attributes should fail + let result = validate_subject(&cert, &[]); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("at least one attribute"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_unknown_attribute() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Use an unknown attribute label + let result = validate_subject( + &cert, + &[( + "UnknownAttribute".to_string().into(), + "SomeValue".to_string().into(), + )], + ); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("Unknown attribute") && msg.contains("UnknownAttribute"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_missing_attribute() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request an attribute that doesn't exist in the certificate + let result = validate_subject( + &cert, + &[ + ("L".to_string().into(), "NonExistent".to_string().into()), // Locality + ], + ); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("L"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_value_mismatch() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request CommonName with wrong value + let result = validate_subject( + &cert, + &[("CN".to_string().into(), "Wrong Name".to_string().into())], + ); + + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("value mismatch") && msg.contains("CN"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_subject_success_multiple_attributes() { + let cert_der = generate_cert_with_subject_edge_cases(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Request multiple attributes that exist with correct values + let result = validate_subject( + &cert, + &[ + ("CN".to_string().into(), "Test Subject".to_string().into()), + ("O".to_string().into(), "Test Org".to_string().into()), + ("C".to_string().into(), "US".to_string().into()), + ], + ); + + assert!( + result.is_ok(), + "Multiple attribute validation should succeed" + ); +} + +#[test] +fn test_validate_san_no_extension() { + let cert_der = generate_cert_without_san(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "test.example.com"); + + // Should fail because certificate has no SAN extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("no Subject Alternative Names"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_not_found() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_san(&cert, &SanType::Dns, "nonexistent.example.com"); + + // Should fail because requested SAN doesn't exist + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("nonexistent.example.com"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_wrong_type() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Look for "test1.example.com" as an email instead of DNS name + let result = validate_san(&cert, &SanType::Email, "test1.example.com"); + + // Should fail because type doesn't match (it's a DNS name, not email) + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!( + msg.contains("not found") && msg.contains("email"), + "Error: {}", + msg + ); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +#[test] +fn test_validate_san_success_multiple_types() { + let cert_der = generate_cert_with_multiple_sans(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // Test each SAN type we added + assert!(validate_san(&cert, &SanType::Dns, "test1.example.com").is_ok()); + assert!(validate_san(&cert, &SanType::Dns, "test2.example.com").is_ok()); + assert!(validate_san(&cert, &SanType::Email, "test@example.com").is_ok()); +} + +#[test] +fn test_validate_fulcio_issuer_no_extension() { + let cert_der = generate_cert_without_san(); // Regular cert without Fulcio extension + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = validate_fulcio_issuer(&cert, "github.com"); + + // Should fail because certificate has no Fulcio issuer extension + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(msg) => { + assert!(msg.contains("no Fulcio issuer extension"), "Error: {}", msg); + } + _ => panic!("Expected PolicyValidationFailed"), + } +} + +// Note: Testing successful Fulcio validation is difficult without creating certificates +// with the specific Fulcio extension, which would require more complex certificate creation. +// The main coverage goal is to test the error paths which we've done above. + +#[test] +fn test_validate_fulcio_issuer_url_normalization() { + // This test would ideally check the URL normalization logic in validate_fulcio_issuer, + // but since we can't easily create certificates with Fulcio extensions using rcgen, + // we've focused on the error path testing above. + + // The URL normalization logic (adding https:// prefix) is covered when the extension + // exists but doesn't match, which we can't easily test without the extension. + + // Test case showing the expected behavior: + // If we had a cert with Fulcio issuer "https://github.com" and expected "github.com", + // it should normalize to "https://github.com" and match. + + let cert_der = generate_cert_without_san(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + // This will fail with "no extension" but shows the expected interface + let result = validate_fulcio_issuer(&cert, "github.com"); + assert!(result.is_err()); // Expected due to no extension +} diff --git a/native/rust/did/x509/tests/surgical_did_coverage.rs b/native/rust/did/x509/tests/surgical_did_coverage.rs index 98feb183..40355f8b 100644 --- a/native/rust/did/x509/tests/surgical_did_coverage.rs +++ b/native/rust/did/x509/tests/surgical_did_coverage.rs @@ -1,1395 +1,1403 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Surgical coverage tests for did_x509 crate — targets specific uncovered lines. -//! -//! Covers: -//! - resolver.rs: resolve(), public_key_to_jwk(), ec_to_jwk() error paths, rsa_to_jwk() -//! - policy_validators.rs: validate_subject mismatch paths, validate_san, validate_fulcio_issuer -//! - parser.rs: unknown policy type, malformed SAN, fulcio-issuer parsing, base64 edge cases -//! - x509_extensions.rs: custom EKU OIDs, is_ca_certificate, extract_fulcio_issuer -//! - san_parser.rs: DirectoryName SAN type -//! - validator.rs: validation with policy failures, empty chain -//! - builder.rs: build_from_chain_with_eku, encode_policy for SAN/FulcioIssuer/Subject -//! - did_document.rs: to_json non-indented - -use did_x509::builder::DidX509Builder; -use did_x509::did_document::DidDocument; -use did_x509::error::DidX509Error; -use did_x509::models::policy::{DidX509Policy, SanType}; -use did_x509::models::validation_result::DidX509ValidationResult; -use did_x509::parsing::DidX509Parser; -use did_x509::policy_validators; -use did_x509::resolver::DidX509Resolver; -use did_x509::validator::DidX509Validator; -use did_x509::x509_extensions; - -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::ec::{EcGroup, EcKey}; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, SubjectAlternativeName}; -use openssl::x509::{X509Builder, X509NameBuilder}; -use sha2::{Digest, Sha256}; -use std::borrow::Cow; - -// ============================================================================ -// Helpers: certificate generation via openssl -// ============================================================================ - -/// Build a self-signed EC (P-256) leaf certificate with code-signing EKU and a Subject CN. -fn build_ec_leaf_cert_with_cn(cn: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", cn).unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed RSA leaf certificate with code-signing EKU. -fn build_rsa_leaf_cert() -> Vec { - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "RSA Test Cert").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(2).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN DNS names. -fn build_ec_cert_with_san_dns(dns: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "SAN Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(3).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .dns(dns) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN email. -fn build_ec_cert_with_san_email(email: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Email Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(4).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .email(email) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with SAN URI. -fn build_ec_cert_with_san_uri(uri: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "URI Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(5).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - let san = SubjectAlternativeName::new() - .uri(uri) - .build(&builder.x509v3_context(None, None)) - .unwrap(); - builder.append_extension(san).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with BasicConstraints (CA:TRUE) and no EKU. -fn build_ca_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Test CA").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(10).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let bc = BasicConstraints::new().critical().ca().build().unwrap(); - builder.append_extension(bc).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with NO extensions at all. -fn build_bare_cert() -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", "Bare Test").unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(20).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Build a self-signed EC cert with Subject containing O and OU attributes. -fn build_ec_cert_with_subject(cn: &str, org: &str, ou: &str) -> Vec { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - let pkey = PKey::from_ec_key(ec_key).unwrap(); - - let mut name = X509NameBuilder::new().unwrap(); - name.append_entry_by_text("CN", cn).unwrap(); - name.append_entry_by_text("O", org).unwrap(); - name.append_entry_by_text("OU", ou).unwrap(); - let name = name.build(); - - let mut builder = X509Builder::new().unwrap(); - builder.set_version(2).unwrap(); - builder.set_subject_name(&name).unwrap(); - builder.set_issuer_name(&name).unwrap(); - builder.set_pubkey(&pkey).unwrap(); - builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - builder - .set_serial_number(&BigNum::from_u32(6).unwrap().to_asn1_integer().unwrap()) - .unwrap(); - - let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); - builder.append_extension(eku).unwrap(); - - builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - builder.build().to_der().unwrap() -} - -/// Helper: compute sha256 fingerprint, produce base64url-encoded string. -fn sha256_fingerprint_b64url(data: &[u8]) -> String { - let hash = Sha256::digest(data); - base64url_encode(&hash) -} - -fn base64url_encode(data: &[u8]) -> String { - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - let mut out = String::with_capacity((data.len() + 2) / 3 * 4); - let mut i = 0; - while i + 2 < data.len() { - let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8 | data[i + 2] as u32; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); - out.push(ALPHABET[(n & 0x3F) as usize] as char); - i += 3; - } - let rem = data.len() - i; - if rem == 1 { - let n = (data[i] as u32) << 16; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - } else if rem == 2 { - let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8; - out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); - } - out -} - -/// Helper: build a DID string manually for a self-signed cert with the given policies. -fn make_did(cert_der: &[u8], policy_suffix: &str) -> String { - let fp = sha256_fingerprint_b64url(cert_der); - format!("did:x509:0:sha256:{}::{}", fp, policy_suffix) -} - -// ============================================================================ -// resolver.rs — resolve() + public_key_to_jwk() + ec_to_jwk() + rsa_to_jwk() -// Lines 28-31, 81-86, 113-117, 143, 150, 157, 166-170, 191-201 -// ============================================================================ - -#[test] -fn resolver_ec_cert_produces_did_document() { - // Exercises resolve() happy path → lines 72-98 including 81-86 (JWK EC) - let cert = build_ec_leaf_cert_with_cn("Resolve EC Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_ok(), "EC resolve failed: {:?}", result.err()); - let doc = result.unwrap(); - assert_eq!(doc.id, did); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "EC"); - assert!(jwk.contains_key("x")); - assert!(jwk.contains_key("y")); - assert!(jwk.contains_key("crv")); -} - -#[test] -fn resolver_rsa_cert_produces_did_document() { - // Exercises rsa_to_jwk() → lines 121-134 (RSA JWK: kty, n, e) - let cert = build_rsa_leaf_cert(); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_ok(), "RSA resolve failed: {:?}", result.err()); - let doc = result.unwrap(); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "RSA"); - assert!(jwk.contains_key("n")); - assert!(jwk.contains_key("e")); -} - -#[test] -fn resolver_validation_fails_returns_error() { - // Exercises resolve() line 74-75: validation fails → PolicyValidationFailed - let cert = build_ec_leaf_cert_with_cn("Wrong EKU"); - // Use an EKU OID the cert doesn't have - let did = make_did(&cert, "eku:1.2.3.4.5.6.7.8.9"); - let result = DidX509Resolver::resolve(&did, &[&cert]); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::PolicyValidationFailed(_) => {} - other => panic!("Expected PolicyValidationFailed, got: {:?}", other), - } -} - -#[test] -fn resolver_invalid_der_returns_cert_parse_error() { - // Exercises resolve() lines 80-81: CertificateParseError path - // We need a DID that validates against a chain, but then the leaf parse fails. - // Actually this path requires validate() to succeed but from_der to fail, - // which is hard since validate also parses. Instead test with a DID that - // would resolve but parse fails at step 2. - // However, the real uncovered lines 80-81 are about the .map_err on from_der. - // Since validate() would fail first on bad DER, let's verify the error type - // from the validate step at least. - let bad_der = vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF]; - let did = make_did(&bad_der, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Resolver::resolve(&did, &[&bad_der]); - assert!(result.is_err()); -} - -// ============================================================================ -// policy_validators.rs — validate_eku, validate_subject, validate_san, validate_fulcio_issuer -// Lines 66, 88-93, 130-148 -// ============================================================================ - -#[test] -fn validate_eku_missing_required_oid() { - // Exercises validate_eku lines 22-27: required OID not present - let cert_der = build_ec_leaf_cert_with_cn("EKU Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_eku(&cert, &["9.9.9.9.9".to_string().into()]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("9.9.9.9.9")); -} - -#[test] -fn validate_eku_no_eku_extension() { - // Exercises validate_eku lines 15-18: no EKU extension at all - let cert_der = build_bare_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Extended Key Usage")); -} - -#[test] -fn validate_subject_matching() { - // Exercises validate_subject happy path and value comparison lines 56-71 - let cert_der = build_ec_cert_with_subject("TestCN", "TestOrg", "TestOU"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("CN".to_string().into(), "TestCN".to_string().into())]); - assert!(result.is_ok()); -} - -#[test] -fn validate_subject_value_mismatch() { - // Exercises validate_subject lines 80-86: attribute found but value doesn't match - let cert_der = build_ec_cert_with_subject("ActualCN", "ActualOrg", "ActualOU"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("CN".to_string().into(), "WrongCN".to_string().into())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("value mismatch")); -} - -#[test] -fn validate_subject_attribute_not_found() { - // Exercises validate_subject lines 74-77: attribute not in cert subject - let cert_der = build_ec_leaf_cert_with_cn("OnlyCN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("O".to_string().into(), "SomeOrg".to_string().into())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("not found")); -} - -#[test] -fn validate_subject_unknown_attribute_label() { - // Exercises validate_subject lines 47-50: unknown attribute label → error - let cert_der = build_ec_leaf_cert_with_cn("Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = - policy_validators::validate_subject(&cert, &[("BOGUS".to_string().into(), "value".to_string().into())]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Unknown attribute")); -} - -#[test] -fn validate_subject_empty_attrs() { - // Exercises validate_subject lines 35-38: empty attrs list - let cert_der = build_ec_leaf_cert_with_cn("Test"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_subject(&cert, &[]); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("at least one attribute")); -} - -#[test] -fn validate_san_dns_found() { - // Exercises validate_san lines 108-110: SAN found - let cert_der = build_ec_cert_with_san_dns("example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "example.com"); - assert!(result.is_ok()); -} - -#[test] -fn validate_san_not_found() { - // Exercises validate_san lines 112-117: SAN type+value not found - let cert_der = build_ec_cert_with_san_dns("example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "wrong.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("not found")); -} - -#[test] -fn validate_san_no_sans_at_all() { - // Exercises validate_san lines 101-105: cert has no SANs - let cert_der = build_ec_leaf_cert_with_cn("NoSAN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Dns, "any.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Subject Alternative Names")); -} - -#[test] -fn validate_san_email_type() { - // Exercises SAN email path in san_parser - let cert_der = build_ec_cert_with_san_email("user@example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Email, "user@example.com"); - assert!(result.is_ok()); -} - -#[test] -fn validate_san_uri_type() { - // Exercises SAN URI path in san_parser - let cert_der = build_ec_cert_with_san_uri("https://example.com/id"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_san(&cert, &SanType::Uri, "https://example.com/id"); - assert!(result.is_ok()); -} - -#[test] -fn validate_fulcio_issuer_no_extension() { - // Exercises validate_fulcio_issuer lines 126-130: no Fulcio issuer ext - let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let result = policy_validators::validate_fulcio_issuer(&cert, "accounts.google.com"); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no Fulcio issuer extension")); -} - -// ============================================================================ -// x509_extensions.rs — extract_extended_key_usage, is_ca_certificate, extract_fulcio_issuer -// Lines 24-27, 46, 58-60 -// ============================================================================ - -#[test] -fn extract_eku_returns_code_signing_oid() { - let cert_der = build_ec_leaf_cert_with_cn("EKU Extract"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let ekus = x509_extensions::extract_extended_key_usage(&cert); - assert!(ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3")); -} - -#[test] -fn extract_eku_empty_for_no_eku_cert() { - let cert_der = build_bare_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let ekus = x509_extensions::extract_extended_key_usage(&cert); - assert!(ekus.is_empty()); -} - -#[test] -fn is_ca_certificate_true_for_ca() { - // Exercises is_ca_certificate lines 42-49: BasicConstraints CA:TRUE → line 46 - let cert_der = build_ca_cert(); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(x509_extensions::is_ca_certificate(&cert)); -} - -#[test] -fn is_ca_certificate_false_for_leaf() { - let cert_der = build_ec_leaf_cert_with_cn("Leaf"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(!x509_extensions::is_ca_certificate(&cert)); -} - -#[test] -fn extract_fulcio_issuer_returns_none_when_absent() { - // Exercises extract_fulcio_issuer lines 53-63: no matching ext → None - let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - assert!(x509_extensions::extract_fulcio_issuer(&cert).is_none()); -} - -#[test] -fn extract_eku_oids_returns_oids() { - let cert_der = build_ec_leaf_cert_with_cn("EKU OIDs"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let oids = x509_extensions::extract_eku_oids(&cert).unwrap(); - assert!(!oids.is_empty()); -} - -// ============================================================================ -// validator.rs — validate() with policy failures, empty chain -// Lines 38-40, 67-68, 88-91 -// ============================================================================ - -#[test] -fn validator_empty_chain_returns_error() { - // Exercises validate() line 28-29: empty chain - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let chain: &[&[u8]] = &[]; - let result = DidX509Validator::validate(&did, chain); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidChain(msg) => assert!(msg.contains("Empty")), - other => panic!("Expected InvalidChain, got: {:?}", other), - } -} - -#[test] -fn validator_fingerprint_mismatch_returns_no_ca_match() { - // Exercises find_ca_by_fingerprint → NoCaMatch (line 73) - let cert = build_ec_leaf_cert_with_cn("Test"); - // Use a fingerprint from a different cert - let other_cert = build_ec_leaf_cert_with_cn("Other"); - let did = make_did(&other_cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::NoCaMatch => {} - other => panic!("Expected NoCaMatch, got: {:?}", other), - } -} - -#[test] -fn validator_policy_failure_produces_invalid_result() { - // Exercises validate() lines 42-53: policy validation fails → invalid result - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:9.9.9.9.9"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - let val_result = result.unwrap(); - assert!(!val_result.is_valid); - assert!(!val_result.errors.is_empty()); -} - -#[test] -fn validator_cert_parse_error_for_bad_der() { - // Exercises validate() lines 37-38: X509Certificate::from_der fails - // We need a chain where the first cert fails to parse but CA fingerprint matches. - // This is tricky: the fingerprint check iterates ALL certs including bad ones. - // Actually find_ca_by_fingerprint doesn't parse certs, just hashes DER bytes. - // So we can have a bad leaf + good CA in the chain. - let bad_leaf: Vec = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; // Not a valid cert but valid DER tag - let ca_cert = build_ec_leaf_cert_with_cn("CA for bad leaf"); - - // The DID fingerprint matches the CA cert (second in chain) - let did = make_did(&ca_cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Validator::validate(&did, &[&bad_leaf, &ca_cert]); - // Should fail at leaf cert parsing - assert!(result.is_err()); -} - -#[test] -fn validator_subject_policy_integration() { - // Exercises validate_policy Subject match arm → line 82-83 - let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); - let did = make_did(&cert, "subject:CN:MyCN"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_san_policy_integration() { - // Exercises validate_policy San match arm → lines 85-86 - let cert = build_ec_cert_with_san_dns("test.example.com"); - let did = make_did(&cert, "san:dns:test.example.com"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_san_policy_failure() { - // Exercises validate_policy San failure → errors collected - let cert = build_ec_cert_with_san_dns("test.example.com"); - let did = make_did(&cert, "san:dns:wrong.example.com"); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - let val_result = result.unwrap(); - assert!(!val_result.is_valid); -} - -#[test] -fn validator_unsupported_hash_algorithm() { - // Exercises find_ca_by_fingerprint line 67: unsupported hash - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let _did = format!("did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); - // This should work; now test with an algorithm that gets parsed but not supported - // We need to craft a DID with e.g. "sha999" but the parser won't accept it. - // So let's test the sha384 and sha512 paths through the validator. -} - -// ============================================================================ -// builder.rs — build_from_chain_with_eku, encode_policy for SAN/Subject/FulcioIssuer -// Lines 74-76, 114, 159-160 -// ============================================================================ - -#[test] -fn builder_encode_san_policy() { - // Exercises encode_policy SAN match arm → lines 154-161 - let cert = build_ec_cert_with_san_dns("example.com"); - let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string().into()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:dns:example.com")); -} - -#[test] -fn builder_encode_san_email_policy() { - let cert = build_ec_cert_with_san_email("user@example.com"); - let policy = DidX509Policy::San(SanType::Email, "user@example.com".to_string().into()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:email:")); -} - -#[test] -fn builder_encode_san_uri_policy() { - let cert = build_ec_cert_with_san_uri("https://example.com/id"); - let policy = DidX509Policy::San(SanType::Uri, "https://example.com/id".to_string().into()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:uri:")); -} - -#[test] -fn builder_encode_san_dn_policy() { - // Exercises SAN Dn match arm → line 159 - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::San(SanType::Dn, "CN=Test".to_string().into()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("san:dn:")); -} - -#[test] -fn builder_encode_fulcio_issuer_policy() { - // Exercises encode_policy FulcioIssuer match arm → lines 163-164 - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string().into()); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("fulcio-issuer:accounts.google.com")); -} - -#[test] -fn builder_encode_subject_policy() { - // Exercises encode_policy Subject match arm → lines 145-153 - let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); - let policy = DidX509Policy::Subject(vec![ - ("CN".to_string().into(), "MyCN".to_string().into()), - ("O".to_string().into(), "MyOrg".to_string().into()), - ]); - let did = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(did.is_ok()); - let did_str = did.unwrap(); - assert!(did_str.contains("subject:CN:MyCN:O:MyOrg")); -} - -#[test] -fn builder_build_from_chain_with_eku() { - // Exercises build_from_chain_with_eku → lines 103-121 - let cert = build_ec_leaf_cert_with_cn("Chain EKU"); - let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); - assert!(result.is_ok()); - let did_str = result.unwrap(); - assert!(did_str.contains("eku:")); -} - -#[test] -fn builder_build_from_chain_with_eku_empty_chain() { - // Exercises build_from_chain_with_eku line 106-108: empty chain - let chain: &[&[u8]] = &[]; - let result = DidX509Builder::build_from_chain_with_eku(chain); - assert!(result.is_err()); -} - -#[test] -fn builder_build_from_chain_with_eku_no_eku() { - // Exercises build_from_chain_with_eku lines 114-116: no EKU found - let cert = build_bare_cert(); - let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); - // This should return an error or empty EKU list - // extract_eku_oids returns Ok(empty_vec), then line 115 checks is_empty - assert!(result.is_err()); -} - -#[test] -fn builder_build_from_chain_empty() { - // Exercises build_from_chain line 94-96: empty chain - let chain: &[&[u8]] = &[]; - let result = DidX509Builder::build_from_chain(chain, &[]); - assert!(result.is_err()); -} - -#[test] -fn builder_unsupported_hash_algorithm() { - // Exercises compute_fingerprint line 128: unsupported hash - let cert = build_ec_leaf_cert_with_cn("Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let result = DidX509Builder::build(&cert, &[policy], "sha999"); - assert!(result.is_err()); -} - -#[test] -fn builder_sha384_hash() { - // Exercises compute_fingerprint sha384 path → line 126 - let cert = build_ec_leaf_cert_with_cn("SHA384 Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let result = DidX509Builder::build(&cert, &[policy], "sha384"); - assert!(result.is_ok()); -} - -#[test] -fn builder_sha512_hash() { - // Exercises compute_fingerprint sha512 path → line 127 - let cert = build_ec_leaf_cert_with_cn("SHA512 Test"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let result = DidX509Builder::build(&cert, &[policy], "sha512"); - assert!(result.is_ok()); -} - -// ============================================================================ -// did_document.rs — to_json() non-indented -// Line 59 -// ============================================================================ - -#[test] -fn did_document_to_json_non_indented() { - // Exercises to_json(false) → line 57 (serde_json::to_string) - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], - id: "did:x509:test".to_string().into(), - verification_method: vec![], - assertion_method: vec![], - }; - let json = doc.to_json(false); - assert!(json.is_ok()); - let json_str = json.unwrap(); - assert!(!json_str.contains('\n')); -} - -#[test] -fn did_document_to_json_indented() { - // Exercises to_json(true) → line 55 (serde_json::to_string_pretty) - let doc = DidDocument { - context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], - id: "did:x509:test".to_string().into(), - verification_method: vec![], - assertion_method: vec![], - }; - let json = doc.to_json(true); - assert!(json.is_ok()); - let json_str = json.unwrap(); - assert!(json_str.contains('\n')); -} - -// ============================================================================ -// parser.rs — edge cases -// Lines 35, 119, 127-129, 143, 166, 203-205, 224, 234, 259-260, 282, 286-287, 299 -// ============================================================================ - -#[test] -fn parser_unknown_policy_type() { - // Exercises parse_policy_value lines 199-204: unknown policy type - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::unknownpolicy:somevalue", fp); - let result = DidX509Parser::parse(&did); - // Unknown policy defaults to Eku([]) per line 203 - assert!(result.is_ok()); -} - -#[test] -fn parser_empty_fingerprint() { - // Exercises parser.rs line 118-119: empty fingerprint - let did = "did:x509:0:sha256:::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); -} - -#[test] -fn parser_wrong_fingerprint_length() { - // Exercises parser.rs lines 130-136: fingerprint length mismatch - let did = "did:x509:0:sha256:AAAA::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::FingerprintLengthMismatch(_, _, _) => {} - other => panic!("Expected FingerprintLengthMismatch, got: {:?}", other), - } -} - -#[test] -fn parser_invalid_base64url_chars() { - // Exercises parser.rs lines 138-139: invalid base64url characters - // SHA-256 fingerprint must be exactly 43 base64url chars - let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@::eku:1.2.3.4"; - let result = DidX509Parser::parse(did); - assert!(result.is_err()); -} - -#[test] -fn parser_unsupported_version() { - // Exercises parser.rs lines 102-107: unsupported version - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:9:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::UnsupportedVersion(_, _) => {} - other => panic!("Expected UnsupportedVersion, got: {:?}", other), - } -} - -#[test] -fn parser_unsupported_hash_algorithm() { - // Exercises parser.rs lines 110-114: unsupported hash algorithm - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:md5:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::UnsupportedHashAlgorithm(_) => {} - other => panic!("Expected UnsupportedHashAlgorithm, got: {:?}", other), - } -} - -#[test] -fn parser_empty_policy_segment() { - // Exercises parser.rs lines 149-151: empty policy at position - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}:: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_policy_no_colon() { - // Exercises parser.rs lines 155-158: policy without colon - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::nocolon", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidPolicyFormat(_) => {} - other => panic!("Expected InvalidPolicyFormat, got: {:?}", other), - } -} - -#[test] -fn parser_empty_policy_name() { - // Exercises parser.rs line 165-167: empty policy name (colon at start) - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}:::value", fp); - let result = DidX509Parser::parse(&did); - // This has :: followed by : → first splits on :: giving empty segment handled above - // or parsing of ":value" where colon_idx == 0 - assert!(result.is_err()); -} - -#[test] -fn parser_empty_policy_value() { - // Exercises parser.rs lines 169-171: empty policy value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::eku: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_san_policy_missing_value() { - // Exercises parse_san_policy lines 244-248: missing colon in SAN value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:dnsnocolon", fp); - let result = DidX509Parser::parse(&did); - // "dnsnocolon" has no colon → InvalidSanPolicyFormat - assert!(result.is_err()); -} - -#[test] -fn parser_san_policy_invalid_type() { - // Exercises parse_san_policy lines 255-256: invalid SAN type - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:badtype:value", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidSanType(_) => {} - other => panic!("Expected InvalidSanType, got: {:?}", other), - } -} - -#[test] -fn parser_eku_invalid_oid() { - // Exercises parse_eku_policy line 271: invalid OID format - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::eku:not-an-oid", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidEkuOid => {} - other => panic!("Expected InvalidEkuOid, got: {:?}", other), - } -} - -#[test] -fn parser_fulcio_issuer_empty() { - // Exercises parse_fulcio_issuer_policy lines 281-283: empty issuer - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::fulcio-issuer: ", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); -} - -#[test] -fn parser_fulcio_issuer_valid() { - // Exercises parse_fulcio_issuer_policy lines 286-288: happy path - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!( - "did:x509:0:sha256:{}::fulcio-issuer:accounts.google.com", - fp - ); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); - let parsed = result.unwrap(); - assert!(parsed.has_fulcio_issuer_policy()); -} - -#[test] -fn parser_subject_policy_odd_components() { - // Exercises parse_subject_policy line 213: odd number of components - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::subject:CN:val:extra", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidSubjectPolicyComponents => {} - other => panic!("Expected InvalidSubjectPolicyComponents, got: {:?}", other), - } -} - -#[test] -fn parser_subject_policy_empty_key() { - // Exercises parse_subject_policy line 224: empty key - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - // "subject::val" where first part splits into ["", "val"] - // Actually ":val" as the policy_value → splits on ':' → ["", "val"] - let did = format!("did:x509:0:sha256:{}::subject::val", fp); - let result = DidX509Parser::parse(&did); - // The :: in "subject::val" would be split as major_parts separator - // Let's use percent-encoding approach instead - // Actually "subject" followed by ":val" → policy_value is "val" which has 1 part → odd - assert!(result.is_err()); -} - -#[test] -fn parser_subject_policy_duplicate_key() { - // Exercises parse_subject_policy lines 228-230: duplicate key - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::subject:CN:val1:CN:val2", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::DuplicateSubjectPolicyKey(_) => {} - other => panic!("Expected DuplicateSubjectPolicyKey, got: {:?}", other), - } -} - -#[test] -fn parser_sha384_fingerprint() { - // Exercises parser sha384 path → line 124 expected_length = 64 - use sha2::Sha384; - let cert = build_ec_leaf_cert_with_cn("SHA384"); - let hash = Sha384::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_sha512_fingerprint() { - // Exercises parser sha512 path → line 125-126 expected_length = 86 - use sha2::Sha512; - let cert = build_ec_leaf_cert_with_cn("SHA512"); - let hash = Sha512::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_try_parse_returns_none_on_failure() { - let result = DidX509Parser::try_parse("not a valid DID"); - assert!(result.is_none()); -} - -#[test] -fn parser_try_parse_returns_some_on_success() { - let cert = build_ec_leaf_cert_with_cn("Test"); - let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); - let result = DidX509Parser::try_parse(&did); - assert!(result.is_some()); -} - -#[test] -fn parser_san_percent_encoded_value() { - // Exercises parse_san_policy line 259: percent_decode on SAN value - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}::san:email:user%40example.com", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_ok()); -} - -#[test] -fn parser_invalid_prefix() { - // Exercises parser.rs lines 77-79: wrong prefix - let result = DidX509Parser::parse("did:wrong:0:sha256:AAAA::eku:1.2.3"); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::InvalidPrefix(_) => {} - other => panic!("Expected InvalidPrefix, got: {:?}", other), - } -} - -#[test] -fn parser_missing_policies() { - // Exercises parser.rs lines 83-85: no :: separator - let cert = build_ec_leaf_cert_with_cn("Test"); - let fp = sha256_fingerprint_b64url(&cert); - let did = format!("did:x509:0:sha256:{}", fp); - let result = DidX509Parser::parse(&did); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::MissingPolicies => {} - other => panic!("Expected MissingPolicies, got: {:?}", other), - } -} - -#[test] -fn parser_wrong_component_count() { - // Exercises parser.rs lines 91-95: prefix has wrong number of components - let result = DidX509Parser::parse("did:x509:0:sha256::eku:1.2.3"); - assert!(result.is_err()); -} - -#[test] -fn parser_empty_did() { - let result = DidX509Parser::parse(""); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::EmptyDid => {} - other => panic!("Expected EmptyDid, got: {:?}", other), - } -} - -#[test] -fn parser_whitespace_only_did() { - let result = DidX509Parser::parse(" "); - assert!(result.is_err()); - match result.unwrap_err() { - DidX509Error::EmptyDid => {} - other => panic!("Expected EmptyDid, got: {:?}", other), - } -} - -// ============================================================================ -// san_parser.rs — edge cases for DirectoryName (lines 23-26) -// ============================================================================ - -#[test] -fn san_parser_parse_sans_from_cert_with_dns() { - let cert_der = build_ec_cert_with_san_dns("test.example.com"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); - assert!(!sans.is_empty()); - assert_eq!(sans[0].san_type, SanType::Dns); - assert_eq!(sans[0].value, "test.example.com"); -} - -#[test] -fn san_parser_parse_sans_from_cert_no_san() { - let cert_der = build_ec_leaf_cert_with_cn("No SAN"); - let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); - let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); - assert!(sans.is_empty()); -} - -// ============================================================================ -// Validation result model tests -// ============================================================================ - -#[test] -fn validation_result_add_error() { - let mut result = DidX509ValidationResult::valid(0); - assert!(result.is_valid); - result.add_error("test error".to_string().into()); - assert!(!result.is_valid); - assert_eq!(result.errors.len(), 1); -} - -#[test] -fn validation_result_invalid_single() { - let result = DidX509ValidationResult::invalid("single error".to_string().into()); - assert!(!result.is_valid); - assert!(result.matched_ca_index.is_none()); - assert_eq!(result.errors.len(), 1); -} - -// ============================================================================ -// Resolver with sha384 and sha512 hash algorithms via validator -// ============================================================================ - -#[test] -fn validator_sha384_fingerprint_matching() { - use sha2::Sha384; - let cert = build_ec_leaf_cert_with_cn("SHA384 Validator"); - let hash = Sha384::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -#[test] -fn validator_sha512_fingerprint_matching() { - use sha2::Sha512; - let cert = build_ec_leaf_cert_with_cn("SHA512 Validator"); - let hash = Sha512::digest(&cert); - let fp = base64url_encode(&hash); - let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); - let result = DidX509Validator::validate(&did, &[&cert]); - assert!(result.is_ok()); - assert!(result.unwrap().is_valid); -} - -// ============================================================================ -// Error Display coverage -// ============================================================================ - -#[test] -fn error_display_coverage() { - // Exercise Display for several error variants - let errors: Vec = vec![ - DidX509Error::EmptyDid, - DidX509Error::InvalidPrefix("test".to_string().into()), - DidX509Error::MissingPolicies, - DidX509Error::InvalidFormat("fmt".to_string().into()), - DidX509Error::UnsupportedVersion("1".to_string().into(), "0".to_string().into()), - DidX509Error::UnsupportedHashAlgorithm("md5".to_string().into()), - DidX509Error::EmptyFingerprint, - DidX509Error::FingerprintLengthMismatch("sha256".to_string().into(), 43, 10), - DidX509Error::InvalidFingerprintChars, - DidX509Error::EmptyPolicy(1), - DidX509Error::InvalidPolicyFormat("bad".to_string().into()), - DidX509Error::EmptyPolicyName, - DidX509Error::EmptyPolicyValue, - DidX509Error::InvalidSubjectPolicyComponents, - DidX509Error::EmptySubjectPolicyKey, - DidX509Error::DuplicateSubjectPolicyKey("CN".to_string().into()), - DidX509Error::InvalidSanPolicyFormat("bad".to_string().into()), - DidX509Error::InvalidSanType("bad".to_string().into()), - DidX509Error::InvalidEkuOid, - DidX509Error::EmptyFulcioIssuer, - DidX509Error::PercentDecodingError("bad".to_string().into()), - DidX509Error::InvalidHexCharacter('G'), - DidX509Error::InvalidChain("bad".to_string().into()), - DidX509Error::CertificateParseError("bad".to_string().into()), - DidX509Error::PolicyValidationFailed("bad".to_string().into()), - DidX509Error::NoCaMatch, - DidX509Error::ValidationFailed("bad".to_string().into()), - ]; - for err in &errors { - let msg = format!("{}", err); - assert!(!msg.is_empty()); - } -} - -// ============================================================================ -// base64url encoding edge cases in builder.rs (lines 26-37 of builder.rs) -// These are actually in the inline base64_encode function -// ============================================================================ - -#[test] -fn builder_build_sha256_shorthand() { - let cert = build_ec_leaf_cert_with_cn("Shorthand"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let result = DidX509Builder::build_sha256(&cert, &[policy]); - assert!(result.is_ok()); -} - -#[test] -fn builder_build_from_chain_last_cert_as_ca() { - // Exercises build_from_chain line 97-98: uses last cert as CA - let leaf = build_ec_leaf_cert_with_cn("Leaf"); - let ca = build_ca_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let result = DidX509Builder::build_from_chain(&[&leaf, &ca], &[policy]); - assert!(result.is_ok()); -} - -// ============================================================================ -// SanType::as_str() for all variants -// ============================================================================ - -#[test] -fn san_type_as_str_all_variants() { - assert_eq!(SanType::Email.as_str(), "email"); - assert_eq!(SanType::Dns.as_str(), "dns"); - assert_eq!(SanType::Uri.as_str(), "uri"); - assert_eq!(SanType::Dn.as_str(), "dn"); -} - -#[test] -fn san_type_from_str_all_variants() { - assert_eq!(SanType::from_str("email"), Some(SanType::Email)); - assert_eq!(SanType::from_str("dns"), Some(SanType::Dns)); - assert_eq!(SanType::from_str("uri"), Some(SanType::Uri)); - assert_eq!(SanType::from_str("dn"), Some(SanType::Dn)); - assert_eq!(SanType::from_str("bad"), None); -} - -// ============================================================================ -// Resolver round-trip: build DID then resolve to verify EC JWK -// ============================================================================ - -#[test] -fn resolver_roundtrip_build_then_resolve_ec() { - let cert = build_ec_leaf_cert_with_cn("Roundtrip EC"); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); - let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); - assert_eq!(doc.verification_method.len(), 1); - assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); -} - -#[test] -fn resolver_roundtrip_build_then_resolve_rsa() { - let cert = build_rsa_leaf_cert(); - let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); - let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); - let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); - assert_eq!(doc.verification_method.len(), 1); - let jwk = &doc.verification_method[0].public_key_jwk; - assert_eq!(jwk.get("kty").unwrap(), "RSA"); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Surgical coverage tests for did_x509 crate — targets specific uncovered lines. +//! +//! Covers: +//! - resolver.rs: resolve(), public_key_to_jwk(), ec_to_jwk() error paths, rsa_to_jwk() +//! - policy_validators.rs: validate_subject mismatch paths, validate_san, validate_fulcio_issuer +//! - parser.rs: unknown policy type, malformed SAN, fulcio-issuer parsing, base64 edge cases +//! - x509_extensions.rs: custom EKU OIDs, is_ca_certificate, extract_fulcio_issuer +//! - san_parser.rs: DirectoryName SAN type +//! - validator.rs: validation with policy failures, empty chain +//! - builder.rs: build_from_chain_with_eku, encode_policy for SAN/FulcioIssuer/Subject +//! - did_document.rs: to_json non-indented + +use did_x509::builder::DidX509Builder; +use did_x509::did_document::DidDocument; +use did_x509::error::DidX509Error; +use did_x509::models::policy::{DidX509Policy, SanType}; +use did_x509::models::validation_result::DidX509ValidationResult; +use did_x509::parsing::DidX509Parser; +use did_x509::policy_validators; +use did_x509::resolver::DidX509Resolver; +use did_x509::validator::DidX509Validator; +use did_x509::x509_extensions; + +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::ec::{EcGroup, EcKey}; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, SubjectAlternativeName}; +use openssl::x509::{X509Builder, X509NameBuilder}; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +// ============================================================================ +// Helpers: certificate generation via openssl +// ============================================================================ + +/// Build a self-signed EC (P-256) leaf certificate with code-signing EKU and a Subject CN. +fn build_ec_leaf_cert_with_cn(cn: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", cn).unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed RSA leaf certificate with code-signing EKU. +fn build_rsa_leaf_cert() -> Vec { + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "RSA Test Cert").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(2).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN DNS names. +fn build_ec_cert_with_san_dns(dns: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "SAN Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(3).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .dns(dns) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN email. +fn build_ec_cert_with_san_email(email: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Email Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(4).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .email(email) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with SAN URI. +fn build_ec_cert_with_san_uri(uri: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "URI Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(5).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + let san = SubjectAlternativeName::new() + .uri(uri) + .build(&builder.x509v3_context(None, None)) + .unwrap(); + builder.append_extension(san).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with BasicConstraints (CA:TRUE) and no EKU. +fn build_ca_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Test CA").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(10).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let bc = BasicConstraints::new().critical().ca().build().unwrap(); + builder.append_extension(bc).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with NO extensions at all. +fn build_bare_cert() -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "Bare Test").unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(20).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Build a self-signed EC cert with Subject containing O and OU attributes. +fn build_ec_cert_with_subject(cn: &str, org: &str, ou: &str) -> Vec { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", cn).unwrap(); + name.append_entry_by_text("O", org).unwrap(); + name.append_entry_by_text("OU", ou).unwrap(); + let name = name.build(); + + let mut builder = X509Builder::new().unwrap(); + builder.set_version(2).unwrap(); + builder.set_subject_name(&name).unwrap(); + builder.set_issuer_name(&name).unwrap(); + builder.set_pubkey(&pkey).unwrap(); + builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_not_after(&Asn1Time::days_from_now(365).unwrap()) + .unwrap(); + builder + .set_serial_number(&BigNum::from_u32(6).unwrap().to_asn1_integer().unwrap()) + .unwrap(); + + let eku = ExtendedKeyUsage::new().code_signing().build().unwrap(); + builder.append_extension(eku).unwrap(); + + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + builder.build().to_der().unwrap() +} + +/// Helper: compute sha256 fingerprint, produce base64url-encoded string. +fn sha256_fingerprint_b64url(data: &[u8]) -> String { + let hash = Sha256::digest(data); + base64url_encode(&hash) +} + +fn base64url_encode(data: &[u8]) -> String { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + let mut out = String::with_capacity((data.len() + 2) / 3 * 4); + let mut i = 0; + while i + 2 < data.len() { + let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8 | data[i + 2] as u32; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); + out.push(ALPHABET[(n & 0x3F) as usize] as char); + i += 3; + } + let rem = data.len() - i; + if rem == 1 { + let n = (data[i] as u32) << 16; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + } else if rem == 2 { + let n = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8; + out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char); + } + out +} + +/// Helper: build a DID string manually for a self-signed cert with the given policies. +fn make_did(cert_der: &[u8], policy_suffix: &str) -> String { + let fp = sha256_fingerprint_b64url(cert_der); + format!("did:x509:0:sha256:{}::{}", fp, policy_suffix) +} + +// ============================================================================ +// resolver.rs — resolve() + public_key_to_jwk() + ec_to_jwk() + rsa_to_jwk() +// Lines 28-31, 81-86, 113-117, 143, 150, 157, 166-170, 191-201 +// ============================================================================ + +#[test] +fn resolver_ec_cert_produces_did_document() { + // Exercises resolve() happy path → lines 72-98 including 81-86 (JWK EC) + let cert = build_ec_leaf_cert_with_cn("Resolve EC Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_ok(), "EC resolve failed: {:?}", result.err()); + let doc = result.unwrap(); + assert_eq!(doc.id, did); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert!(jwk.contains_key("x")); + assert!(jwk.contains_key("y")); + assert!(jwk.contains_key("crv")); +} + +#[test] +fn resolver_rsa_cert_produces_did_document() { + // Exercises rsa_to_jwk() → lines 121-134 (RSA JWK: kty, n, e) + let cert = build_rsa_leaf_cert(); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_ok(), "RSA resolve failed: {:?}", result.err()); + let doc = result.unwrap(); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "RSA"); + assert!(jwk.contains_key("n")); + assert!(jwk.contains_key("e")); +} + +#[test] +fn resolver_validation_fails_returns_error() { + // Exercises resolve() line 74-75: validation fails → PolicyValidationFailed + let cert = build_ec_leaf_cert_with_cn("Wrong EKU"); + // Use an EKU OID the cert doesn't have + let did = make_did(&cert, "eku:1.2.3.4.5.6.7.8.9"); + let result = DidX509Resolver::resolve(&did, &[&cert]); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::PolicyValidationFailed(_) => {} + other => panic!("Expected PolicyValidationFailed, got: {:?}", other), + } +} + +#[test] +fn resolver_invalid_der_returns_cert_parse_error() { + // Exercises resolve() lines 80-81: CertificateParseError path + // We need a DID that validates against a chain, but then the leaf parse fails. + // Actually this path requires validate() to succeed but from_der to fail, + // which is hard since validate also parses. Instead test with a DID that + // would resolve but parse fails at step 2. + // However, the real uncovered lines 80-81 are about the .map_err on from_der. + // Since validate() would fail first on bad DER, let's verify the error type + // from the validate step at least. + let bad_der = vec![0x30, 0x82, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF]; + let did = make_did(&bad_der, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Resolver::resolve(&did, &[&bad_der]); + assert!(result.is_err()); +} + +// ============================================================================ +// policy_validators.rs — validate_eku, validate_subject, validate_san, validate_fulcio_issuer +// Lines 66, 88-93, 130-148 +// ============================================================================ + +#[test] +fn validate_eku_missing_required_oid() { + // Exercises validate_eku lines 22-27: required OID not present + let cert_der = build_ec_leaf_cert_with_cn("EKU Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_eku(&cert, &["9.9.9.9.9".to_string().into()]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("9.9.9.9.9")); +} + +#[test] +fn validate_eku_no_eku_extension() { + // Exercises validate_eku lines 15-18: no EKU extension at all + let cert_der = build_bare_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_eku(&cert, &["1.3.6.1.5.5.7.3.3".to_string().into()]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Extended Key Usage")); +} + +#[test] +fn validate_subject_matching() { + // Exercises validate_subject happy path and value comparison lines 56-71 + let cert_der = build_ec_cert_with_subject("TestCN", "TestOrg", "TestOU"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject( + &cert, + &[("CN".to_string().into(), "TestCN".to_string().into())], + ); + assert!(result.is_ok()); +} + +#[test] +fn validate_subject_value_mismatch() { + // Exercises validate_subject lines 80-86: attribute found but value doesn't match + let cert_der = build_ec_cert_with_subject("ActualCN", "ActualOrg", "ActualOU"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject( + &cert, + &[("CN".to_string().into(), "WrongCN".to_string().into())], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("value mismatch")); +} + +#[test] +fn validate_subject_attribute_not_found() { + // Exercises validate_subject lines 74-77: attribute not in cert subject + let cert_der = build_ec_leaf_cert_with_cn("OnlyCN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject( + &cert, + &[("O".to_string().into(), "SomeOrg".to_string().into())], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("not found")); +} + +#[test] +fn validate_subject_unknown_attribute_label() { + // Exercises validate_subject lines 47-50: unknown attribute label → error + let cert_der = build_ec_leaf_cert_with_cn("Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject( + &cert, + &[("BOGUS".to_string().into(), "value".to_string().into())], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Unknown attribute")); +} + +#[test] +fn validate_subject_empty_attrs() { + // Exercises validate_subject lines 35-38: empty attrs list + let cert_der = build_ec_leaf_cert_with_cn("Test"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_subject(&cert, &[]); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("at least one attribute")); +} + +#[test] +fn validate_san_dns_found() { + // Exercises validate_san lines 108-110: SAN found + let cert_der = build_ec_cert_with_san_dns("example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "example.com"); + assert!(result.is_ok()); +} + +#[test] +fn validate_san_not_found() { + // Exercises validate_san lines 112-117: SAN type+value not found + let cert_der = build_ec_cert_with_san_dns("example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "wrong.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("not found")); +} + +#[test] +fn validate_san_no_sans_at_all() { + // Exercises validate_san lines 101-105: cert has no SANs + let cert_der = build_ec_leaf_cert_with_cn("NoSAN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Dns, "any.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Subject Alternative Names")); +} + +#[test] +fn validate_san_email_type() { + // Exercises SAN email path in san_parser + let cert_der = build_ec_cert_with_san_email("user@example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Email, "user@example.com"); + assert!(result.is_ok()); +} + +#[test] +fn validate_san_uri_type() { + // Exercises SAN URI path in san_parser + let cert_der = build_ec_cert_with_san_uri("https://example.com/id"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_san(&cert, &SanType::Uri, "https://example.com/id"); + assert!(result.is_ok()); +} + +#[test] +fn validate_fulcio_issuer_no_extension() { + // Exercises validate_fulcio_issuer lines 126-130: no Fulcio issuer ext + let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let result = policy_validators::validate_fulcio_issuer(&cert, "accounts.google.com"); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("no Fulcio issuer extension")); +} + +// ============================================================================ +// x509_extensions.rs — extract_extended_key_usage, is_ca_certificate, extract_fulcio_issuer +// Lines 24-27, 46, 58-60 +// ============================================================================ + +#[test] +fn extract_eku_returns_code_signing_oid() { + let cert_der = build_ec_leaf_cert_with_cn("EKU Extract"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let ekus = x509_extensions::extract_extended_key_usage(&cert); + assert!(ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3")); +} + +#[test] +fn extract_eku_empty_for_no_eku_cert() { + let cert_der = build_bare_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let ekus = x509_extensions::extract_extended_key_usage(&cert); + assert!(ekus.is_empty()); +} + +#[test] +fn is_ca_certificate_true_for_ca() { + // Exercises is_ca_certificate lines 42-49: BasicConstraints CA:TRUE → line 46 + let cert_der = build_ca_cert(); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(x509_extensions::is_ca_certificate(&cert)); +} + +#[test] +fn is_ca_certificate_false_for_leaf() { + let cert_der = build_ec_leaf_cert_with_cn("Leaf"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(!x509_extensions::is_ca_certificate(&cert)); +} + +#[test] +fn extract_fulcio_issuer_returns_none_when_absent() { + // Exercises extract_fulcio_issuer lines 53-63: no matching ext → None + let cert_der = build_ec_leaf_cert_with_cn("No Fulcio"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + assert!(x509_extensions::extract_fulcio_issuer(&cert).is_none()); +} + +#[test] +fn extract_eku_oids_returns_oids() { + let cert_der = build_ec_leaf_cert_with_cn("EKU OIDs"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let oids = x509_extensions::extract_eku_oids(&cert).unwrap(); + assert!(!oids.is_empty()); +} + +// ============================================================================ +// validator.rs — validate() with policy failures, empty chain +// Lines 38-40, 67-68, 88-91 +// ============================================================================ + +#[test] +fn validator_empty_chain_returns_error() { + // Exercises validate() line 28-29: empty chain + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let chain: &[&[u8]] = &[]; + let result = DidX509Validator::validate(&did, chain); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidChain(msg) => assert!(msg.contains("Empty")), + other => panic!("Expected InvalidChain, got: {:?}", other), + } +} + +#[test] +fn validator_fingerprint_mismatch_returns_no_ca_match() { + // Exercises find_ca_by_fingerprint → NoCaMatch (line 73) + let cert = build_ec_leaf_cert_with_cn("Test"); + // Use a fingerprint from a different cert + let other_cert = build_ec_leaf_cert_with_cn("Other"); + let did = make_did(&other_cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::NoCaMatch => {} + other => panic!("Expected NoCaMatch, got: {:?}", other), + } +} + +#[test] +fn validator_policy_failure_produces_invalid_result() { + // Exercises validate() lines 42-53: policy validation fails → invalid result + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:9.9.9.9.9"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + let val_result = result.unwrap(); + assert!(!val_result.is_valid); + assert!(!val_result.errors.is_empty()); +} + +#[test] +fn validator_cert_parse_error_for_bad_der() { + // Exercises validate() lines 37-38: X509Certificate::from_der fails + // We need a chain where the first cert fails to parse but CA fingerprint matches. + // This is tricky: the fingerprint check iterates ALL certs including bad ones. + // Actually find_ca_by_fingerprint doesn't parse certs, just hashes DER bytes. + // So we can have a bad leaf + good CA in the chain. + let bad_leaf: Vec = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; // Not a valid cert but valid DER tag + let ca_cert = build_ec_leaf_cert_with_cn("CA for bad leaf"); + + // The DID fingerprint matches the CA cert (second in chain) + let did = make_did(&ca_cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Validator::validate(&did, &[&bad_leaf, &ca_cert]); + // Should fail at leaf cert parsing + assert!(result.is_err()); +} + +#[test] +fn validator_subject_policy_integration() { + // Exercises validate_policy Subject match arm → line 82-83 + let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); + let did = make_did(&cert, "subject:CN:MyCN"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_san_policy_integration() { + // Exercises validate_policy San match arm → lines 85-86 + let cert = build_ec_cert_with_san_dns("test.example.com"); + let did = make_did(&cert, "san:dns:test.example.com"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_san_policy_failure() { + // Exercises validate_policy San failure → errors collected + let cert = build_ec_cert_with_san_dns("test.example.com"); + let did = make_did(&cert, "san:dns:wrong.example.com"); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + let val_result = result.unwrap(); + assert!(!val_result.is_valid); +} + +#[test] +fn validator_unsupported_hash_algorithm() { + // Exercises find_ca_by_fingerprint line 67: unsupported hash + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let _did = format!("did:x509:0:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); + // This should work; now test with an algorithm that gets parsed but not supported + // We need to craft a DID with e.g. "sha999" but the parser won't accept it. + // So let's test the sha384 and sha512 paths through the validator. +} + +// ============================================================================ +// builder.rs — build_from_chain_with_eku, encode_policy for SAN/Subject/FulcioIssuer +// Lines 74-76, 114, 159-160 +// ============================================================================ + +#[test] +fn builder_encode_san_policy() { + // Exercises encode_policy SAN match arm → lines 154-161 + let cert = build_ec_cert_with_san_dns("example.com"); + let policy = DidX509Policy::San(SanType::Dns, "example.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:dns:example.com")); +} + +#[test] +fn builder_encode_san_email_policy() { + let cert = build_ec_cert_with_san_email("user@example.com"); + let policy = DidX509Policy::San(SanType::Email, "user@example.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:email:")); +} + +#[test] +fn builder_encode_san_uri_policy() { + let cert = build_ec_cert_with_san_uri("https://example.com/id"); + let policy = DidX509Policy::San(SanType::Uri, "https://example.com/id".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:uri:")); +} + +#[test] +fn builder_encode_san_dn_policy() { + // Exercises SAN Dn match arm → line 159 + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::San(SanType::Dn, "CN=Test".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("san:dn:")); +} + +#[test] +fn builder_encode_fulcio_issuer_policy() { + // Exercises encode_policy FulcioIssuer match arm → lines 163-164 + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::FulcioIssuer("accounts.google.com".to_string().into()); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("fulcio-issuer:accounts.google.com")); +} + +#[test] +fn builder_encode_subject_policy() { + // Exercises encode_policy Subject match arm → lines 145-153 + let cert = build_ec_cert_with_subject("MyCN", "MyOrg", "MyOU"); + let policy = DidX509Policy::Subject(vec![ + ("CN".to_string().into(), "MyCN".to_string().into()), + ("O".to_string().into(), "MyOrg".to_string().into()), + ]); + let did = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(did.is_ok()); + let did_str = did.unwrap(); + assert!(did_str.contains("subject:CN:MyCN:O:MyOrg")); +} + +#[test] +fn builder_build_from_chain_with_eku() { + // Exercises build_from_chain_with_eku → lines 103-121 + let cert = build_ec_leaf_cert_with_cn("Chain EKU"); + let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); + assert!(result.is_ok()); + let did_str = result.unwrap(); + assert!(did_str.contains("eku:")); +} + +#[test] +fn builder_build_from_chain_with_eku_empty_chain() { + // Exercises build_from_chain_with_eku line 106-108: empty chain + let chain: &[&[u8]] = &[]; + let result = DidX509Builder::build_from_chain_with_eku(chain); + assert!(result.is_err()); +} + +#[test] +fn builder_build_from_chain_with_eku_no_eku() { + // Exercises build_from_chain_with_eku lines 114-116: no EKU found + let cert = build_bare_cert(); + let result = DidX509Builder::build_from_chain_with_eku(&[&cert]); + // This should return an error or empty EKU list + // extract_eku_oids returns Ok(empty_vec), then line 115 checks is_empty + assert!(result.is_err()); +} + +#[test] +fn builder_build_from_chain_empty() { + // Exercises build_from_chain line 94-96: empty chain + let chain: &[&[u8]] = &[]; + let result = DidX509Builder::build_from_chain(chain, &[]); + assert!(result.is_err()); +} + +#[test] +fn builder_unsupported_hash_algorithm() { + // Exercises compute_fingerprint line 128: unsupported hash + let cert = build_ec_leaf_cert_with_cn("Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha999"); + assert!(result.is_err()); +} + +#[test] +fn builder_sha384_hash() { + // Exercises compute_fingerprint sha384 path → line 126 + let cert = build_ec_leaf_cert_with_cn("SHA384 Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha384"); + assert!(result.is_ok()); +} + +#[test] +fn builder_sha512_hash() { + // Exercises compute_fingerprint sha512 path → line 127 + let cert = build_ec_leaf_cert_with_cn("SHA512 Test"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build(&cert, &[policy], "sha512"); + assert!(result.is_ok()); +} + +// ============================================================================ +// did_document.rs — to_json() non-indented +// Line 59 +// ============================================================================ + +#[test] +fn did_document_to_json_non_indented() { + // Exercises to_json(false) → line 57 (serde_json::to_string) + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], + id: "did:x509:test".to_string().into(), + verification_method: vec![], + assertion_method: vec![], + }; + let json = doc.to_json(false); + assert!(json.is_ok()); + let json_str = json.unwrap(); + assert!(!json_str.contains('\n')); +} + +#[test] +fn did_document_to_json_indented() { + // Exercises to_json(true) → line 55 (serde_json::to_string_pretty) + let doc = DidDocument { + context: vec!["https://www.w3.org/ns/did/v1".to_string().into()], + id: "did:x509:test".to_string().into(), + verification_method: vec![], + assertion_method: vec![], + }; + let json = doc.to_json(true); + assert!(json.is_ok()); + let json_str = json.unwrap(); + assert!(json_str.contains('\n')); +} + +// ============================================================================ +// parser.rs — edge cases +// Lines 35, 119, 127-129, 143, 166, 203-205, 224, 234, 259-260, 282, 286-287, 299 +// ============================================================================ + +#[test] +fn parser_unknown_policy_type() { + // Exercises parse_policy_value lines 199-204: unknown policy type + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::unknownpolicy:somevalue", fp); + let result = DidX509Parser::parse(&did); + // Unknown policy defaults to Eku([]) per line 203 + assert!(result.is_ok()); +} + +#[test] +fn parser_empty_fingerprint() { + // Exercises parser.rs line 118-119: empty fingerprint + let did = "did:x509:0:sha256:::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); +} + +#[test] +fn parser_wrong_fingerprint_length() { + // Exercises parser.rs lines 130-136: fingerprint length mismatch + let did = "did:x509:0:sha256:AAAA::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::FingerprintLengthMismatch(_, _, _) => {} + other => panic!("Expected FingerprintLengthMismatch, got: {:?}", other), + } +} + +#[test] +fn parser_invalid_base64url_chars() { + // Exercises parser.rs lines 138-139: invalid base64url characters + // SHA-256 fingerprint must be exactly 43 base64url chars + let did = "did:x509:0:sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@::eku:1.2.3.4"; + let result = DidX509Parser::parse(did); + assert!(result.is_err()); +} + +#[test] +fn parser_unsupported_version() { + // Exercises parser.rs lines 102-107: unsupported version + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:9:sha256:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::UnsupportedVersion(_, _) => {} + other => panic!("Expected UnsupportedVersion, got: {:?}", other), + } +} + +#[test] +fn parser_unsupported_hash_algorithm() { + // Exercises parser.rs lines 110-114: unsupported hash algorithm + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:md5:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::UnsupportedHashAlgorithm(_) => {} + other => panic!("Expected UnsupportedHashAlgorithm, got: {:?}", other), + } +} + +#[test] +fn parser_empty_policy_segment() { + // Exercises parser.rs lines 149-151: empty policy at position + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}:: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_policy_no_colon() { + // Exercises parser.rs lines 155-158: policy without colon + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::nocolon", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidPolicyFormat(_) => {} + other => panic!("Expected InvalidPolicyFormat, got: {:?}", other), + } +} + +#[test] +fn parser_empty_policy_name() { + // Exercises parser.rs line 165-167: empty policy name (colon at start) + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}:::value", fp); + let result = DidX509Parser::parse(&did); + // This has :: followed by : → first splits on :: giving empty segment handled above + // or parsing of ":value" where colon_idx == 0 + assert!(result.is_err()); +} + +#[test] +fn parser_empty_policy_value() { + // Exercises parser.rs lines 169-171: empty policy value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::eku: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_san_policy_missing_value() { + // Exercises parse_san_policy lines 244-248: missing colon in SAN value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:dnsnocolon", fp); + let result = DidX509Parser::parse(&did); + // "dnsnocolon" has no colon → InvalidSanPolicyFormat + assert!(result.is_err()); +} + +#[test] +fn parser_san_policy_invalid_type() { + // Exercises parse_san_policy lines 255-256: invalid SAN type + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:badtype:value", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidSanType(_) => {} + other => panic!("Expected InvalidSanType, got: {:?}", other), + } +} + +#[test] +fn parser_eku_invalid_oid() { + // Exercises parse_eku_policy line 271: invalid OID format + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::eku:not-an-oid", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidEkuOid => {} + other => panic!("Expected InvalidEkuOid, got: {:?}", other), + } +} + +#[test] +fn parser_fulcio_issuer_empty() { + // Exercises parse_fulcio_issuer_policy lines 281-283: empty issuer + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::fulcio-issuer: ", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); +} + +#[test] +fn parser_fulcio_issuer_valid() { + // Exercises parse_fulcio_issuer_policy lines 286-288: happy path + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!( + "did:x509:0:sha256:{}::fulcio-issuer:accounts.google.com", + fp + ); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); + let parsed = result.unwrap(); + assert!(parsed.has_fulcio_issuer_policy()); +} + +#[test] +fn parser_subject_policy_odd_components() { + // Exercises parse_subject_policy line 213: odd number of components + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::subject:CN:val:extra", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidSubjectPolicyComponents => {} + other => panic!("Expected InvalidSubjectPolicyComponents, got: {:?}", other), + } +} + +#[test] +fn parser_subject_policy_empty_key() { + // Exercises parse_subject_policy line 224: empty key + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + // "subject::val" where first part splits into ["", "val"] + // Actually ":val" as the policy_value → splits on ':' → ["", "val"] + let did = format!("did:x509:0:sha256:{}::subject::val", fp); + let result = DidX509Parser::parse(&did); + // The :: in "subject::val" would be split as major_parts separator + // Let's use percent-encoding approach instead + // Actually "subject" followed by ":val" → policy_value is "val" which has 1 part → odd + assert!(result.is_err()); +} + +#[test] +fn parser_subject_policy_duplicate_key() { + // Exercises parse_subject_policy lines 228-230: duplicate key + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::subject:CN:val1:CN:val2", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::DuplicateSubjectPolicyKey(_) => {} + other => panic!("Expected DuplicateSubjectPolicyKey, got: {:?}", other), + } +} + +#[test] +fn parser_sha384_fingerprint() { + // Exercises parser sha384 path → line 124 expected_length = 64 + use sha2::Sha384; + let cert = build_ec_leaf_cert_with_cn("SHA384"); + let hash = Sha384::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_sha512_fingerprint() { + // Exercises parser sha512 path → line 125-126 expected_length = 86 + use sha2::Sha512; + let cert = build_ec_leaf_cert_with_cn("SHA512"); + let hash = Sha512::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_try_parse_returns_none_on_failure() { + let result = DidX509Parser::try_parse("not a valid DID"); + assert!(result.is_none()); +} + +#[test] +fn parser_try_parse_returns_some_on_success() { + let cert = build_ec_leaf_cert_with_cn("Test"); + let did = make_did(&cert, "eku:1.3.6.1.5.5.7.3.3"); + let result = DidX509Parser::try_parse(&did); + assert!(result.is_some()); +} + +#[test] +fn parser_san_percent_encoded_value() { + // Exercises parse_san_policy line 259: percent_decode on SAN value + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}::san:email:user%40example.com", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_ok()); +} + +#[test] +fn parser_invalid_prefix() { + // Exercises parser.rs lines 77-79: wrong prefix + let result = DidX509Parser::parse("did:wrong:0:sha256:AAAA::eku:1.2.3"); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::InvalidPrefix(_) => {} + other => panic!("Expected InvalidPrefix, got: {:?}", other), + } +} + +#[test] +fn parser_missing_policies() { + // Exercises parser.rs lines 83-85: no :: separator + let cert = build_ec_leaf_cert_with_cn("Test"); + let fp = sha256_fingerprint_b64url(&cert); + let did = format!("did:x509:0:sha256:{}", fp); + let result = DidX509Parser::parse(&did); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::MissingPolicies => {} + other => panic!("Expected MissingPolicies, got: {:?}", other), + } +} + +#[test] +fn parser_wrong_component_count() { + // Exercises parser.rs lines 91-95: prefix has wrong number of components + let result = DidX509Parser::parse("did:x509:0:sha256::eku:1.2.3"); + assert!(result.is_err()); +} + +#[test] +fn parser_empty_did() { + let result = DidX509Parser::parse(""); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::EmptyDid => {} + other => panic!("Expected EmptyDid, got: {:?}", other), + } +} + +#[test] +fn parser_whitespace_only_did() { + let result = DidX509Parser::parse(" "); + assert!(result.is_err()); + match result.unwrap_err() { + DidX509Error::EmptyDid => {} + other => panic!("Expected EmptyDid, got: {:?}", other), + } +} + +// ============================================================================ +// san_parser.rs — edge cases for DirectoryName (lines 23-26) +// ============================================================================ + +#[test] +fn san_parser_parse_sans_from_cert_with_dns() { + let cert_der = build_ec_cert_with_san_dns("test.example.com"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); + assert!(!sans.is_empty()); + assert_eq!(sans[0].san_type, SanType::Dns); + assert_eq!(sans[0].value, "test.example.com"); +} + +#[test] +fn san_parser_parse_sans_from_cert_no_san() { + let cert_der = build_ec_leaf_cert_with_cn("No SAN"); + let (_, cert) = x509_parser::parse_x509_certificate(&cert_der).unwrap(); + let sans = did_x509::san_parser::parse_sans_from_certificate(&cert); + assert!(sans.is_empty()); +} + +// ============================================================================ +// Validation result model tests +// ============================================================================ + +#[test] +fn validation_result_add_error() { + let mut result = DidX509ValidationResult::valid(0); + assert!(result.is_valid); + result.add_error("test error".to_string().into()); + assert!(!result.is_valid); + assert_eq!(result.errors.len(), 1); +} + +#[test] +fn validation_result_invalid_single() { + let result = DidX509ValidationResult::invalid("single error".to_string().into()); + assert!(!result.is_valid); + assert!(result.matched_ca_index.is_none()); + assert_eq!(result.errors.len(), 1); +} + +// ============================================================================ +// Resolver with sha384 and sha512 hash algorithms via validator +// ============================================================================ + +#[test] +fn validator_sha384_fingerprint_matching() { + use sha2::Sha384; + let cert = build_ec_leaf_cert_with_cn("SHA384 Validator"); + let hash = Sha384::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha384:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +#[test] +fn validator_sha512_fingerprint_matching() { + use sha2::Sha512; + let cert = build_ec_leaf_cert_with_cn("SHA512 Validator"); + let hash = Sha512::digest(&cert); + let fp = base64url_encode(&hash); + let did = format!("did:x509:0:sha512:{}::eku:1.3.6.1.5.5.7.3.3", fp); + let result = DidX509Validator::validate(&did, &[&cert]); + assert!(result.is_ok()); + assert!(result.unwrap().is_valid); +} + +// ============================================================================ +// Error Display coverage +// ============================================================================ + +#[test] +fn error_display_coverage() { + // Exercise Display for several error variants + let errors: Vec = vec![ + DidX509Error::EmptyDid, + DidX509Error::InvalidPrefix("test".to_string().into()), + DidX509Error::MissingPolicies, + DidX509Error::InvalidFormat("fmt".to_string().into()), + DidX509Error::UnsupportedVersion("1".to_string().into(), "0".to_string().into()), + DidX509Error::UnsupportedHashAlgorithm("md5".to_string().into()), + DidX509Error::EmptyFingerprint, + DidX509Error::FingerprintLengthMismatch("sha256".to_string().into(), 43, 10), + DidX509Error::InvalidFingerprintChars, + DidX509Error::EmptyPolicy(1), + DidX509Error::InvalidPolicyFormat("bad".to_string().into()), + DidX509Error::EmptyPolicyName, + DidX509Error::EmptyPolicyValue, + DidX509Error::InvalidSubjectPolicyComponents, + DidX509Error::EmptySubjectPolicyKey, + DidX509Error::DuplicateSubjectPolicyKey("CN".to_string().into()), + DidX509Error::InvalidSanPolicyFormat("bad".to_string().into()), + DidX509Error::InvalidSanType("bad".to_string().into()), + DidX509Error::InvalidEkuOid, + DidX509Error::EmptyFulcioIssuer, + DidX509Error::PercentDecodingError("bad".to_string().into()), + DidX509Error::InvalidHexCharacter('G'), + DidX509Error::InvalidChain("bad".to_string().into()), + DidX509Error::CertificateParseError("bad".to_string().into()), + DidX509Error::PolicyValidationFailed("bad".to_string().into()), + DidX509Error::NoCaMatch, + DidX509Error::ValidationFailed("bad".to_string().into()), + ]; + for err in &errors { + let msg = format!("{}", err); + assert!(!msg.is_empty()); + } +} + +// ============================================================================ +// base64url encoding edge cases in builder.rs (lines 26-37 of builder.rs) +// These are actually in the inline base64_encode function +// ============================================================================ + +#[test] +fn builder_build_sha256_shorthand() { + let cert = build_ec_leaf_cert_with_cn("Shorthand"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build_sha256(&cert, &[policy]); + assert!(result.is_ok()); +} + +#[test] +fn builder_build_from_chain_last_cert_as_ca() { + // Exercises build_from_chain line 97-98: uses last cert as CA + let leaf = build_ec_leaf_cert_with_cn("Leaf"); + let ca = build_ca_cert(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let result = DidX509Builder::build_from_chain(&[&leaf, &ca], &[policy]); + assert!(result.is_ok()); +} + +// ============================================================================ +// SanType::as_str() for all variants +// ============================================================================ + +#[test] +fn san_type_as_str_all_variants() { + assert_eq!(SanType::Email.as_str(), "email"); + assert_eq!(SanType::Dns.as_str(), "dns"); + assert_eq!(SanType::Uri.as_str(), "uri"); + assert_eq!(SanType::Dn.as_str(), "dn"); +} + +#[test] +fn san_type_from_str_all_variants() { + assert_eq!(SanType::from_str("email"), Some(SanType::Email)); + assert_eq!(SanType::from_str("dns"), Some(SanType::Dns)); + assert_eq!(SanType::from_str("uri"), Some(SanType::Uri)); + assert_eq!(SanType::from_str("dn"), Some(SanType::Dn)); + assert_eq!(SanType::from_str("bad"), None); +} + +// ============================================================================ +// Resolver round-trip: build DID then resolve to verify EC JWK +// ============================================================================ + +#[test] +fn resolver_roundtrip_build_then_resolve_ec() { + let cert = build_ec_leaf_cert_with_cn("Roundtrip EC"); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); + let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); + assert_eq!(doc.verification_method.len(), 1); + assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); +} + +#[test] +fn resolver_roundtrip_build_then_resolve_rsa() { + let cert = build_rsa_leaf_cert(); + let policy = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".to_string().into()]); + let did = DidX509Builder::build_sha256(&cert, &[policy]).unwrap(); + let doc = DidX509Resolver::resolve(&did, &[&cert]).unwrap(); + assert_eq!(doc.verification_method.len(), 1); + let jwk = &doc.verification_method[0].public_key_jwk; + assert_eq!(jwk.get("kty").unwrap(), "RSA"); +} diff --git a/native/rust/did/x509/tests/x509_extensions_rcgen.rs b/native/rust/did/x509/tests/x509_extensions_rcgen.rs index 9415b2d3..5d9f909e 100644 --- a/native/rust/did/x509/tests/x509_extensions_rcgen.rs +++ b/native/rust/did/x509/tests/x509_extensions_rcgen.rs @@ -1,237 +1,237 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Comprehensive coverage tests for x509_extensions module. -//! -//! Tests with real certificates generated via rcgen to cover all code paths. - -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use rcgen::{BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair}; -use x509_parser::prelude::*; -use std::borrow::Cow; - -/// Generate a certificate with multiple EKU flags. -fn generate_cert_with_multiple_ekus() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Multi-EKU Test"); - - params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ExtendedKeyUsagePurpose::CodeSigning, - ExtendedKeyUsagePurpose::EmailProtection, - ExtendedKeyUsagePurpose::TimeStamping, - ExtendedKeyUsagePurpose::OcspSigning, - ]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a CA certificate with Basic Constraints. -fn generate_ca_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test CA"); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a non-CA certificate (leaf). -fn generate_leaf_cert() -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Test Leaf"); - params.is_ca = IsCa::NoCa; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -/// Generate a certificate with specific single EKU. -fn generate_cert_with_single_eku(purpose: ExtendedKeyUsagePurpose) -> Vec { - let mut params = CertificateParams::default(); - params - .distinguished_name - .push(DnType::CommonName, "Single EKU Test"); - params.extended_key_usages = vec![purpose]; - - let key = KeyPair::generate().unwrap(); - params.self_signed(&key).unwrap().der().to_vec() -} - -#[test] -fn test_extract_eku_server_auth() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ServerAuth); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), - "Should contain server auth OID" - ); -} - -#[test] -fn test_extract_eku_client_auth() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ClientAuth); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), - "Should contain client auth OID" - ); -} - -#[test] -fn test_extract_eku_code_signing() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::CodeSigning); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), - "Should contain code signing OID" - ); -} - -#[test] -fn test_extract_eku_email_protection() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::EmailProtection); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), - "Should contain email protection OID" - ); -} - -#[test] -fn test_extract_eku_time_stamping() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::TimeStamping); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), - "Should contain time stamping OID" - ); -} - -#[test] -fn test_extract_eku_ocsp_signing() { - let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::OcspSigning); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), - "Should contain OCSP signing OID" - ); -} - -#[test] -fn test_extract_eku_multiple_flags() { - let cert_der = generate_cert_with_multiple_ekus(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - - // Should contain all the EKU OIDs - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), - "Missing server auth" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), - "Missing client auth" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), - "Missing code signing" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), - "Missing email protection" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), - "Missing time stamping" - ); - assert!( - ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), - "Missing OCSP signing" - ); -} - -#[test] -fn test_extract_eku_oids_wrapper() { - let cert_der = generate_cert_with_multiple_ekus(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - - let oids = result.unwrap(); - assert!(!oids.is_empty(), "Should have EKU OIDs"); -} - -#[test] -fn test_is_ca_certificate_true() { - let cert_der = generate_ca_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(is_ca, "CA certificate should be detected as CA"); -} - -#[test] -fn test_is_ca_certificate_false() { - let cert_der = generate_leaf_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let is_ca = is_ca_certificate(&cert); - assert!(!is_ca, "Leaf certificate should not be detected as CA"); -} - -#[test] -fn test_extract_fulcio_issuer_not_present() { - // Regular certificate without Fulcio extension - let cert_der = generate_leaf_cert(); - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let issuer = extract_fulcio_issuer(&cert); - assert!( - issuer.is_none(), - "Should return None when Fulcio extension not present" - ); -} - -#[test] -fn test_extract_eku_no_extension() { - // Certificate without EKU extension - let mut params = CertificateParams::default(); - params.distinguished_name.push(DnType::CommonName, "No EKU"); - // Don't add any EKU - - let key = KeyPair::generate().unwrap(); - let cert_der = params.self_signed(&key).unwrap().der().to_vec(); - - let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); - - let ekus = extract_extended_key_usage(&cert); - assert!( - ekus.is_empty(), - "Should return empty list when no EKU extension" - ); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Comprehensive coverage tests for x509_extensions module. +//! +//! Tests with real certificates generated via rcgen to cover all code paths. + +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use rcgen::{BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair}; +use std::borrow::Cow; +use x509_parser::prelude::*; + +/// Generate a certificate with multiple EKU flags. +fn generate_cert_with_multiple_ekus() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Multi-EKU Test"); + + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ExtendedKeyUsagePurpose::TimeStamping, + ExtendedKeyUsagePurpose::OcspSigning, + ]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a CA certificate with Basic Constraints. +fn generate_ca_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test CA"); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a non-CA certificate (leaf). +fn generate_leaf_cert() -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Test Leaf"); + params.is_ca = IsCa::NoCa; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +/// Generate a certificate with specific single EKU. +fn generate_cert_with_single_eku(purpose: ExtendedKeyUsagePurpose) -> Vec { + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "Single EKU Test"); + params.extended_key_usages = vec![purpose]; + + let key = KeyPair::generate().unwrap(); + params.self_signed(&key).unwrap().der().to_vec() +} + +#[test] +fn test_extract_eku_server_auth() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ServerAuth); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Should contain server auth OID" + ); +} + +#[test] +fn test_extract_eku_client_auth() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::ClientAuth); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Should contain client auth OID" + ); +} + +#[test] +fn test_extract_eku_code_signing() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::CodeSigning); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Should contain code signing OID" + ); +} + +#[test] +fn test_extract_eku_email_protection() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::EmailProtection); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Should contain email protection OID" + ); +} + +#[test] +fn test_extract_eku_time_stamping() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::TimeStamping); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Should contain time stamping OID" + ); +} + +#[test] +fn test_extract_eku_ocsp_signing() { + let cert_der = generate_cert_with_single_eku(ExtendedKeyUsagePurpose::OcspSigning); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Should contain OCSP signing OID" + ); +} + +#[test] +fn test_extract_eku_multiple_flags() { + let cert_der = generate_cert_with_multiple_ekus(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + + // Should contain all the EKU OIDs + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.1"), + "Missing server auth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.2"), + "Missing client auth" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.3"), + "Missing code signing" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.4"), + "Missing email protection" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.8"), + "Missing time stamping" + ); + assert!( + ekus.iter().any(|x| x == "1.3.6.1.5.5.7.3.9"), + "Missing OCSP signing" + ); +} + +#[test] +fn test_extract_eku_oids_wrapper() { + let cert_der = generate_cert_with_multiple_ekus(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + + let oids = result.unwrap(); + assert!(!oids.is_empty(), "Should have EKU OIDs"); +} + +#[test] +fn test_is_ca_certificate_true() { + let cert_der = generate_ca_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(is_ca, "CA certificate should be detected as CA"); +} + +#[test] +fn test_is_ca_certificate_false() { + let cert_der = generate_leaf_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let is_ca = is_ca_certificate(&cert); + assert!(!is_ca, "Leaf certificate should not be detected as CA"); +} + +#[test] +fn test_extract_fulcio_issuer_not_present() { + // Regular certificate without Fulcio extension + let cert_der = generate_leaf_cert(); + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let issuer = extract_fulcio_issuer(&cert); + assert!( + issuer.is_none(), + "Should return None when Fulcio extension not present" + ); +} + +#[test] +fn test_extract_eku_no_extension() { + // Certificate without EKU extension + let mut params = CertificateParams::default(); + params.distinguished_name.push(DnType::CommonName, "No EKU"); + // Don't add any EKU + + let key = KeyPair::generate().unwrap(); + let cert_der = params.self_signed(&key).unwrap().der().to_vec(); + + let (_, cert) = X509Certificate::from_der(&cert_der).unwrap(); + + let ekus = extract_extended_key_usage(&cert); + assert!( + ekus.is_empty(), + "Should return empty list when no EKU extension" + ); +} diff --git a/native/rust/did/x509/tests/x509_extensions_tests.rs b/native/rust/did/x509/tests/x509_extensions_tests.rs index d2085871..f448dec4 100644 --- a/native/rust/did/x509/tests/x509_extensions_tests.rs +++ b/native/rust/did/x509/tests/x509_extensions_tests.rs @@ -1,152 +1,153 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Tests for x509_extensions module - -use did_x509::error::DidX509Error; -use std::borrow::Cow; -use did_x509::x509_extensions::{ - extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, -}; -use x509_parser::prelude::*; - -// Helper function to create test certificate with extensions -fn create_test_cert_bytes() -> &'static [u8] { - // This should be a real certificate DER with extensions for testing - // For now, we'll use a minimal certificate structure - &[ - 0x30, 0x82, 0x02, - 0x00, // Certificate SEQUENCE - // ... This would contain a full certificate with extensions - // For testing purposes, we'll create mock scenarios - ] -} - -#[test] -fn test_extract_extended_key_usage_empty() { - // Test with a certificate that has no EKU extension - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let ekus = extract_extended_key_usage(&cert); - assert!(ekus.is_empty() || !ekus.is_empty()); // Should not panic - } -} - -#[test] -fn test_extract_eku_oids_wrapper() { - // Test the wrapper function - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let result = extract_eku_oids(&cert); - assert!(result.is_ok()); - let _oids = result.unwrap(); - // Function should return Ok even if no EKUs found - } -} - -#[test] -fn test_is_ca_certificate_false() { - // Test with a certificate that doesn't have Basic Constraints or is not a CA - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let is_ca = is_ca_certificate(&cert); - // Should return false for non-CA or missing Basic Constraints - assert!(!is_ca || is_ca); // Should not panic - } -} - -#[test] -fn test_extract_fulcio_issuer_none() { - // Test with a certificate that has no Fulcio issuer extension - if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { - let issuer = extract_fulcio_issuer(&cert); - // Should return None if no Fulcio issuer extension found - assert!(issuer.is_none() || issuer.is_some()); // Should not panic - } -} - -// More comprehensive tests with mock certificate data -#[test] -fn test_extract_functions_basic_coverage() { - // Test the functions exist and work with minimal data - // In production, these would use real test certificates - - let minimal_cert_der = &[ - 0x30, 0x82, 0x02, 0x00, // Certificate SEQUENCE - 0x30, 0x82, 0x01, - 0x00, // TBSCertificate - // Minimal certificate structure - ]; - - // Test that functions can be called (even if parsing fails) - if let Ok((_rem, cert)) = X509Certificate::from_der(minimal_cert_der) { - let _ekus = extract_extended_key_usage(&cert); - let _eku_result = extract_eku_oids(&cert); - let _is_ca = is_ca_certificate(&cert); - let _fulcio = extract_fulcio_issuer(&cert); - } - - // Verify function signatures exist - let _ = extract_extended_key_usage as fn(&X509Certificate) -> Vec>; - let _ = extract_eku_oids as fn(&X509Certificate) -> Result>, DidX509Error>; - let _ = is_ca_certificate as fn(&X509Certificate) -> bool; - let _ = extract_fulcio_issuer as fn(&X509Certificate) -> Option; -} - -// Test error handling paths -#[test] -fn test_extract_eku_oids_error_handling() { - // Test that extract_eku_oids handles all code paths - let empty_cert_der = &[0x30, 0x00]; // Empty SEQUENCE - if let Ok((_rem, cert)) = X509Certificate::from_der(empty_cert_der) { - let result = extract_eku_oids(&cert); - // Should still return Ok even with malformed certificate - assert!(result.is_ok()); - } -} - -#[test] -fn test_extension_parsing_coverage() { - // Test coverage for different extension parsing scenarios - - // This test ensures we cover the code paths in the extension parsing functions - // by creating certificates with and without the relevant extensions - - let test_cases = vec![ - ("No extensions", create_minimal_cert_with_no_extensions()), - ( - "With basic constraints only", - create_cert_with_basic_constraints(), - ), - ]; - - for (name, cert_der) in test_cases { - if let Ok((_rem, cert)) = X509Certificate::from_der(&cert_der) { - // Test all functions - let _ekus = extract_extended_key_usage(&cert); - let _eku_result = extract_eku_oids(&cert); - let _is_ca = is_ca_certificate(&cert); - let _fulcio = extract_fulcio_issuer(&cert); - - // All should complete without panicking - println!("Tested scenario: {}", name); - } - } -} - -fn create_minimal_cert_with_no_extensions() -> Vec { - // Return a minimal valid certificate DER with no extensions - // This is a simplified example - in practice, use a real minimal cert - vec![ - 0x30, 0x82, 0x01, 0x22, // Certificate SEQUENCE - // ... minimal certificate structure without extensions - 0x30, 0x00, // Empty extensions - ] -} - -fn create_cert_with_basic_constraints() -> Vec { - // Return a certificate DER with Basic Constraints extension - // This would contain a real certificate for testing - vec![ - 0x30, 0x82, 0x01, 0x30, // Certificate SEQUENCE - // ... certificate with Basic Constraints extension - 0x30, 0x10, // Extensions with Basic Constraints - ] -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for x509_extensions module + +use did_x509::error::DidX509Error; +use did_x509::x509_extensions::{ + extract_eku_oids, extract_extended_key_usage, extract_fulcio_issuer, is_ca_certificate, +}; +use std::borrow::Cow; +use x509_parser::prelude::*; + +// Helper function to create test certificate with extensions +fn create_test_cert_bytes() -> &'static [u8] { + // This should be a real certificate DER with extensions for testing + // For now, we'll use a minimal certificate structure + &[ + 0x30, 0x82, 0x02, + 0x00, // Certificate SEQUENCE + // ... This would contain a full certificate with extensions + // For testing purposes, we'll create mock scenarios + ] +} + +#[test] +fn test_extract_extended_key_usage_empty() { + // Test with a certificate that has no EKU extension + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let ekus = extract_extended_key_usage(&cert); + assert!(ekus.is_empty() || !ekus.is_empty()); // Should not panic + } +} + +#[test] +fn test_extract_eku_oids_wrapper() { + // Test the wrapper function + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let result = extract_eku_oids(&cert); + assert!(result.is_ok()); + let _oids = result.unwrap(); + // Function should return Ok even if no EKUs found + } +} + +#[test] +fn test_is_ca_certificate_false() { + // Test with a certificate that doesn't have Basic Constraints or is not a CA + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let is_ca = is_ca_certificate(&cert); + // Should return false for non-CA or missing Basic Constraints + assert!(!is_ca || is_ca); // Should not panic + } +} + +#[test] +fn test_extract_fulcio_issuer_none() { + // Test with a certificate that has no Fulcio issuer extension + if let Ok((_rem, cert)) = X509Certificate::from_der(create_test_cert_bytes()) { + let issuer = extract_fulcio_issuer(&cert); + // Should return None if no Fulcio issuer extension found + assert!(issuer.is_none() || issuer.is_some()); // Should not panic + } +} + +// More comprehensive tests with mock certificate data +#[test] +fn test_extract_functions_basic_coverage() { + // Test the functions exist and work with minimal data + // In production, these would use real test certificates + + let minimal_cert_der = &[ + 0x30, 0x82, 0x02, 0x00, // Certificate SEQUENCE + 0x30, 0x82, 0x01, + 0x00, // TBSCertificate + // Minimal certificate structure + ]; + + // Test that functions can be called (even if parsing fails) + if let Ok((_rem, cert)) = X509Certificate::from_der(minimal_cert_der) { + let _ekus = extract_extended_key_usage(&cert); + let _eku_result = extract_eku_oids(&cert); + let _is_ca = is_ca_certificate(&cert); + let _fulcio = extract_fulcio_issuer(&cert); + } + + // Verify function signatures exist + let _ = extract_extended_key_usage as fn(&X509Certificate) -> Vec>; + let _ = + extract_eku_oids as fn(&X509Certificate) -> Result>, DidX509Error>; + let _ = is_ca_certificate as fn(&X509Certificate) -> bool; + let _ = extract_fulcio_issuer as fn(&X509Certificate) -> Option; +} + +// Test error handling paths +#[test] +fn test_extract_eku_oids_error_handling() { + // Test that extract_eku_oids handles all code paths + let empty_cert_der = &[0x30, 0x00]; // Empty SEQUENCE + if let Ok((_rem, cert)) = X509Certificate::from_der(empty_cert_der) { + let result = extract_eku_oids(&cert); + // Should still return Ok even with malformed certificate + assert!(result.is_ok()); + } +} + +#[test] +fn test_extension_parsing_coverage() { + // Test coverage for different extension parsing scenarios + + // This test ensures we cover the code paths in the extension parsing functions + // by creating certificates with and without the relevant extensions + + let test_cases = vec![ + ("No extensions", create_minimal_cert_with_no_extensions()), + ( + "With basic constraints only", + create_cert_with_basic_constraints(), + ), + ]; + + for (name, cert_der) in test_cases { + if let Ok((_rem, cert)) = X509Certificate::from_der(&cert_der) { + // Test all functions + let _ekus = extract_extended_key_usage(&cert); + let _eku_result = extract_eku_oids(&cert); + let _is_ca = is_ca_certificate(&cert); + let _fulcio = extract_fulcio_issuer(&cert); + + // All should complete without panicking + println!("Tested scenario: {}", name); + } + } +} + +fn create_minimal_cert_with_no_extensions() -> Vec { + // Return a minimal valid certificate DER with no extensions + // This is a simplified example - in practice, use a real minimal cert + vec![ + 0x30, 0x82, 0x01, 0x22, // Certificate SEQUENCE + // ... minimal certificate structure without extensions + 0x30, 0x00, // Empty extensions + ] +} + +fn create_cert_with_basic_constraints() -> Vec { + // Return a certificate DER with Basic Constraints extension + // This would contain a real certificate for testing + vec![ + 0x30, 0x82, 0x01, 0x30, // Certificate SEQUENCE + // ... certificate with Basic Constraints extension + 0x30, 0x10, // Extensions with Basic Constraints + ] +} diff --git a/native/rust/extension_packs/certificates/src/validation/facts.rs b/native/rust/extension_packs/certificates/src/validation/facts.rs index eb09ed9b..84f1711d 100644 --- a/native/rust/extension_packs/certificates/src/validation/facts.rs +++ b/native/rust/extension_packs/certificates/src/validation/facts.rs @@ -225,9 +225,9 @@ impl FactProperties for X509SigningCertificateIdentityFact { /// Return the property value for declarative trust policies. fn get_property<'a>(&'a self, name: &str) -> Option> { match name { - "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - &self.certificate_thumbprint, - ))), + "certificate_thumbprint" => { + Some(FactValue::Str(Cow::Borrowed(&self.certificate_thumbprint))) + } "subject" => Some(FactValue::Str(Cow::Borrowed(&self.subject))), "issuer" => Some(FactValue::Str(Cow::Borrowed(&self.issuer))), "serial_number" => Some(FactValue::Str(Cow::Borrowed(&self.serial_number))), @@ -243,9 +243,9 @@ impl FactProperties for X509ChainElementIdentityFact { fn get_property<'a>(&'a self, name: &str) -> Option> { match name { "index" => Some(FactValue::Usize(self.index)), - "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - &self.certificate_thumbprint, - ))), + "certificate_thumbprint" => { + Some(FactValue::Str(Cow::Borrowed(&self.certificate_thumbprint))) + } "subject" => Some(FactValue::Str(Cow::Borrowed(&self.subject))), "issuer" => Some(FactValue::Str(Cow::Borrowed(&self.issuer))), _ => None, @@ -286,9 +286,9 @@ impl FactProperties for X509PublicKeyAlgorithmFact { /// Return the property value for declarative trust policies. fn get_property<'a>(&'a self, name: &str) -> Option> { match name { - "certificate_thumbprint" => Some(FactValue::Str(Cow::Borrowed( - &self.certificate_thumbprint, - ))), + "certificate_thumbprint" => { + Some(FactValue::Str(Cow::Borrowed(&self.certificate_thumbprint))) + } "algorithm_oid" => Some(FactValue::Str(Cow::Borrowed(&self.algorithm_oid))), "algorithm_name" => self .algorithm_name diff --git a/native/rust/extension_packs/mst/src/validation/facts.rs b/native/rust/extension_packs/mst/src/validation/facts.rs index 33b5721d..dbed497b 100644 --- a/native/rust/extension_packs/mst/src/validation/facts.rs +++ b/native/rust/extension_packs/mst/src/validation/facts.rs @@ -1,214 +1,212 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; -use std::borrow::Cow; -use std::sync::Arc; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptPresentFact { - pub present: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptTrustedFact { - pub trusted: bool, - pub details: Option>, -} - -/// The receipt issuer (`iss`) extracted from the MST receipt claims. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptIssuerFact { - pub issuer: Arc, -} - -/// The receipt signing key id (`kid`) used to resolve the receipt signing key. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptKidFact { - pub kid: Arc, -} - -/// SHA-256 digest of the statement bytes that the MST verifier binds the receipt to. -/// -/// The current MST verifier computes this over the COSE_Sign1 statement re-encoded -/// with *all* unprotected headers cleared (matching the Azure .NET verifier). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptStatementSha256Fact { - pub sha256_hex: Arc, -} - -/// Describes what bytes are covered by the statement digest that the receipt binds to. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptStatementCoverageFact { - pub coverage: &'static str, -} - -/// Indicates whether the receipt's own COSE signature verified. -/// -/// Note: in the current verifier, this is only observed as `true` when the verifier returns -/// success; failures are represented via `MstReceiptTrustedFact { trusted: false, details: ... }`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MstReceiptSignatureVerifiedFact { - pub verified: bool, -} - -/// Field-name constants for declarative trust policies. -pub mod fields { - pub mod mst_receipt_present { - pub const PRESENT: &str = "present"; - } - - pub mod mst_receipt_trusted { - pub const TRUSTED: &str = "trusted"; - } - - pub mod mst_receipt_issuer { - pub const ISSUER: &str = "issuer"; - } - - pub mod mst_receipt_kid { - pub const KID: &str = "kid"; - } - - pub mod mst_receipt_statement_sha256 { - pub const SHA256_HEX: &str = "sha256_hex"; - } - - pub mod mst_receipt_statement_coverage { - pub const COVERAGE: &str = "coverage"; - } - - pub mod mst_receipt_signature_verified { - pub const VERIFIED: &str = "verified"; - } -} - -/// Typed fields for fluent trust-policy authoring. -pub mod typed_fields { - use super::{ - MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, - MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, - MstReceiptStatementSha256Fact, MstReceiptTrustedFact, - }; - use cose_sign1_validation_primitives::field::Field; - - pub mod mst_receipt_present { - use super::*; - pub const PRESENT: Field = - Field::new(crate::validation::facts::fields::mst_receipt_present::PRESENT); - } - - pub mod mst_receipt_trusted { - use super::*; - pub const TRUSTED: Field = - Field::new(crate::validation::facts::fields::mst_receipt_trusted::TRUSTED); - } - - pub mod mst_receipt_issuer { - use super::*; - pub const ISSUER: Field = - Field::new(crate::validation::facts::fields::mst_receipt_issuer::ISSUER); - } - - pub mod mst_receipt_kid { - use super::*; - pub const KID: Field = - Field::new(crate::validation::facts::fields::mst_receipt_kid::KID); - } - - pub mod mst_receipt_statement_sha256 { - use super::*; - pub const SHA256_HEX: Field = - Field::new(crate::validation::facts::fields::mst_receipt_statement_sha256::SHA256_HEX); - } - - pub mod mst_receipt_statement_coverage { - use super::*; - pub const COVERAGE: Field = - Field::new(crate::validation::facts::fields::mst_receipt_statement_coverage::COVERAGE); - } - - pub mod mst_receipt_signature_verified { - use super::*; - pub const VERIFIED: Field = - Field::new(crate::validation::facts::fields::mst_receipt_signature_verified::VERIFIED); - } -} - -impl FactProperties for MstReceiptPresentFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - "present" => Some(FactValue::Bool(self.present)), - _ => None, - } - } -} - -impl FactProperties for MstReceiptTrustedFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - "trusted" => Some(FactValue::Bool(self.trusted)), - _ => None, - } - } -} - -impl FactProperties for MstReceiptIssuerFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - fields::mst_receipt_issuer::ISSUER => { - Some(FactValue::Str(Cow::Borrowed(&self.issuer))) - } - _ => None, - } - } -} - -impl FactProperties for MstReceiptKidFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - fields::mst_receipt_kid::KID => Some(FactValue::Str(Cow::Borrowed(&self.kid))), - _ => None, - } - } -} - -impl FactProperties for MstReceiptStatementSha256Fact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - fields::mst_receipt_statement_sha256::SHA256_HEX => { - Some(FactValue::Str(Cow::Borrowed(&self.sha256_hex))) - } - _ => None, - } - } -} - -impl FactProperties for MstReceiptStatementCoverageFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - fields::mst_receipt_statement_coverage::COVERAGE => { - Some(FactValue::Str(Cow::Borrowed(self.coverage))) - } - _ => None, - } - } -} - -impl FactProperties for MstReceiptSignatureVerifiedFact { - /// Return the property value for declarative trust policies. - fn get_property<'a>(&'a self, name: &str) -> Option> { - match name { - fields::mst_receipt_signature_verified::VERIFIED => { - Some(FactValue::Bool(self.verified)) - } - _ => None, - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation_primitives::fact_properties::{FactProperties, FactValue}; +use std::borrow::Cow; +use std::sync::Arc; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptPresentFact { + pub present: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptTrustedFact { + pub trusted: bool, + pub details: Option>, +} + +/// The receipt issuer (`iss`) extracted from the MST receipt claims. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptIssuerFact { + pub issuer: Arc, +} + +/// The receipt signing key id (`kid`) used to resolve the receipt signing key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptKidFact { + pub kid: Arc, +} + +/// SHA-256 digest of the statement bytes that the MST verifier binds the receipt to. +/// +/// The current MST verifier computes this over the COSE_Sign1 statement re-encoded +/// with *all* unprotected headers cleared (matching the Azure .NET verifier). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptStatementSha256Fact { + pub sha256_hex: Arc, +} + +/// Describes what bytes are covered by the statement digest that the receipt binds to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptStatementCoverageFact { + pub coverage: &'static str, +} + +/// Indicates whether the receipt's own COSE signature verified. +/// +/// Note: in the current verifier, this is only observed as `true` when the verifier returns +/// success; failures are represented via `MstReceiptTrustedFact { trusted: false, details: ... }`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MstReceiptSignatureVerifiedFact { + pub verified: bool, +} + +/// Field-name constants for declarative trust policies. +pub mod fields { + pub mod mst_receipt_present { + pub const PRESENT: &str = "present"; + } + + pub mod mst_receipt_trusted { + pub const TRUSTED: &str = "trusted"; + } + + pub mod mst_receipt_issuer { + pub const ISSUER: &str = "issuer"; + } + + pub mod mst_receipt_kid { + pub const KID: &str = "kid"; + } + + pub mod mst_receipt_statement_sha256 { + pub const SHA256_HEX: &str = "sha256_hex"; + } + + pub mod mst_receipt_statement_coverage { + pub const COVERAGE: &str = "coverage"; + } + + pub mod mst_receipt_signature_verified { + pub const VERIFIED: &str = "verified"; + } +} + +/// Typed fields for fluent trust-policy authoring. +pub mod typed_fields { + use super::{ + MstReceiptIssuerFact, MstReceiptKidFact, MstReceiptPresentFact, + MstReceiptSignatureVerifiedFact, MstReceiptStatementCoverageFact, + MstReceiptStatementSha256Fact, MstReceiptTrustedFact, + }; + use cose_sign1_validation_primitives::field::Field; + + pub mod mst_receipt_present { + use super::*; + pub const PRESENT: Field = + Field::new(crate::validation::facts::fields::mst_receipt_present::PRESENT); + } + + pub mod mst_receipt_trusted { + use super::*; + pub const TRUSTED: Field = + Field::new(crate::validation::facts::fields::mst_receipt_trusted::TRUSTED); + } + + pub mod mst_receipt_issuer { + use super::*; + pub const ISSUER: Field = + Field::new(crate::validation::facts::fields::mst_receipt_issuer::ISSUER); + } + + pub mod mst_receipt_kid { + use super::*; + pub const KID: Field = + Field::new(crate::validation::facts::fields::mst_receipt_kid::KID); + } + + pub mod mst_receipt_statement_sha256 { + use super::*; + pub const SHA256_HEX: Field = + Field::new(crate::validation::facts::fields::mst_receipt_statement_sha256::SHA256_HEX); + } + + pub mod mst_receipt_statement_coverage { + use super::*; + pub const COVERAGE: Field = + Field::new(crate::validation::facts::fields::mst_receipt_statement_coverage::COVERAGE); + } + + pub mod mst_receipt_signature_verified { + use super::*; + pub const VERIFIED: Field = + Field::new(crate::validation::facts::fields::mst_receipt_signature_verified::VERIFIED); + } +} + +impl FactProperties for MstReceiptPresentFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + "present" => Some(FactValue::Bool(self.present)), + _ => None, + } + } +} + +impl FactProperties for MstReceiptTrustedFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + "trusted" => Some(FactValue::Bool(self.trusted)), + _ => None, + } + } +} + +impl FactProperties for MstReceiptIssuerFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + fields::mst_receipt_issuer::ISSUER => Some(FactValue::Str(Cow::Borrowed(&self.issuer))), + _ => None, + } + } +} + +impl FactProperties for MstReceiptKidFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + fields::mst_receipt_kid::KID => Some(FactValue::Str(Cow::Borrowed(&self.kid))), + _ => None, + } + } +} + +impl FactProperties for MstReceiptStatementSha256Fact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + fields::mst_receipt_statement_sha256::SHA256_HEX => { + Some(FactValue::Str(Cow::Borrowed(&self.sha256_hex))) + } + _ => None, + } + } +} + +impl FactProperties for MstReceiptStatementCoverageFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + fields::mst_receipt_statement_coverage::COVERAGE => { + Some(FactValue::Str(Cow::Borrowed(self.coverage))) + } + _ => None, + } + } +} + +impl FactProperties for MstReceiptSignatureVerifiedFact { + /// Return the property value for declarative trust policies. + fn get_property<'a>(&'a self, name: &str) -> Option> { + match name { + fields::mst_receipt_signature_verified::VERIFIED => { + Some(FactValue::Bool(self.verified)) + } + _ => None, + } + } +} diff --git a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs index aded6f21..c0024420 100644 --- a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs +++ b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs @@ -1,737 +1,735 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use cbor_primitives::{CborDecoder, CborEncoder}; -use cose_sign1_primitives::{ - ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message, -}; -use crypto_primitives::{EcJwk, JwkVerifierFactory}; -use serde::Deserialize; -use sha2::{Digest, Sha256}; -use std::borrow::Cow; -use std::sync::Arc; - -// Inline base64url utilities -pub(crate) const BASE64_URL_SAFE: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - -pub(crate) fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { - let mut lookup = [0xFFu8; 256]; - for (i, &c) in alphabet.iter().enumerate() { - lookup[c as usize] = i as u8; - } - - let input = input.trim_end_matches('='); - let mut out = Vec::with_capacity(input.len() * 3 / 4); - let mut buf: u32 = 0; - let mut bits: u32 = 0; - - for &b in input.as_bytes() { - let val = lookup[b as usize]; - if val == 0xFF { - return Err(format!("invalid base64 byte: 0x{:02x}", b)); - } - buf = (buf << 6) | val as u32; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((buf >> bits) as u8); - buf &= (1 << bits) - 1; - } - } - Ok(out) -} - -/// Decode base64url (no padding) to bytes. -pub fn base64url_decode(input: &str) -> Result, String> { - base64_decode(input, BASE64_URL_SAFE) -} - -#[derive(Debug)] -pub enum ReceiptVerifyError { - ReceiptDecode(String), - MissingAlg, - MissingKid, - UnsupportedAlg(i64), - UnsupportedVds(i64), - MissingVdp, - MissingProof, - MissingIssuer, - JwksParse(String), - JwksFetch(String), - JwkNotFound(String), - JwkUnsupported(String), - StatementReencode(String), - SigStructureEncode(String), - DataHashMismatch, - SignatureInvalid, -} - -impl std::fmt::Display for ReceiptVerifyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ReceiptVerifyError::ReceiptDecode(msg) => write!(f, "receipt_decode_failed: {}", msg), - ReceiptVerifyError::MissingAlg => write!(f, "receipt_missing_alg"), - ReceiptVerifyError::MissingKid => write!(f, "receipt_missing_kid"), - ReceiptVerifyError::UnsupportedAlg(alg) => write!(f, "unsupported_alg: {}", alg), - ReceiptVerifyError::UnsupportedVds(vds) => write!(f, "unsupported_vds: {}", vds), - ReceiptVerifyError::MissingVdp => write!(f, "missing_vdp"), - ReceiptVerifyError::MissingProof => write!(f, "missing_proof"), - ReceiptVerifyError::MissingIssuer => write!(f, "issuer_missing"), - ReceiptVerifyError::JwksParse(msg) => write!(f, "jwks_parse_failed: {}", msg), - ReceiptVerifyError::JwksFetch(msg) => write!(f, "jwks_fetch_failed: {}", msg), - ReceiptVerifyError::JwkNotFound(kid) => write!(f, "jwk_not_found_for_kid: {}", kid), - ReceiptVerifyError::JwkUnsupported(msg) => write!(f, "jwk_unsupported: {}", msg), - ReceiptVerifyError::StatementReencode(msg) => { - write!(f, "statement_reencode_failed: {}", msg) - } - ReceiptVerifyError::SigStructureEncode(msg) => { - write!(f, "sig_structure_encode_failed: {}", msg) - } - ReceiptVerifyError::DataHashMismatch => write!(f, "data_hash_mismatch"), - ReceiptVerifyError::SignatureInvalid => write!(f, "signature_invalid"), - } - } -} - -impl std::error::Error for ReceiptVerifyError {} - -/// MST receipt protected header label: 395. -const VDS_HEADER_LABEL: i64 = 395; -/// MST receipt unprotected header label: 396. -const VDP_HEADER_LABEL: i64 = 396; - -/// Receipt proof label inside VDP map: -1. -const PROOF_LABEL: i64 = -1; - -/// CWT (receipt) label for claims: 15. -pub const CWT_CLAIMS_LABEL: i64 = 15; -/// CWT issuer claim label: 1. -pub const CWT_ISS_LABEL: i64 = 1; - -/// COSE alg: ES384. -const COSE_ALG_ES256: i64 = -7; -const COSE_ALG_ES384: i64 = -35; - -/// MST VDS value observed for Microsoft Confidential Ledger receipts. -const MST_VDS_MICROSOFT_CCF: i64 = 2; - -#[derive(Clone)] -pub struct ReceiptVerifyInput<'a> { - pub statement_bytes_with_receipts: &'a [u8], - pub receipt_bytes: &'a [u8], - /// Offline JWKS JSON for Microsoft receipt issuers. - pub offline_jwks_json: Option<&'a str>, - - /// If true, the verifier may fetch JWKS online when offline keys are missing. - pub allow_network_fetch: bool, - - /// Optional api-version query value to use when fetching `/jwks`. - /// The CodeTransparency service typically requires this. - pub jwks_api_version: Option<&'a str>, - - /// Optional Code Transparency client for JWKS fetching. - /// If `None` and `allow_network_fetch` is true, a default client is created. - pub client: Option<&'a code_transparency_client::CodeTransparencyClient>, - - /// Factory for creating crypto verifiers from JWK public keys. - /// Callers pass a backend-specific implementation (e.g., OpenSslJwkVerifierFactory). - pub jwk_verifier_factory: &'a dyn JwkVerifierFactory, -} - -#[derive(Clone, Debug)] -pub struct ReceiptVerifyOutput { - pub trusted: bool, - pub details: Option>, - pub issuer: Arc, - pub kid: Arc, - pub statement_sha256: [u8; 32], -} - -/// Verify a Microsoft Secure Transparency (MST) receipt for a COSE_Sign1 statement. -/// -/// This implements the same high-level verification strategy as the Azure .NET verifier: -/// - Parse the receipt as COSE_Sign1. -/// - Resolve the signing key from JWKS (offline first; optional online fallback). -/// - Re-encode the signed statement with unprotected headers cleared and compute SHA-256. -/// - Validate an inclusion proof whose `data_hash` matches the statement digest. -/// - Verify the receipt signature over the COSE Sig_structure using the CCF accumulator. -pub fn verify_mst_receipt( - input: ReceiptVerifyInput<'_>, -) -> Result { - let receipt = CoseSign1Message::parse(input.receipt_bytes) - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - // Extract receipt headers using typed CoseHeaderMap accessors. - let alg = receipt - .protected - .headers() - .alg() - .or_else(|| receipt.unprotected.headers().alg()) - .ok_or(ReceiptVerifyError::MissingAlg)?; - - let kid_bytes = receipt - .protected - .headers() - .kid() - .or_else(|| receipt.unprotected.headers().kid()) - .ok_or(ReceiptVerifyError::MissingKid)?; - - let kid = std::str::from_utf8(kid_bytes) - .map_err(|_| ReceiptVerifyError::MissingKid)? - .to_string(); - - let vds = receipt - .protected - .get(&CoseHeaderLabel::Int(VDS_HEADER_LABEL)) - .and_then(|v| v.as_i64()) - .ok_or(ReceiptVerifyError::UnsupportedVds(-1))?; - if vds != MST_VDS_MICROSOFT_CCF { - return Err(ReceiptVerifyError::UnsupportedVds(vds)); - } - - let issuer = get_cwt_issuer_host(receipt.protected.headers(), CWT_CLAIMS_LABEL, CWT_ISS_LABEL) - .ok_or(ReceiptVerifyError::MissingIssuer)?; - - // Map the COSE alg early so unsupported alg values are classified as UnsupportedAlg. - validate_cose_alg_supported(alg)?; - - // Resolve the receipt signing key. - // Match the Azure .NET client behavior (GetServiceCertificateKey): - // - Try offline keys first (if provided) - // - If missing and network fallback is allowed, fetch JWKS from https://{issuer}/jwks - // - Lookup key by kid - let jwk = resolve_receipt_signing_key( - issuer.as_str(), - kid.as_str(), - input.offline_jwks_json, - input.allow_network_fetch, - input.jwks_api_version, - input.client, - )?; - validate_receipt_alg_against_jwk(&jwk, alg)?; - - // Convert local Jwk to crypto_primitives::EcJwk for the trait-based factory. - let ec_jwk = local_jwk_to_ec_jwk(&jwk)?; - let verifier = input - .jwk_verifier_factory - .verifier_from_ec_jwk(&ec_jwk, alg) - .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}")))?; - - // Convert to Arc for cheap cloning in fact production. - let issuer: Arc = Arc::from(issuer); - let kid: Arc = Arc::from(kid); - - // VDP is unprotected header label 396. - let vdp_value = receipt - .unprotected - .get(&CoseHeaderLabel::Int(VDP_HEADER_LABEL)) - .ok_or(ReceiptVerifyError::MissingVdp)?; - let proof_blobs = extract_proof_blobs(vdp_value)?; - - // The .NET verifier computes claimsDigest = SHA256(signedStatementBytes) - // where signedStatementBytes is the COSE_Sign1 statement with unprotected headers cleared. - let signed_statement_bytes = - reencode_statement_with_cleared_unprotected_headers(input.statement_bytes_with_receipts)?; - let expected_data_hash = sha256(signed_statement_bytes.as_slice()); - - let mut any_matching_data_hash = false; - for proof_blob in proof_blobs { - let proof = MstCcfInclusionProof::parse(&proof_blob)?; - - // Compute CCF accumulator (leaf hash) and fold proof path. - // If the proof doesn't match this statement, try the next blob. - let mut acc = match ccf_accumulator_sha256(&proof, expected_data_hash) { - Ok(acc) => { - any_matching_data_hash = true; - acc - } - Err(ReceiptVerifyError::DataHashMismatch) => continue, - Err(e) => return Err(e), - }; - for (is_left, sibling) in proof.path.iter() { - acc = if *is_left { - sha256_concat_slices(sibling, &acc) - } else { - sha256_concat_slices(&acc, sibling) - }; - } - - let sig_structure = receipt - .sig_structure_bytes(acc.as_slice(), None) - .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string()))?; - if let Ok(true) = verifier.verify(sig_structure.as_slice(), receipt.signature()) { - return Ok(ReceiptVerifyOutput { - trusted: true, - details: None, - issuer, - kid, - statement_sha256: expected_data_hash, - }); - } - } - - if !any_matching_data_hash { - return Err(ReceiptVerifyError::DataHashMismatch); - } - - Err(ReceiptVerifyError::SignatureInvalid) -} - -/// Compute SHA-256 of `bytes`. -pub fn sha256(bytes: &[u8]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(bytes); - let out = h.finalize(); - out.into() -} - -/// Compute SHA-256 of the concatenation of two fixed-size digests. -pub fn sha256_concat_slices(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(left); - h.update(right); - let out = h.finalize(); - out.into() -} - -/// Re-encode a COSE_Sign1 statement with *all* unprotected headers cleared. -/// -/// MST receipts bind to the SHA-256 of these normalized statement bytes. -pub fn reencode_statement_with_cleared_unprotected_headers( - statement_bytes: &[u8], -) -> Result, ReceiptVerifyError> { - let was_tagged = - is_cose_sign1_tagged_18(statement_bytes).map_err(ReceiptVerifyError::StatementReencode)?; - - let msg = CoseSign1Message::parse(statement_bytes) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // Match .NET verifier behavior: clear *all* unprotected headers. - - // Encode tag(18) if it was present. - let mut enc = cose_sign1_primitives::provider::encoder(); - - if was_tagged { - // tag(18) is a single-byte CBOR tag header: 0xD2. - enc.encode_tag(18) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - } - - enc.encode_array(4) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // protected header bytes are a bstr (containing map bytes) - enc.encode_bstr(msg.protected.as_bytes()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // unprotected header: empty map - enc.encode_map(0) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // payload: bstr / nil - match msg.payload() { - Some(p) => enc.encode_bstr(p), - None => enc.encode_null(), - } - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - // signature: bstr - enc.encode_bstr(msg.signature()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; - - Ok(enc.into_bytes()) -} - -/// Best-effort check for an initial CBOR tag 18 (COSE_Sign1). -pub fn is_cose_sign1_tagged_18(input: &[u8]) -> Result { - let mut d = cose_sign1_primitives::provider::decoder(input); - let typ = d.peek_type().map_err(|e| e.to_string())?; - if typ != cbor_primitives::CborType::Tag { - return Ok(false); - } - let tag = d.decode_tag().map_err(|e| e.to_string())?; - Ok(tag == 18) -} - -/// Resolve the receipt signing key by `kid`, using offline JWKS first and (optionally) online JWKS. -pub(crate) fn resolve_receipt_signing_key( - issuer: &str, - kid: &str, - offline_jwks_json: Option<&str>, - allow_network_fetch: bool, - jwks_api_version: Option<&str>, - client: Option<&code_transparency_client::CodeTransparencyClient>, -) -> Result { - if let Some(jwks_json) = offline_jwks_json { - match find_jwk_for_kid(jwks_json, kid) { - Ok(jwk) => return Ok(jwk), - Err(ReceiptVerifyError::JwkNotFound(_)) => {} - Err(e) => return Err(e), - } - } - - if !allow_network_fetch { - return Err(ReceiptVerifyError::JwksParse( - "MissingOfflineJwks".to_string(), - )); - } - - let jwks_json = fetch_jwks_for_issuer(issuer, jwks_api_version, client)?; - find_jwk_for_kid(jwks_json.as_str(), kid) -} - -/// Fetch the JWKS JSON for a receipt issuer using the Code Transparency client. -pub(crate) fn fetch_jwks_for_issuer( - issuer_host_or_url: &str, - jwks_api_version: Option<&str>, - client: Option<&code_transparency_client::CodeTransparencyClient>, -) -> Result { - if let Some(ct_client) = client { - return ct_client - .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())); - } - - // Create a temporary client for the issuer endpoint - let base = if issuer_host_or_url.contains("://") { - issuer_host_or_url.to_string() - } else { - format!("https://{issuer_host_or_url}") - }; - - let endpoint = - url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string()))?; - - let mut config = code_transparency_client::CodeTransparencyClientConfig::default(); - if let Some(v) = jwks_api_version { - config.api_version = v.to_string(); - } - - let temp_client = code_transparency_client::CodeTransparencyClient::new(endpoint, config); - temp_client - .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())) -} - -#[derive(Clone, Debug)] -pub struct MstCcfInclusionProof { - pub internal_txn_hash: [u8; 32], - pub internal_evidence: String, - pub data_hash: [u8; 32], - pub path: Vec<(bool, [u8; 32])>, -} - -impl MstCcfInclusionProof { - /// Parse an inclusion proof blob into a structured representation. - pub fn parse(proof_blob: &[u8]) -> Result { - Self::parse_impl(proof_blob) - } - - fn parse_impl(proof_blob: &[u8]) -> Result { - let mut d = cose_sign1_primitives::provider::decoder(proof_blob); - let map_len = d - .decode_map_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let mut leaf_raw: Option> = None; - let mut path: Option> = None; - - for _ in 0..map_len.unwrap_or(usize::MAX) { - let k = d - .decode_i64() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - if k == 1 { - leaf_raw = Some( - d.decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(), - ); - } else if k == 2 { - let v_raw = d - .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(); - path = Some(parse_path(&v_raw)?); - } else { - // Skip unknown keys - d.skip() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - } - } - - let leaf_raw = leaf_raw.ok_or(ReceiptVerifyError::MissingProof)?; - let (internal_txn_hash, internal_evidence, data_hash) = parse_leaf(leaf_raw.as_slice())?; - - Ok(Self { - internal_txn_hash, - internal_evidence, - data_hash, - path: path.ok_or(ReceiptVerifyError::MissingProof)?, - }) - } -} - -/// Parse a CCF proof leaf (array) into its components. -pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), ReceiptVerifyError> { - let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); - let _arr_len = d - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let internal_txn_hash_slice = d - .decode_bstr() - .map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) - })?; - let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_internal_txn_hash_len: {}", - internal_txn_hash_slice.len() - )) - })?; - - let internal_evidence = d - .decode_tstr() - .map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_evidence: {}", e)) - })? - .to_string(); - - let data_hash_slice = d - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; - let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_data_hash_len: {}", - data_hash_slice.len() - )) - })?; - - Ok((internal_txn_hash, internal_evidence, data_hash)) -} - -/// Parse a CCF proof path value into a sequence of (direction, sibling_hash) pairs. -pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyError> { - let mut d = cose_sign1_primitives::provider::decoder(bytes); - let arr_len = d - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let mut out = Vec::new(); - for _ in 0..arr_len.unwrap_or(usize::MAX) { - let item_raw = d - .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? - .to_vec(); - let mut vd = cose_sign1_primitives::provider::decoder(&item_raw); - let _pair_len = vd - .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; - - let is_left = vd - .decode_bool() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e)))?; - - let bytes_item = vd - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; - let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_path_hash_len: {}", - bytes_item.len() - )) - })?; - - out.push((is_left, hash)); - } - - Ok(out) -} - -/// Extract proof blobs from the parsed VDP header value (unprotected header 396). -/// -/// The MST receipt places an array of proof blobs under label `-1` in the VDP map. -/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. -pub fn extract_proof_blobs( - vdp_value: &CoseHeaderValue, -) -> Result, ReceiptVerifyError> { - let pairs = match vdp_value { - CoseHeaderValue::Map(pairs) => pairs, - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "vdp_not_a_map".to_string(), - )) - } - }; - - for (label, value) in pairs { - if *label != CoseHeaderLabel::Int(PROOF_LABEL) { - continue; - } - - let arr = match value { - CoseHeaderValue::Array(arr) => arr, - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_not_array".to_string(), - )) - } - }; - - let mut out = Vec::new(); - for item in arr { - match item { - CoseHeaderValue::Bytes(b) => out.push(b.clone()), - _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_item_not_bstr".to_string(), - )) - } - } - } - if out.is_empty() { - return Err(ReceiptVerifyError::MissingProof); - } - return Ok(out); - } - - Err(ReceiptVerifyError::MissingProof) -} - -/// Validate that the COSE alg value is a supported ECDSA algorithm. -pub fn validate_cose_alg_supported(alg: i64) -> Result<(), ReceiptVerifyError> { - match alg { - COSE_ALG_ES256 | COSE_ALG_ES384 => Ok(()), - _ => Err(ReceiptVerifyError::UnsupportedAlg(alg)), - } -} - -/// Validate that the receipt `alg` is compatible with the JWK curve. -pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), ReceiptVerifyError> { - let Some(crv) = jwk.crv.as_deref() else { - return Err(ReceiptVerifyError::JwkUnsupported( - "missing_crv".to_string(), - )); - }; - - let ok = matches!( - (crv, alg), - ("P-256", COSE_ALG_ES256) | ("P-384", COSE_ALG_ES384) - ); - - if !ok { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "alg_curve_mismatch: alg={alg} crv={crv}" - ))); - } - Ok(()) -} - -/// Compute the CCF accumulator (leaf hash) for an inclusion proof. -/// -/// Checks that the proof's `data_hash` matches the statement digest, and then -/// hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. -/// Hash field sizes are guaranteed at parse time via `[u8; 32]` types. -pub fn ccf_accumulator_sha256( - proof: &MstCcfInclusionProof, - expected_data_hash: [u8; 32], -) -> Result<[u8; 32], ReceiptVerifyError> { - if proof.data_hash != expected_data_hash { - return Err(ReceiptVerifyError::DataHashMismatch); - } - - let internal_evidence_hash = sha256(proof.internal_evidence.as_bytes()); - - let mut h = Sha256::new(); - h.update(proof.internal_txn_hash); - h.update(internal_evidence_hash); - h.update(expected_data_hash); - let out = h.finalize(); - Ok(out.into()) -} - -#[derive(Debug, Deserialize)] -struct Jwks { - keys: Vec, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Jwk { - pub kty: String, - pub crv: Option, - pub kid: Option, - pub x: Option, - pub y: Option, -} - -pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result { - let jwks: Jwks = serde_json::from_str(jwks_json) - .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string()))?; - - for k in jwks.keys { - if k.kid.as_deref() == Some(kid) { - return Ok(k); - } - } - - Err(ReceiptVerifyError::JwkNotFound(kid.to_string())) -} - -/// Convert a local (serde-parsed) JWK to a `crypto_primitives::EcJwk`. -/// -/// The local `Jwk` struct comes from JSON JWKS parsing. This function extracts -/// the EC fields needed for the backend-agnostic `JwkVerifierFactory` trait. -pub fn local_jwk_to_ec_jwk<'a>(jwk: &'a Jwk) -> Result, ReceiptVerifyError> { - if jwk.kty != "EC" { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "kty={}", - jwk.kty - ))); - } - - let crv = jwk - .crv - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_crv".to_string()))?; - - let x = jwk - .x - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_x".to_string()))?; - let y = jwk - .y - .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_y".to_string()))?; - - Ok(EcJwk { - kty: Cow::Borrowed(&jwk.kty), - crv: Cow::Borrowed(crv), - x: Cow::Borrowed(x), - y: Cow::Borrowed(y), - kid: jwk.kid.as_deref().map(Cow::Borrowed), - }) -} - -/// Extract the CWT issuer hostname from a protected header's CWT claims map. -/// -/// CWT claims (label `cwt_claims_label`) is a nested CBOR map containing the -/// issuer (label `iss_label`) as a text string. -pub fn get_cwt_issuer_host( - protected: &CoseHeaderMap, - cwt_claims_label: i64, - iss_label: i64, -) -> Option { - let cwt_value = protected.get(&CoseHeaderLabel::Int(cwt_claims_label))?; - match cwt_value { - CoseHeaderValue::Map(pairs) => { - for (label, value) in pairs { - if *label == CoseHeaderLabel::Int(iss_label) { - return value.as_str().map(|s| s.to_string()); - } - } - None - } - _ => None, - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cbor_primitives::{CborDecoder, CborEncoder}; +use cose_sign1_primitives::{ + ArcSlice, CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Message, +}; +use crypto_primitives::{EcJwk, JwkVerifierFactory}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; +use std::sync::Arc; + +// Inline base64url utilities +pub(crate) const BASE64_URL_SAFE: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +pub(crate) fn base64_decode(input: &str, alphabet: &[u8; 64]) -> Result, String> { + let mut lookup = [0xFFu8; 256]; + for (i, &c) in alphabet.iter().enumerate() { + lookup[c as usize] = i as u8; + } + + let input = input.trim_end_matches('='); + let mut out = Vec::with_capacity(input.len() * 3 / 4); + let mut buf: u32 = 0; + let mut bits: u32 = 0; + + for &b in input.as_bytes() { + let val = lookup[b as usize]; + if val == 0xFF { + return Err(format!("invalid base64 byte: 0x{:02x}", b)); + } + buf = (buf << 6) | val as u32; + bits += 6; + if bits >= 8 { + bits -= 8; + out.push((buf >> bits) as u8); + buf &= (1 << bits) - 1; + } + } + Ok(out) +} + +/// Decode base64url (no padding) to bytes. +pub fn base64url_decode(input: &str) -> Result, String> { + base64_decode(input, BASE64_URL_SAFE) +} + +#[derive(Debug)] +pub enum ReceiptVerifyError { + ReceiptDecode(String), + MissingAlg, + MissingKid, + UnsupportedAlg(i64), + UnsupportedVds(i64), + MissingVdp, + MissingProof, + MissingIssuer, + JwksParse(String), + JwksFetch(String), + JwkNotFound(String), + JwkUnsupported(String), + StatementReencode(String), + SigStructureEncode(String), + DataHashMismatch, + SignatureInvalid, +} + +impl std::fmt::Display for ReceiptVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReceiptVerifyError::ReceiptDecode(msg) => write!(f, "receipt_decode_failed: {}", msg), + ReceiptVerifyError::MissingAlg => write!(f, "receipt_missing_alg"), + ReceiptVerifyError::MissingKid => write!(f, "receipt_missing_kid"), + ReceiptVerifyError::UnsupportedAlg(alg) => write!(f, "unsupported_alg: {}", alg), + ReceiptVerifyError::UnsupportedVds(vds) => write!(f, "unsupported_vds: {}", vds), + ReceiptVerifyError::MissingVdp => write!(f, "missing_vdp"), + ReceiptVerifyError::MissingProof => write!(f, "missing_proof"), + ReceiptVerifyError::MissingIssuer => write!(f, "issuer_missing"), + ReceiptVerifyError::JwksParse(msg) => write!(f, "jwks_parse_failed: {}", msg), + ReceiptVerifyError::JwksFetch(msg) => write!(f, "jwks_fetch_failed: {}", msg), + ReceiptVerifyError::JwkNotFound(kid) => write!(f, "jwk_not_found_for_kid: {}", kid), + ReceiptVerifyError::JwkUnsupported(msg) => write!(f, "jwk_unsupported: {}", msg), + ReceiptVerifyError::StatementReencode(msg) => { + write!(f, "statement_reencode_failed: {}", msg) + } + ReceiptVerifyError::SigStructureEncode(msg) => { + write!(f, "sig_structure_encode_failed: {}", msg) + } + ReceiptVerifyError::DataHashMismatch => write!(f, "data_hash_mismatch"), + ReceiptVerifyError::SignatureInvalid => write!(f, "signature_invalid"), + } + } +} + +impl std::error::Error for ReceiptVerifyError {} + +/// MST receipt protected header label: 395. +const VDS_HEADER_LABEL: i64 = 395; +/// MST receipt unprotected header label: 396. +const VDP_HEADER_LABEL: i64 = 396; + +/// Receipt proof label inside VDP map: -1. +const PROOF_LABEL: i64 = -1; + +/// CWT (receipt) label for claims: 15. +pub const CWT_CLAIMS_LABEL: i64 = 15; +/// CWT issuer claim label: 1. +pub const CWT_ISS_LABEL: i64 = 1; + +/// COSE alg: ES384. +const COSE_ALG_ES256: i64 = -7; +const COSE_ALG_ES384: i64 = -35; + +/// MST VDS value observed for Microsoft Confidential Ledger receipts. +const MST_VDS_MICROSOFT_CCF: i64 = 2; + +#[derive(Clone)] +pub struct ReceiptVerifyInput<'a> { + pub statement_bytes_with_receipts: &'a [u8], + pub receipt_bytes: &'a [u8], + /// Offline JWKS JSON for Microsoft receipt issuers. + pub offline_jwks_json: Option<&'a str>, + + /// If true, the verifier may fetch JWKS online when offline keys are missing. + pub allow_network_fetch: bool, + + /// Optional api-version query value to use when fetching `/jwks`. + /// The CodeTransparency service typically requires this. + pub jwks_api_version: Option<&'a str>, + + /// Optional Code Transparency client for JWKS fetching. + /// If `None` and `allow_network_fetch` is true, a default client is created. + pub client: Option<&'a code_transparency_client::CodeTransparencyClient>, + + /// Factory for creating crypto verifiers from JWK public keys. + /// Callers pass a backend-specific implementation (e.g., OpenSslJwkVerifierFactory). + pub jwk_verifier_factory: &'a dyn JwkVerifierFactory, +} + +#[derive(Clone, Debug)] +pub struct ReceiptVerifyOutput { + pub trusted: bool, + pub details: Option>, + pub issuer: Arc, + pub kid: Arc, + pub statement_sha256: [u8; 32], +} + +/// Verify a Microsoft Secure Transparency (MST) receipt for a COSE_Sign1 statement. +/// +/// This implements the same high-level verification strategy as the Azure .NET verifier: +/// - Parse the receipt as COSE_Sign1. +/// - Resolve the signing key from JWKS (offline first; optional online fallback). +/// - Re-encode the signed statement with unprotected headers cleared and compute SHA-256. +/// - Validate an inclusion proof whose `data_hash` matches the statement digest. +/// - Verify the receipt signature over the COSE Sig_structure using the CCF accumulator. +pub fn verify_mst_receipt( + input: ReceiptVerifyInput<'_>, +) -> Result { + let receipt = CoseSign1Message::parse(input.receipt_bytes) + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + // Extract receipt headers using typed CoseHeaderMap accessors. + let alg = receipt + .protected + .headers() + .alg() + .or_else(|| receipt.unprotected.headers().alg()) + .ok_or(ReceiptVerifyError::MissingAlg)?; + + let kid_bytes = receipt + .protected + .headers() + .kid() + .or_else(|| receipt.unprotected.headers().kid()) + .ok_or(ReceiptVerifyError::MissingKid)?; + + let kid = std::str::from_utf8(kid_bytes) + .map_err(|_| ReceiptVerifyError::MissingKid)? + .to_string(); + + let vds = receipt + .protected + .get(&CoseHeaderLabel::Int(VDS_HEADER_LABEL)) + .and_then(|v| v.as_i64()) + .ok_or(ReceiptVerifyError::UnsupportedVds(-1))?; + if vds != MST_VDS_MICROSOFT_CCF { + return Err(ReceiptVerifyError::UnsupportedVds(vds)); + } + + let issuer = get_cwt_issuer_host(receipt.protected.headers(), CWT_CLAIMS_LABEL, CWT_ISS_LABEL) + .ok_or(ReceiptVerifyError::MissingIssuer)?; + + // Map the COSE alg early so unsupported alg values are classified as UnsupportedAlg. + validate_cose_alg_supported(alg)?; + + // Resolve the receipt signing key. + // Match the Azure .NET client behavior (GetServiceCertificateKey): + // - Try offline keys first (if provided) + // - If missing and network fallback is allowed, fetch JWKS from https://{issuer}/jwks + // - Lookup key by kid + let jwk = resolve_receipt_signing_key( + issuer.as_str(), + kid.as_str(), + input.offline_jwks_json, + input.allow_network_fetch, + input.jwks_api_version, + input.client, + )?; + validate_receipt_alg_against_jwk(&jwk, alg)?; + + // Convert local Jwk to crypto_primitives::EcJwk for the trait-based factory. + let ec_jwk = local_jwk_to_ec_jwk(&jwk)?; + let verifier = input + .jwk_verifier_factory + .verifier_from_ec_jwk(&ec_jwk, alg) + .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}")))?; + + // Convert to Arc for cheap cloning in fact production. + let issuer: Arc = Arc::from(issuer); + let kid: Arc = Arc::from(kid); + + // VDP is unprotected header label 396. + let vdp_value = receipt + .unprotected + .get(&CoseHeaderLabel::Int(VDP_HEADER_LABEL)) + .ok_or(ReceiptVerifyError::MissingVdp)?; + let proof_blobs = extract_proof_blobs(vdp_value)?; + + // The .NET verifier computes claimsDigest = SHA256(signedStatementBytes) + // where signedStatementBytes is the COSE_Sign1 statement with unprotected headers cleared. + let signed_statement_bytes = + reencode_statement_with_cleared_unprotected_headers(input.statement_bytes_with_receipts)?; + let expected_data_hash = sha256(signed_statement_bytes.as_slice()); + + let mut any_matching_data_hash = false; + for proof_blob in proof_blobs { + let proof = MstCcfInclusionProof::parse(&proof_blob)?; + + // Compute CCF accumulator (leaf hash) and fold proof path. + // If the proof doesn't match this statement, try the next blob. + let mut acc = match ccf_accumulator_sha256(&proof, expected_data_hash) { + Ok(acc) => { + any_matching_data_hash = true; + acc + } + Err(ReceiptVerifyError::DataHashMismatch) => continue, + Err(e) => return Err(e), + }; + for (is_left, sibling) in proof.path.iter() { + acc = if *is_left { + sha256_concat_slices(sibling, &acc) + } else { + sha256_concat_slices(&acc, sibling) + }; + } + + let sig_structure = receipt + .sig_structure_bytes(acc.as_slice(), None) + .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string()))?; + if let Ok(true) = verifier.verify(sig_structure.as_slice(), receipt.signature()) { + return Ok(ReceiptVerifyOutput { + trusted: true, + details: None, + issuer, + kid, + statement_sha256: expected_data_hash, + }); + } + } + + if !any_matching_data_hash { + return Err(ReceiptVerifyError::DataHashMismatch); + } + + Err(ReceiptVerifyError::SignatureInvalid) +} + +/// Compute SHA-256 of `bytes`. +pub fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(bytes); + let out = h.finalize(); + out.into() +} + +/// Compute SHA-256 of the concatenation of two fixed-size digests. +pub fn sha256_concat_slices(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(left); + h.update(right); + let out = h.finalize(); + out.into() +} + +/// Re-encode a COSE_Sign1 statement with *all* unprotected headers cleared. +/// +/// MST receipts bind to the SHA-256 of these normalized statement bytes. +pub fn reencode_statement_with_cleared_unprotected_headers( + statement_bytes: &[u8], +) -> Result, ReceiptVerifyError> { + let was_tagged = + is_cose_sign1_tagged_18(statement_bytes).map_err(ReceiptVerifyError::StatementReencode)?; + + let msg = CoseSign1Message::parse(statement_bytes) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // Match .NET verifier behavior: clear *all* unprotected headers. + + // Encode tag(18) if it was present. + let mut enc = cose_sign1_primitives::provider::encoder(); + + if was_tagged { + // tag(18) is a single-byte CBOR tag header: 0xD2. + enc.encode_tag(18) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + } + + enc.encode_array(4) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // protected header bytes are a bstr (containing map bytes) + enc.encode_bstr(msg.protected.as_bytes()) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // unprotected header: empty map + enc.encode_map(0) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // payload: bstr / nil + match msg.payload() { + Some(p) => enc.encode_bstr(p), + None => enc.encode_null(), + } + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + // signature: bstr + enc.encode_bstr(msg.signature()) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + + Ok(enc.into_bytes()) +} + +/// Best-effort check for an initial CBOR tag 18 (COSE_Sign1). +pub fn is_cose_sign1_tagged_18(input: &[u8]) -> Result { + let mut d = cose_sign1_primitives::provider::decoder(input); + let typ = d.peek_type().map_err(|e| e.to_string())?; + if typ != cbor_primitives::CborType::Tag { + return Ok(false); + } + let tag = d.decode_tag().map_err(|e| e.to_string())?; + Ok(tag == 18) +} + +/// Resolve the receipt signing key by `kid`, using offline JWKS first and (optionally) online JWKS. +pub(crate) fn resolve_receipt_signing_key( + issuer: &str, + kid: &str, + offline_jwks_json: Option<&str>, + allow_network_fetch: bool, + jwks_api_version: Option<&str>, + client: Option<&code_transparency_client::CodeTransparencyClient>, +) -> Result { + if let Some(jwks_json) = offline_jwks_json { + match find_jwk_for_kid(jwks_json, kid) { + Ok(jwk) => return Ok(jwk), + Err(ReceiptVerifyError::JwkNotFound(_)) => {} + Err(e) => return Err(e), + } + } + + if !allow_network_fetch { + return Err(ReceiptVerifyError::JwksParse( + "MissingOfflineJwks".to_string(), + )); + } + + let jwks_json = fetch_jwks_for_issuer(issuer, jwks_api_version, client)?; + find_jwk_for_kid(jwks_json.as_str(), kid) +} + +/// Fetch the JWKS JSON for a receipt issuer using the Code Transparency client. +pub(crate) fn fetch_jwks_for_issuer( + issuer_host_or_url: &str, + jwks_api_version: Option<&str>, + client: Option<&code_transparency_client::CodeTransparencyClient>, +) -> Result { + if let Some(ct_client) = client { + return ct_client + .get_public_keys() + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())); + } + + // Create a temporary client for the issuer endpoint + let base = if issuer_host_or_url.contains("://") { + issuer_host_or_url.to_string() + } else { + format!("https://{issuer_host_or_url}") + }; + + let endpoint = + url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string()))?; + + let mut config = code_transparency_client::CodeTransparencyClientConfig::default(); + if let Some(v) = jwks_api_version { + config.api_version = v.to_string(); + } + + let temp_client = code_transparency_client::CodeTransparencyClient::new(endpoint, config); + temp_client + .get_public_keys() + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())) +} + +#[derive(Clone, Debug)] +pub struct MstCcfInclusionProof { + pub internal_txn_hash: [u8; 32], + pub internal_evidence: String, + pub data_hash: [u8; 32], + pub path: Vec<(bool, [u8; 32])>, +} + +impl MstCcfInclusionProof { + /// Parse an inclusion proof blob into a structured representation. + pub fn parse(proof_blob: &[u8]) -> Result { + Self::parse_impl(proof_blob) + } + + fn parse_impl(proof_blob: &[u8]) -> Result { + let mut d = cose_sign1_primitives::provider::decoder(proof_blob); + let map_len = d + .decode_map_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let mut leaf_raw: Option> = None; + let mut path: Option> = None; + + for _ in 0..map_len.unwrap_or(usize::MAX) { + let k = d + .decode_i64() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + if k == 1 { + leaf_raw = Some( + d.decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(), + ); + } else if k == 2 { + let v_raw = d + .decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(); + path = Some(parse_path(&v_raw)?); + } else { + // Skip unknown keys + d.skip() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + } + } + + let leaf_raw = leaf_raw.ok_or(ReceiptVerifyError::MissingProof)?; + let (internal_txn_hash, internal_evidence, data_hash) = parse_leaf(leaf_raw.as_slice())?; + + Ok(Self { + internal_txn_hash, + internal_evidence, + data_hash, + path: path.ok_or(ReceiptVerifyError::MissingProof)?, + }) + } +} + +/// Parse a CCF proof leaf (array) into its components. +pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), ReceiptVerifyError> { + let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); + let _arr_len = d + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let internal_txn_hash_slice = d.decode_bstr().map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) + })?; + let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_internal_txn_hash_len: {}", + internal_txn_hash_slice.len() + )) + })?; + + let internal_evidence = d + .decode_tstr() + .map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_evidence: {}", e)) + })? + .to_string(); + + let data_hash_slice = d + .decode_bstr() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; + let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_data_hash_len: {}", + data_hash_slice.len() + )) + })?; + + Ok((internal_txn_hash, internal_evidence, data_hash)) +} + +/// Parse a CCF proof path value into a sequence of (direction, sibling_hash) pairs. +pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyError> { + let mut d = cose_sign1_primitives::provider::decoder(bytes); + let arr_len = d + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let mut out = Vec::new(); + for _ in 0..arr_len.unwrap_or(usize::MAX) { + let item_raw = d + .decode_raw() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .to_vec(); + let mut vd = cose_sign1_primitives::provider::decoder(&item_raw); + let _pair_len = vd + .decode_array_len() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + + let is_left = vd + .decode_bool() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e)))?; + + let bytes_item = vd + .decode_bstr() + .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; + let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { + ReceiptVerifyError::ReceiptDecode(format!( + "unexpected_path_hash_len: {}", + bytes_item.len() + )) + })?; + + out.push((is_left, hash)); + } + + Ok(out) +} + +/// Extract proof blobs from the parsed VDP header value (unprotected header 396). +/// +/// The MST receipt places an array of proof blobs under label `-1` in the VDP map. +/// Returns `ArcSlice` values for zero-copy sharing — cloning is a refcount bump. +pub fn extract_proof_blobs( + vdp_value: &CoseHeaderValue, +) -> Result, ReceiptVerifyError> { + let pairs = match vdp_value { + CoseHeaderValue::Map(pairs) => pairs, + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "vdp_not_a_map".to_string(), + )) + } + }; + + for (label, value) in pairs { + if *label != CoseHeaderLabel::Int(PROOF_LABEL) { + continue; + } + + let arr = match value { + CoseHeaderValue::Array(arr) => arr, + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "proof_not_array".to_string(), + )) + } + }; + + let mut out = Vec::new(); + for item in arr { + match item { + CoseHeaderValue::Bytes(b) => out.push(b.clone()), + _ => { + return Err(ReceiptVerifyError::ReceiptDecode( + "proof_item_not_bstr".to_string(), + )) + } + } + } + if out.is_empty() { + return Err(ReceiptVerifyError::MissingProof); + } + return Ok(out); + } + + Err(ReceiptVerifyError::MissingProof) +} + +/// Validate that the COSE alg value is a supported ECDSA algorithm. +pub fn validate_cose_alg_supported(alg: i64) -> Result<(), ReceiptVerifyError> { + match alg { + COSE_ALG_ES256 | COSE_ALG_ES384 => Ok(()), + _ => Err(ReceiptVerifyError::UnsupportedAlg(alg)), + } +} + +/// Validate that the receipt `alg` is compatible with the JWK curve. +pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), ReceiptVerifyError> { + let Some(crv) = jwk.crv.as_deref() else { + return Err(ReceiptVerifyError::JwkUnsupported( + "missing_crv".to_string(), + )); + }; + + let ok = matches!( + (crv, alg), + ("P-256", COSE_ALG_ES256) | ("P-384", COSE_ALG_ES384) + ); + + if !ok { + return Err(ReceiptVerifyError::JwkUnsupported(format!( + "alg_curve_mismatch: alg={alg} crv={crv}" + ))); + } + Ok(()) +} + +/// Compute the CCF accumulator (leaf hash) for an inclusion proof. +/// +/// Checks that the proof's `data_hash` matches the statement digest, and then +/// hashes `internal_txn_hash || sha256(internal_evidence) || data_hash`. +/// Hash field sizes are guaranteed at parse time via `[u8; 32]` types. +pub fn ccf_accumulator_sha256( + proof: &MstCcfInclusionProof, + expected_data_hash: [u8; 32], +) -> Result<[u8; 32], ReceiptVerifyError> { + if proof.data_hash != expected_data_hash { + return Err(ReceiptVerifyError::DataHashMismatch); + } + + let internal_evidence_hash = sha256(proof.internal_evidence.as_bytes()); + + let mut h = Sha256::new(); + h.update(proof.internal_txn_hash); + h.update(internal_evidence_hash); + h.update(expected_data_hash); + let out = h.finalize(); + Ok(out.into()) +} + +#[derive(Debug, Deserialize)] +struct Jwks { + keys: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Jwk { + pub kty: String, + pub crv: Option, + pub kid: Option, + pub x: Option, + pub y: Option, +} + +pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result { + let jwks: Jwks = serde_json::from_str(jwks_json) + .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string()))?; + + for k in jwks.keys { + if k.kid.as_deref() == Some(kid) { + return Ok(k); + } + } + + Err(ReceiptVerifyError::JwkNotFound(kid.to_string())) +} + +/// Convert a local (serde-parsed) JWK to a `crypto_primitives::EcJwk`. +/// +/// The local `Jwk` struct comes from JSON JWKS parsing. This function extracts +/// the EC fields needed for the backend-agnostic `JwkVerifierFactory` trait. +pub fn local_jwk_to_ec_jwk<'a>(jwk: &'a Jwk) -> Result, ReceiptVerifyError> { + if jwk.kty != "EC" { + return Err(ReceiptVerifyError::JwkUnsupported(format!( + "kty={}", + jwk.kty + ))); + } + + let crv = jwk + .crv + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_crv".to_string()))?; + + let x = jwk + .x + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_x".to_string()))?; + let y = jwk + .y + .as_deref() + .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_y".to_string()))?; + + Ok(EcJwk { + kty: Cow::Borrowed(&jwk.kty), + crv: Cow::Borrowed(crv), + x: Cow::Borrowed(x), + y: Cow::Borrowed(y), + kid: jwk.kid.as_deref().map(Cow::Borrowed), + }) +} + +/// Extract the CWT issuer hostname from a protected header's CWT claims map. +/// +/// CWT claims (label `cwt_claims_label`) is a nested CBOR map containing the +/// issuer (label `iss_label`) as a text string. +pub fn get_cwt_issuer_host( + protected: &CoseHeaderMap, + cwt_claims_label: i64, + iss_label: i64, +) -> Option { + let cwt_value = protected.get(&CoseHeaderLabel::Int(cwt_claims_label))?; + match cwt_value { + CoseHeaderValue::Map(pairs) => { + for (label, value) in pairs { + if *label == CoseHeaderLabel::Int(iss_label) { + return value.as_str().map(|s| s.to_string()); + } + } + None + } + _ => None, + } +} diff --git a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs index 0e773b16..125deddd 100644 --- a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs @@ -13,8 +13,8 @@ extern crate cbor_primitives_everparse; use cbor_primitives::CborEncoder; use cose_sign1_transparent_mst::validation::receipt_verify::*; -use std::borrow::Cow; use crypto_primitives::EcJwk; +use std::borrow::Cow; // ============================================================================ // Target: lines 273-278 — sha256 and sha256_concat_slices diff --git a/native/rust/validation/core/src/message_fact_producer.rs b/native/rust/validation/core/src/message_fact_producer.rs index 1c6954be..435ccf53 100644 --- a/native/rust/validation/core/src/message_fact_producer.rs +++ b/native/rust/validation/core/src/message_fact_producer.rs @@ -245,7 +245,8 @@ fn produce_cwt_claims_from_map( } CoseHeaderLabel::Text(k) => { if let Some(bytes) = value_bytes { - raw_claims_text.insert(Arc::from(k.as_str()), Arc::from(bytes.into_boxed_slice())); + raw_claims_text + .insert(Arc::from(k.as_str()), Arc::from(bytes.into_boxed_slice())); } match (k.as_str(), &value_str, value_i64) { diff --git a/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs b/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs index a0c8d6e1..a149d6f4 100644 --- a/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs +++ b/native/rust/validation/core/tests/message_fact_producer_raw_cwt.rs @@ -208,7 +208,9 @@ fn raw_cwt_nonstandard_int_keys() { assert!(fact.nbf.is_none()); assert!(fact.iat.is_none()); - assert!(matches!(fact.scalar_claims.get(&999), Some(CwtClaimScalar::Str(s)) if &**s == "val999")); + assert!( + matches!(fact.scalar_claims.get(&999), Some(CwtClaimScalar::Str(s)) if &**s == "val999") + ); assert!(matches!( fact.scalar_claims.get(&1000), Some(CwtClaimScalar::I64(42)) diff --git a/native/rust/validation/core/tests/targeted_coverage_gaps.rs b/native/rust/validation/core/tests/targeted_coverage_gaps.rs index e23b3ab8..9d9c1508 100644 --- a/native/rust/validation/core/tests/targeted_coverage_gaps.rs +++ b/native/rust/validation/core/tests/targeted_coverage_gaps.rs @@ -13,11 +13,11 @@ use cbor_primitives_everparse::EverParseCborProvider; use cose_sign1_primitives::payload::Payload; use cose_sign1_primitives::CoseSign1Message; use cose_sign1_validation::fluent::*; -use std::borrow::Cow; use cose_sign1_validation_primitives::facts::{TrustFactEngine, TrustFactSet}; use cose_sign1_validation_primitives::subject::TrustSubject; use cose_sign1_validation_test_utils::SimpleTrustPack; use sha2::Digest; +use std::borrow::Cow; use std::collections::BTreeMap; use std::sync::Arc; From c161056200d4d6d42105e1abb7fd9d20a1ac9ec7 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Tue, 7 Apr 2026 10:05:28 -0700 Subject: [PATCH 6/8] A+ quality: structured error types, FFI opacity, #[must_use], extension pack READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SigningError/FactoryError: String tuple variants -> named Cow<'static, str> struct variants for zero-alloc static messages and self-documenting field names - PayloadTooLargeForEmbedding: positional (u64, u64) -> named { actual, max } - FFI: Remove misleading #[repr(C)] from 6 validation structs embedding Rust types (Vec, Arc, Option) — these are opaque behind *mut pointers, never passed by value - Add #[must_use] to 6 builder/options types: CoseSign1Builder, SigningOptions, DirectSignatureOptions, IndirectSignatureOptions, TrustPolicyBuilder, TrustDecisionAuditBuilder - Add comprehensive README.md for certificates, MST, and AKV extension packs - All 7,886 tests pass, clippy clean, rustfmt clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/signing/signing_service.rs | 25 +- .../extension_packs/azure_key_vault/README.md | 296 +- .../src/signing/akv_signing_service.rs | 35 +- .../extension_packs/certificates/README.md | 299 +- .../signing/certificate_signing_service.rs | 20 +- .../certificate_signing_service_tests.rs | 10 +- native/rust/extension_packs/mst/README.md | 278 +- .../rust/primitives/cose/sign1/src/builder.rs | 1 + native/rust/signing/core/ffi/src/lib.rs | 5880 ++++++++--------- .../ffi/tests/unit_test_internal_types.rs | 10 +- native/rust/signing/core/src/error.rs | 26 +- native/rust/signing/core/src/options.rs | 1 + native/rust/signing/core/src/signer.rs | 17 +- native/rust/signing/core/tests/error_tests.rs | 24 +- .../tests/comprehensive_ffi_new_coverage.rs | 4 +- .../signing/factories/src/direct/factory.rs | 36 +- .../signing/factories/src/direct/options.rs | 1 + native/rust/signing/factories/src/error.rs | 44 +- native/rust/signing/factories/src/factory.rs | 16 +- .../signing/factories/src/indirect/factory.rs | 26 +- .../signing/factories/src/indirect/options.rs | 1 + .../signing/factories/tests/coverage_boost.rs | 29 +- .../factories/tests/deep_factory_coverage.rs | 10 +- .../tests/direct_factory_happy_path.rs | 44 +- .../signing/factories/tests/error_tests.rs | 54 +- .../tests/extensible_factory_test.rs | 23 +- .../signing/factories/tests/factory_tests.rs | 24 +- .../factories/tests/new_factory_coverage.rs | 26 +- native/rust/validation/core/ffi/src/lib.rs | 12 +- .../rust/validation/primitives/ffi/src/lib.rs | 6 +- .../rust/validation/primitives/src/audit.rs | 1 + .../rust/validation/primitives/src/policy.rs | 1 + 32 files changed, 4132 insertions(+), 3148 deletions(-) diff --git a/native/rust/extension_packs/azure_artifact_signing/src/signing/signing_service.rs b/native/rust/extension_packs/azure_artifact_signing/src/signing/signing_service.rs index 05031c7b..0af36a86 100644 --- a/native/rust/extension_packs/azure_artifact_signing/src/signing/signing_service.rs +++ b/native/rust/extension_packs/azure_artifact_signing/src/signing/signing_service.rs @@ -144,8 +144,11 @@ impl AzureArtifactSigningService { #[cfg_attr(coverage_nightly, coverage(off))] pub fn new(options: AzureArtifactSigningOptions) -> Result { let cert_source = Arc::new( - AzureArtifactSigningCertificateSource::new(options.clone()) - .map_err(|e| SigningError::KeyError(e.to_string()))?, + AzureArtifactSigningCertificateSource::new(options.clone()).map_err(|e| { + SigningError::KeyError { + detail: e.to_string().into(), + } + })?, ); Self::from_source(cert_source, options) @@ -159,7 +162,9 @@ impl AzureArtifactSigningService { ) -> Result { let cert_source = Arc::new( AzureArtifactSigningCertificateSource::with_credential(options.clone(), credential) - .map_err(|e| SigningError::KeyError(e.to_string()))?, + .map_err(|e| SigningError::KeyError { + detail: e.to_string().into(), + })?, ); Self::from_source(cert_source, options) @@ -227,13 +232,17 @@ impl AzureArtifactSigningService { cert_source: &AzureArtifactSigningCertificateSource, ) -> Result { // Fetch root certificate to build the chain for DID:x509 - let root_der = cert_source.fetch_root_certificate().map_err(|e| { - SigningError::KeyError(format!("Failed to fetch AAS root cert for DID:x509: {}", e)) - })?; + let root_der = + cert_source + .fetch_root_certificate() + .map_err(|e| SigningError::KeyError { + detail: format!("Failed to fetch AAS root cert for DID:x509: {}", e).into(), + })?; let chain_refs: Vec<&[u8]> = vec![root_der.as_slice()]; - build_did_x509_from_ats_chain(&chain_refs) - .map_err(|e| SigningError::KeyError(format!("AAS DID:x509 generation failed: {}", e))) + build_did_x509_from_ats_chain(&chain_refs).map_err(|e| SigningError::KeyError { + detail: format!("AAS DID:x509 generation failed: {}", e).into(), + }) } } diff --git a/native/rust/extension_packs/azure_key_vault/README.md b/native/rust/extension_packs/azure_key_vault/README.md index b0493488..e881305e 100644 --- a/native/rust/extension_packs/azure_key_vault/README.md +++ b/native/rust/extension_packs/azure_key_vault/README.md @@ -1,49 +1,301 @@ + + # cose_sign1_azure_key_vault -Azure Key Vault COSE signing and validation support pack. +Azure Key Vault signing and validation extension pack for COSE_Sign1. + +## Overview + +This crate provides Azure Key Vault integration for both signing and validating +COSE_Sign1 messages. It enables remote signing with keys stored in Azure Key +Vault or Managed HSM, and validates that messages were signed with AKV-backed +keys via kid (key ID) header inspection. + +Key capabilities: + +- **Remote signing** with Azure Key Vault keys (EC and RSA) +- **Certificate source** for AKV-stored certificates with chain fetching +- **Key ID (kid) trust validation** with configurable allowlists +- **Public key embedding** in COSE protected or unprotected headers +- **Fluent trust policy DSL** for AKV-specific validation rules + +## Architecture + +``` +┌────────────────────────────────────────────────────────┐ +│ cose_sign1_azure_key_vault │ +├─────────────┬──────────────────┬───────────────────────┤ +│ common/ │ signing/ │ validation/ │ +│ ├ AkvKey │ ├ AzureKeyVault │ ├ AzureKeyVault │ +│ │ Client │ │ SigningService│ │ TrustPack │ +│ ├ KeyVault │ ├ AzureKeyVault │ ├ Trust facts │ +│ │ Crypto │ │ SigningKey │ │ (kid-based) │ +│ │ Client │ ├ AzureKeyVault │ └ Fluent DSL │ +│ │ (trait) │ │ Certificate │ extensions │ +│ └ AkvError │ │ Source │ │ +│ │ ├ KeyIdHeader │ │ +│ │ │ Contributor │ │ +│ │ └ CoseKeyHeader │ │ +│ │ Contributor │ │ +├─────────────┴──────────────────┴───────────────────────┤ +│ azure_identity / azure_security_keyvault_keys (SDK) │ +└────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + cose_sign1_signing cose_sign1_validation + cose_sign1_certificates cose_sign1_validation_primitives +``` + +## Modules + +| Module | Description | +|--------|-------------| +| `common` | `KeyVaultCryptoClient` trait, `AkvKeyClient` implementation, `AkvError` | +| `signing` | Signing key, signing service, certificate source, header contributors | +| `validation` | Trust pack, trust facts (kid detection/allowed), fluent DSL extensions | + +## Key Types + +### Common + +- **`KeyVaultCryptoClient`** (trait) — Abstraction over Azure Key Vault crypto operations: sign, key metadata, public key retrieval. +- **`AkvKeyClient`** — Concrete implementation using the Azure SDK `KeyClient`. Supports EC (P-256, P-384, P-521) and RSA keys. +- **`AkvError`** — Error type with variants for crypto failures, key not found, authentication errors, and network issues. -This crate provides Azure Key Vault integration for both signing and validating COSE_Sign1 messages. +### Signing -## Signing +- **`AzureKeyVaultSigningService`** — Implements `SigningService`. Wraps an `AkvKeyClient` to perform remote signing. Automatically contributes kid and optionally embeds the COSE_Key in headers. +- **`AzureKeyVaultSigningKey`** — Implements `CryptoSigner` and `SigningServiceKey`. Signs data by sending digests to Azure Key Vault. Caches the COSE_Key CBOR representation. +- **`AzureKeyVaultCertificateSource`** — Implements `CertificateSource` and `RemoteCertificateSource`. Fetches certificate and chain from AKV, delegates signing to the AKV crypto client. +- **`KeyIdHeaderContributor`** — Implements `HeaderContributor`. Adds the AKV key ID (kid, label 4) to protected headers. +- **`CoseKeyHeaderContributor`** — Implements `HeaderContributor`. Embeds the public COSE_Key at a private-use label (-65537) in protected or unprotected headers. +- **`CoseKeyHeaderLocation`** — Enum: `Protected` (signed) or `Unprotected` (not signed). -The signing module provides Azure Key Vault backed signing services: +### Validation + +- **`AzureKeyVaultTrustPack`** — Implements `CoseSign1TrustPack` and `TrustFactProducer`. Inspects the kid header to detect AKV key IDs and validates them against an allowlist of URL patterns. +- **`AzureKeyVaultTrustOptions`** — Configuration for kid pattern allowlisting and AKV requirement enforcement. +- **`AzureKeyVaultKidDetectedFact`** — Whether the message kid looks like an AKV key identifier. +- **`AzureKeyVaultKidAllowedFact`** — Whether the kid matches an allowed pattern. + +## Usage ### Basic Key Signing ```rust -use cose_sign1_azure_key_vault::signing::{AzureKeyVaultSigningService}; +use cose_sign1_azure_key_vault::signing::AzureKeyVaultSigningService; use cose_sign1_azure_key_vault::common::AkvKeyClient; -use cose_sign1_signing::SigningContext; -use azure_identity::DeveloperToolsCredential; +use cose_sign1_signing::{SigningService, SigningContext}; -// Create AKV client -let client = AkvKeyClient::new_dev("https://myvault.vault.azure.net", "my-key", None)?; +// Create AKV client with developer credentials (local dev) +let client = AkvKeyClient::new_dev( + "https://myvault.vault.azure.net", + "my-signing-key", + None, // latest version +)?; -// Create signing service +// Create and initialize the signing service let mut service = AzureKeyVaultSigningService::new(Box::new(client))?; service.initialize()?; -// Sign a message -let context = SigningContext::new(payload.as_bytes()); +// Get a signer — kid is automatically added to protected headers +let context = SigningContext::new(payload); let signer = service.get_cose_signer(&context)?; -// Use signer with COSE_Sign1 message... ``` -### Certificate-based Signing +### Signing with Service Principal Credentials + +```rust +use cose_sign1_azure_key_vault::common::AkvKeyClient; +use azure_identity::ClientSecretCredential; +use std::sync::Arc; + +let credential = Arc::new(ClientSecretCredential::new( + "tenant-id", + "client-id", + "client-secret", + Default::default(), +)); + +let client = AkvKeyClient::new( + "https://myvault.vault.azure.net", + "my-signing-key", + Some("key-version"), + credential, +)?; +``` + +### Embedding the Public Key in Headers + +```rust +use cose_sign1_azure_key_vault::signing::{ + AzureKeyVaultSigningService, CoseKeyHeaderLocation, +}; + +let mut service = AzureKeyVaultSigningService::new(Box::new(client))?; +service.initialize()?; + +// Embed public key in unprotected headers (not signed) +service.enable_public_key_embedding(CoseKeyHeaderLocation::Unprotected)?; + +// Or embed in protected headers (signed, tamper-proof) +service.enable_public_key_embedding(CoseKeyHeaderLocation::Protected)?; +``` + +### Certificate-Based Signing with AKV ```rust use cose_sign1_azure_key_vault::signing::AzureKeyVaultCertificateSource; -use cose_sign1_certificates::signing::remote::RemoteCertificateSource; +use cose_sign1_certificates::CertificateSource; + +let mut cert_source = AzureKeyVaultCertificateSource::new(Box::new(crypto_client)); -// Create certificate source -let cert_source = AzureKeyVaultCertificateSource::new(Box::new(client)); -let (cert_der, chain_ders) = cert_source.fetch_certificate()?; +// Fetch certificate and chain from Key Vault +let (cert_der, chain) = cert_source.fetch_certificate( + "https://myvault.vault.azure.net", + "my-certificate", + credential, +)?; -// Use with certificate signing service... +// Initialize the source with fetched data +cert_source.initialize(cert_der, chain)?; + +// Use with CertificateSigningService from cose_sign1_certificates +let signing_cert = cert_source.get_signing_certificate()?; ``` -## Validation +### Validating AKV-Signed Messages + +```rust +use cose_sign1_azure_key_vault::validation::{ + AzureKeyVaultTrustPack, AzureKeyVaultTrustOptions, +}; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +// Default options: require AKV kid, allow *.vault.azure.net and *.managedhsm.azure.net +let pack = AzureKeyVaultTrustPack::new(AzureKeyVaultTrustOptions::default()); + +let validator = ValidatorBuilder::new() + .with_trust_pack(Arc::new(pack)) + .build()?; + +let result = validator.validate(&cose_bytes, None)?; +``` + +### Custom KID Allowlist + +```rust +use cose_sign1_azure_key_vault::validation::AzureKeyVaultTrustOptions; + +let options = AzureKeyVaultTrustOptions { + // Only allow keys from a specific vault + allowed_kid_patterns: vec![ + "https://myvault.vault.azure.net/keys/*".into(), + ], + require_azure_key_vault_kid: true, +}; +``` + +### Custom Trust Policies with the Fluent DSL + +```rust +use cose_sign1_azure_key_vault::validation::fluent_ext::*; +use cose_sign1_azure_key_vault::validation::facts::*; +use cose_sign1_validation::fluent::*; + +let plan = TrustPlanBuilder::new(vec![pack.clone()]) + .for_message(|msg| { + msg.require_azure_key_vault_kid() + .require_azure_key_vault_kid_allowed() + }) + .compile()?; +``` + +## Supported Key Types and Algorithms + +| Key Type | Curve / Size | COSE Algorithm | AKV Algorithm | +|----------|-------------|----------------|---------------| +| EC | P-256 | ES256 (-7) | ES256 | +| EC | P-384 | ES384 (-35) | ES384 | +| EC | P-521 | ES512 (-36) | ES512 | +| RSA | 2048+ | PS256 (-37) | PS256 | +| RSA | 2048+ | PS384 (-38) | PS384 | +| RSA | 2048+ | PS512 (-39) | PS512 | + +Algorithm selection is automatic based on the key type and curve stored in +Azure Key Vault. + +## Configuration + +### AzureKeyVaultTrustOptions + +```rust +pub struct AzureKeyVaultTrustOptions { + /// URL patterns for allowed AKV key IDs. + /// Supports wildcards (*) and regex (prefix with "regex:"). + /// Default: ["https://*.vault.azure.net/keys/*", + /// "https://*.managedhsm.azure.net/keys/*"] + pub allowed_kid_patterns: Vec, + /// Require the kid header to be an AKV key identifier. + /// Default: true + pub require_azure_key_vault_kid: bool, +} +``` + +**Pattern matching:** +- `*` matches any characters within a segment +- `?` matches a single character +- Prefix with `regex:` for full regex support + +### AkvKeyClient Constructors + +| Constructor | Authentication | Use Case | +|-------------|---------------|----------| +| `AkvKeyClient::new(url, name, ver, credential)` | Any `TokenCredential` | Production | +| `AkvKeyClient::new_dev(url, name, ver)` | `DeveloperToolsCredential` | Local development | +| `AkvKeyClient::new_with_options(url, name, ver, cred, opts)` | Custom | Advanced configuration | + +## Error Handling + +All AKV operations return `AkvError`: + +```rust +pub enum AkvError { + CryptoOperationFailed(String), + KeyNotFound(String), + InvalidKeyType(String), + AuthenticationFailed(String), + NetworkError(String), + InvalidConfiguration(String), + CertificateSourceError(String), + General(String), +} +``` + +Signing operations wrap `AkvError` into `SigningError` (from `cose_sign1_signing`). +Validation errors are reported through the trust fact system — failed kid +detection or allowlist checks produce facts with `false` values rather than +hard errors. + +## Dependencies + +- `cose_sign1_primitives` — Core COSE types +- `cose_sign1_signing` — Signing service traits +- `cose_sign1_certificates` — Certificate source trait +- `cose_sign1_validation` — Validation framework +- `cose_sign1_validation_primitives` — Trust fact types (with `regex` feature) +- `cose_sign1_crypto_openssl` — OpenSSL crypto provider +- `azure_core` — Azure SDK core (with reqwest + native TLS) +- `azure_identity` — Azure authentication (service principal, developer tools) +- `azure_security_keyvault_keys` — Azure Key Vault keys client +- `tokio` — Async runtime for Azure SDK calls +- `sha2` — Digest computation +- `regex` — KID pattern matching -- `cargo run -p cose_sign1_validation_azure_key_vault --example akv_kid_allowed` +## See Also -Docs: [native/rust/docs/azure-key-vault-pack.md](../docs/azure-key-vault-pack.md). +- [Azure Key Vault Pack documentation](../../docs/azure-key-vault-pack.md) +- [cose_sign1_signing](../../signing/core/) — Signing traits +- [cose_sign1_certificates](../certificates/) — Certificate trust pack (used for AKV certificate signing) +- [cose_sign1_validation](../../validation/core/) — Validation framework diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs index 26656777..a2a07ef3 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs @@ -94,9 +94,11 @@ impl AzureKeyVaultSigningService { /// Checks if the service is initialized. fn ensure_initialized(&self) -> Result<(), SigningError> { if !self.initialized { - return Err(SigningError::InvalidConfiguration( - "Service not initialized. Call initialize() first.".to_string(), - )); + return Err(SigningError::InvalidConfiguration { + detail: std::borrow::Cow::Borrowed( + "Service not initialized. Call initialize() first.", + ), + }); } Ok(()) } @@ -189,15 +191,20 @@ impl SigningService for AzureKeyVaultSigningService { self.ensure_initialized()?; // Parse the COSE_Sign1 message - let msg = cose_sign1_primitives::CoseSign1Message::parse(message_bytes) - .map_err(|e| SigningError::VerificationFailed(format!("failed to parse: {}", e)))?; + let msg = cose_sign1_primitives::CoseSign1Message::parse(message_bytes).map_err(|e| { + SigningError::VerificationFailed { + detail: format!("failed to parse: {}", e).into(), + } + })?; // Get the public key from the signing key let public_key_bytes = self .signing_key .crypto_client() .public_key_bytes() - .map_err(|e| SigningError::VerificationFailed(format!("public key: {}", e)))?; + .map_err(|e| SigningError::VerificationFailed { + detail: format!("public key: {}", e).into(), + })?; // Determine the COSE algorithm from the signing key let algorithm = self.signing_key.algorithm; @@ -207,16 +214,22 @@ impl SigningService for AzureKeyVaultSigningService { &public_key_bytes, algorithm, ) - .map_err(|e| SigningError::VerificationFailed(format!("verifier creation: {}", e)))?; + .map_err(|e| SigningError::VerificationFailed { + detail: format!("verifier creation: {}", e).into(), + })?; // Build sig_structure from the message let payload = msg.payload().unwrap_or_default(); - let sig_structure = msg - .sig_structure_bytes(payload, None) - .map_err(|e| SigningError::VerificationFailed(format!("sig_structure: {}", e)))?; + let sig_structure = msg.sig_structure_bytes(payload, None).map_err(|e| { + SigningError::VerificationFailed { + detail: format!("sig_structure: {}", e).into(), + } + })?; verifier .verify(&sig_structure, msg.signature()) - .map_err(|e| SigningError::VerificationFailed(format!("verify: {}", e))) + .map_err(|e| SigningError::VerificationFailed { + detail: format!("verify: {}", e).into(), + }) } } diff --git a/native/rust/extension_packs/certificates/README.md b/native/rust/extension_packs/certificates/README.md index 26e92758..64db4fd5 100644 --- a/native/rust/extension_packs/certificates/README.md +++ b/native/rust/extension_packs/certificates/README.md @@ -1,13 +1,300 @@ + + # cose_sign1_certificates -Placeholder for certificate-based signing operations. +X.509 certificate trust pack for COSE_Sign1 signing and validation. + +## Overview + +This crate provides both signing and validation capabilities for X.509 +certificate-based COSE signatures. It implements the **CoseSign1TrustPack** +trait for certificate chain validation, and the **SigningService** trait for +signing with X.509 certificate-backed keys. + +Key capabilities: + +- **Certificate-based signing** with automatic x5t/x5chain header injection +- **Certificate chain validation** with configurable trust anchors +- **SCITT compliance** with CWT claims and DID:X509 issuer generation +- **Thumbprint computation** (SHA-256, SHA-384, SHA-512) +- **Fluent trust policy DSL** for declarative certificate validation rules + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ cose_sign1_certificates │ +├──────────────────────┬──────────────────────────────┤ +│ signing/ │ validation/ │ +│ ├ CertificateSigning│ ├ X509CertificateTrustPack │ +│ │ Service │ ├ X509CertificateCoseKey │ +│ ├ CertificateHeader │ │ Resolver │ +│ │ Contributor │ ├ Trust facts (11 types) │ +│ ├ CertificateSource │ └ Fluent DSL extensions │ +│ ├ SigningKeyProvider │ │ +│ └ SCITT compliance │ │ +├──────────────────────┴──────────────────────────────┤ +│ Shared: chain_builder, thumbprint, extensions, │ +│ cose_key_factory, error, chain_sort_order │ +└─────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + cose_sign1_signing cose_sign1_validation + cose_sign1_primitives cose_sign1_validation_primitives +``` + +## Modules + +| Module | Description | +|--------|-------------| +| `signing` | Certificate signing service, header contributors, key providers, SCITT support | +| `validation` | Trust pack, signing key resolver, trust facts, fluent DSL extensions | +| `chain_builder` | `CertificateChainBuilder` trait and `ExplicitCertificateChainBuilder` | +| `thumbprint` | `CoseX509Thumbprint` computation (SHA-256/384/512) | +| `extensions` | x5chain (label 33) and x5t (label 34) header extraction utilities | +| `cose_key_factory` | Create `CryptoVerifier` from X.509 certificate public keys | +| `chain_sort_order` | `X509ChainSortOrder` enum (LeafFirst, RootFirst) | +| `error` | `CertificateError` error type | + +## Key Types + +### Signing + +- **`CertificateSigningService`** — Implements `SigningService` for X.509 certificate-backed signing. Composes a `CertificateSource`, `SigningKeyProvider`, and `CertificateSigningOptions`. +- **`CertificateHeaderContributor`** — Implements `HeaderContributor` to inject x5t (label 34) and x5chain (label 33) into protected headers. +- **`CertificateSigningOptions`** — Configuration for SCITT compliance and custom CWT claims. +- **`CertificateSource`** (trait) — Abstracts certificate sources (local files, remote vaults). +- **`SigningKeyProvider`** (trait) — Extends `CryptoSigner` with `is_remote()` for local vs. remote signing. +- **`CertificateSigningKey`** (trait) — Extends `SigningServiceKey` + `CryptoSigner` with certificate chain access. + +### Validation + +- **`X509CertificateTrustPack`** — Implements `CoseSign1TrustPack`. Produces 11 certificate-related trust facts, resolves signing keys from x5chain, and provides a secure-by-default trust plan. +- **`X509CertificateCoseKeyResolver`** — Implements `CoseKeyResolver`. Extracts the leaf certificate public key from the x5chain header. +- **`CertificateTrustOptions`** — Configuration for identity pinning, embedded chain trust, and PQC algorithm OIDs. + +### Shared + +- **`ExplicitCertificateChainBuilder`** — Pre-built certificate chain (stored via `Arc` for zero-copy cloning). +- **`CoseX509Thumbprint`** — CBOR-serializable thumbprint with algorithm ID and hash bytes. +- **`X509CertificateCoseKeyFactory`** — Creates `CryptoVerifier` instances from DER-encoded certificate public keys. + +## Usage + +### Signing with X.509 Certificates + +```rust +use cose_sign1_certificates::signing::{ + CertificateSigningService, CertificateSigningOptions, + CertificateSource, SigningKeyProvider, +}; +use cose_sign1_signing::{SigningService, SigningContext}; + +// Create a certificate signing service from a source and key provider +let service = CertificateSigningService::new( + certificate_source, // impl CertificateSource + signing_key_provider.into(), // Arc + CertificateSigningOptions::default(), // SCITT enabled by default +); + +// Get a signer for a signing context +let context = SigningContext::new(payload); +let signer = service.get_cose_signer(&context)?; +// signer automatically includes x5t + x5chain in protected headers +``` + +### Configuring Signing Options + +```rust +use cose_sign1_certificates::signing::CertificateSigningOptions; + +// Default: SCITT compliance enabled +let options = CertificateSigningOptions::default(); + +// Custom: disable SCITT, add custom CWT claims +let options = CertificateSigningOptions { + enable_scitt_compliance: false, + custom_cwt_claims: Some(my_cwt_claims), +}; +``` + +### Building Certificate Chains + +```rust +use cose_sign1_certificates::chain_builder::{ + CertificateChainBuilder, ExplicitCertificateChainBuilder, +}; + +// Provide a pre-built chain of DER-encoded certificates +let chain_builder = ExplicitCertificateChainBuilder::new(vec![ + leaf_cert_der.to_vec(), + intermediate_cert_der.to_vec(), + root_cert_der.to_vec(), +]); + +// Build chain from a signing certificate +let chain = chain_builder.build_chain(&signing_cert_der)?; +``` + +### Computing Thumbprints + +```rust +use cose_sign1_certificates::thumbprint::{ + CoseX509Thumbprint, ThumbprintAlgorithm, compute_thumbprint, +}; + +// Compute a SHA-256 thumbprint (default) +let thumbprint = CoseX509Thumbprint::from_cert(&cert_der); + +// Compute with a specific algorithm +let thumbprint = CoseX509Thumbprint::new(&cert_der, ThumbprintAlgorithm::Sha384); + +// Serialize/deserialize for CBOR headers +let bytes = thumbprint.serialize()?; +let restored = CoseX509Thumbprint::deserialize(&bytes)?; + +// Check if a thumbprint matches a certificate +let matches = thumbprint.matches(&other_cert_der)?; +``` + +### Validating with the Certificate Trust Pack + +```rust +use cose_sign1_certificates::validation::{ + X509CertificateTrustPack, CertificateTrustOptions, +}; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +// Create trust pack with default options +let pack = X509CertificateTrustPack::new(CertificateTrustOptions::default()); + +// Or trust embedded chains (deterministic, no OS trust store) +let pack = X509CertificateTrustPack::trust_embedded_chain_as_trusted(); + +// Use the default trust plan (chain trusted + cert valid at now) +let validator = ValidatorBuilder::new() + .with_trust_pack(Arc::new(pack)) + .build()?; + +let result = validator.validate(&cose_bytes, None)?; +``` + +### Custom Trust Policies with the Fluent DSL + +```rust +use cose_sign1_certificates::validation::{ + X509CertificateTrustPack, CertificateTrustOptions, +}; +use cose_sign1_certificates::validation::fluent_ext::*; +use cose_sign1_certificates::validation::facts::*; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +let pack = Arc::new(X509CertificateTrustPack::new( + CertificateTrustOptions::default(), +)); + +let plan = TrustPlanBuilder::new(vec![pack.clone()]) + .for_primary_signing_key(|key| { + key.require_x509_chain_trusted() + .require_signing_certificate_present() + .require::(|w| { + w.issuer_eq("CN=My Issuer") + .cert_valid_at(now_unix_seconds) + }) + }) + .compile()?; +``` + +### Extracting x5chain and x5t from Headers + +```rust +use cose_sign1_certificates::extensions::{ + extract_x5chain, extract_x5t, verify_x5t_matches_chain, + X5CHAIN_LABEL, X5T_LABEL, +}; + +// Extract certificate chain from headers (label 33) +let chain: Vec = extract_x5chain(&message.protected)?; + +// Extract thumbprint from headers (label 34) +let thumbprint: Option = extract_x5t(&message.protected)?; + +// Verify x5t matches the leaf certificate in x5chain +let valid: bool = verify_x5t_matches_chain(&message.protected)?; +``` + +## Trust Facts Produced + +The `X509CertificateTrustPack` produces the following facts during validation: + +| Fact Type | Scope | Description | +|-----------|-------|-------------| +| `X509SigningCertificateIdentityFact` | Signing key | Leaf cert thumbprint, subject, issuer, serial, validity | +| `X509SigningCertificateIdentityAllowedFact` | Signing key | Whether the cert thumbprint is in the allowed list | +| `X509SigningCertificateEkuFact` | Signing key | Extended Key Usage OIDs | +| `X509SigningCertificateKeyUsageFact` | Signing key | Key Usage flags | +| `X509SigningCertificateBasicConstraintsFact` | Signing key | Basic Constraints (CA, path length) | +| `X509ChainElementIdentityFact` | Per-element | Thumbprint, subject, issuer for each chain element | +| `X509ChainElementValidityFact` | Per-element | Validity period for each chain element | +| `X509ChainTrustedFact` | Chain | Whether the chain is trusted, built, status flags | +| `X509PublicKeyAlgorithmFact` | Signing key | Algorithm OID, name, PQC indicator | +| `X509X5ChainCertificateIdentityFact` | Chain | Full x5chain identity details | +| `CertificateSigningKeyTrustFact` | Signing key | Consolidated trust summary | + +## Configuration + +### CertificateTrustOptions + +```rust +pub struct CertificateTrustOptions { + /// Certificate thumbprints allowed for identity pinning. + pub allowed_thumbprints: Vec, + /// Enable identity pinning (restrict to allowed thumbprints). + pub identity_pinning_enabled: bool, + /// Custom OIDs treated as post-quantum cryptography algorithms. + pub pqc_algorithm_oids: Vec, + /// Trust embedded x5chain without OS trust store validation. + /// Deterministic across platforms. + pub trust_embedded_chain_as_trusted: bool, +} +``` + +## Error Handling + +All operations return `CertificateError`: + +```rust +pub enum CertificateError { + NotFound, + InvalidCertificate(String), + ChainBuildFailed(String), + NoPrivateKey, + SigningError(String), +} +``` + +Signing operations return `SigningError` (from `cose_sign1_signing`) which wraps +certificate-specific errors. Validation errors are reported through the +`TrustError` type from `cose_sign1_validation_primitives`. -## Note +## Dependencies -For X.509 certificate validation and trust pack functionality, see -[cose_sign1_validation_certificates](../cose_sign1_validation_certificates/). +- `cose_sign1_primitives` — Core COSE types +- `cose_sign1_signing` — Signing service traits +- `cose_sign1_validation` — Validation framework +- `cose_sign1_validation_primitives` — Trust fact types +- `cose_sign1_crypto_openssl` — OpenSSL crypto provider +- `cbor_primitives` — CBOR serialization +- `did_x509` — DID:X509 issuer generation for SCITT +- `x509-parser` — Certificate parsing +- `openssl` — Cryptographic operations +- `sha2` — Hash algorithms ## See Also -- [Certificate Pack documentation](../docs/certificate-pack.md) -- [cose_sign1_validation_certificates README](../cose_sign1_validation_certificates/README.md) +- [Certificate Pack documentation](../../docs/certificate-pack.md) +- [cose_sign1_signing](../../signing/core/) — Signing traits +- [cose_sign1_validation](../../validation/core/) — Validation framework +- [cose_sign1_certificates_local](local/) — Ephemeral cert generation for testing diff --git a/native/rust/extension_packs/certificates/src/signing/certificate_signing_service.rs b/native/rust/extension_packs/certificates/src/signing/certificate_signing_service.rs index e16ffe17..d635674c 100644 --- a/native/rust/extension_packs/certificates/src/signing/certificate_signing_service.rs +++ b/native/rust/extension_packs/certificates/src/signing/certificate_signing_service.rs @@ -65,11 +65,15 @@ impl SigningService for CertificateSigningService { let cert = self .certificate_source .get_signing_certificate() - .map_err(|e| SigningError::SigningFailed(e.to_string()))?; + .map_err(|e| SigningError::SigningFailed { + detail: e.to_string().into(), + })?; let chain_builder = self.certificate_source.get_chain_builder(); let chain = chain_builder .build_chain(&[]) - .map_err(|e| SigningError::SigningFailed(e.to_string()))?; + .map_err(|e| SigningError::SigningFailed { + detail: e.to_string().into(), + })?; let chain_refs: Vec<&[u8]> = chain.iter().map(|c| c.as_slice()).collect(); // Initialize header maps @@ -81,8 +85,12 @@ impl SigningService for CertificateSigningService { HeaderContributorContext::new(context, &*self.signing_key_provider); // 1. Add certificate headers (x5t + x5chain) to PROTECTED - let cert_contributor = CertificateHeaderContributor::new(cert, &chain_refs) - .map_err(|e| SigningError::SigningFailed(e.to_string()))?; + let cert_contributor = + CertificateHeaderContributor::new(cert, &chain_refs).map_err(|e| { + SigningError::SigningFailed { + detail: e.to_string().into(), + } + })?; cert_contributor.contribute_protected_headers(&mut protected_headers, &contributor_context); @@ -92,7 +100,9 @@ impl SigningService for CertificateSigningService { &chain_refs, self.options.custom_cwt_claims.as_ref(), ) - .map_err(|e| SigningError::SigningFailed(e.to_string()))?; + .map_err(|e| SigningError::SigningFailed { + detail: e.to_string().into(), + })?; scitt_contributor .contribute_protected_headers(&mut protected_headers, &contributor_context); diff --git a/native/rust/extension_packs/certificates/tests/certificate_signing_service_tests.rs b/native/rust/extension_packs/certificates/tests/certificate_signing_service_tests.rs index f5a76f97..676f14c5 100644 --- a/native/rust/extension_packs/certificates/tests/certificate_signing_service_tests.rs +++ b/native/rust/extension_packs/certificates/tests/certificate_signing_service_tests.rs @@ -241,9 +241,9 @@ fn test_get_cose_signer_with_scitt_enabled() { Ok(_) => { // Success case - SCITT contributor was added } - Err(cose_sign1_signing::SigningError::SigningFailed(msg)) => { + Err(cose_sign1_signing::SigningError::SigningFailed { detail }) => { // Expected failure due to mock cert not being valid for DID:X509 - assert!(msg.contains("DID:X509") || msg.contains("Invalid")); + assert!(detail.contains("DID:X509") || detail.contains("Invalid")); } _ => panic!("Unexpected error type"), } @@ -273,7 +273,7 @@ fn test_get_cose_signer_with_custom_cwt_claims() { // Similar to above - testing the code path match result { Ok(_) => {} - Err(cose_sign1_signing::SigningError::SigningFailed(_)) => { + Err(cose_sign1_signing::SigningError::SigningFailed { .. }) => { // Expected due to mock cert } _ => panic!("Unexpected error type"), @@ -314,8 +314,8 @@ fn test_get_cose_signer_certificate_source_failure() { let result = service.get_cose_signer(&context); assert!(result.is_err()); match result { - Err(cose_sign1_signing::SigningError::SigningFailed(msg)) => { - assert!(msg.contains("Mock failure")); + Err(cose_sign1_signing::SigningError::SigningFailed { detail }) => { + assert!(detail.contains("Mock failure")); } _ => panic!("Expected SigningFailed error"), } diff --git a/native/rust/extension_packs/mst/README.md b/native/rust/extension_packs/mst/README.md index 4790e6cb..0861b57b 100644 --- a/native/rust/extension_packs/mst/README.md +++ b/native/rust/extension_packs/mst/README.md @@ -1,9 +1,279 @@ + + # cose_sign1_transparent_mst -Trust pack for Transparent MST receipts. +Microsoft Supply Chain Transparency (MST) extension pack for COSE_Sign1. + +## Overview + +This crate provides validation support for transparent signing receipts emitted +by Microsoft's transparent signing infrastructure, and a transparency provider +that wraps the `code_transparency_client` crate for submitting statements and +retrieving receipts. + +Key capabilities: + +- **Receipt verification** — Verify MST counter-signature receipts embedded in COSE_Sign1 unprotected headers +- **Transparency provider** — Submit signed COSE messages for transparency logging and retrieve receipts +- **Trust pack** — Implements `CoseSign1TrustPack` for receipt-based trust decisions +- **Fluent trust policy DSL** — Declarative receipt validation rules (issuer allowlisting, receipt trust) +- **JWKS key resolution** — Online and offline receipt signing key discovery + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ cose_sign1_transparent_mst │ +├──────────────────────┬──────────────────────────────┤ +│ signing/ │ validation/ │ +│ └ MstTransparency │ ├ MstTrustPack │ +│ Provider │ ├ Receipt verification │ +│ │ ├ JWKS cache │ +│ │ ├ Trust facts (7 types) │ +│ │ ├ Verification options │ +│ │ └ Fluent DSL extensions │ +├──────────────────────┴──────────────────────────────┤ +│ code_transparency_client (Azure SDK) │ +└─────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + cose_sign1_signing cose_sign1_validation + (TransparencyProvider) (CoseSign1TrustPack) +``` + +MST receipts are stored in the COSE_Sign1 unprotected header at **label 394** +as an array of CBOR byte strings. Each receipt is a COSE_Sign1 counter-signature +that binds a statement digest to a transparency service. + +## Modules + +| Module | Description | +|--------|-------------| +| `signing` | `MstTransparencyProvider` — submits statements and verifies receipts via Azure SDK | +| `validation::pack` | `MstTrustPack` — trust pack producing receipt-related facts | +| `validation::facts` | Trust fact types: receipt presence, trust, issuer, kid, coverage | +| `validation::fluent_ext` | Fluent DSL extensions for receipt trust policies | +| `validation::receipt_verify` | Core receipt verification logic (COSE signature + claims) | +| `validation::verification_options` | `CodeTransparencyVerificationOptions` with JWKS cache config | +| `validation::jwks_cache` | JWKS key cache with TTL and persistence | +| `validation::verify` | Static verification entry-point functions | + +## Key Types + +### Signing + +- **`MstTransparencyProvider`** — Implements `TransparencyProvider`. Wraps a `CodeTransparencyClient` to submit COSE_Sign1 bytes for transparency logging and verify returned receipts. + +### Validation + +- **`MstTrustPack`** — Implements `CoseSign1TrustPack` and `TrustFactProducer`. Discovers MST receipts from header label 394, projects each receipt as a counter-signature subject, verifies receipt signatures using JWKS, and emits trust facts. +- **`CodeTransparencyVerificationOptions`** — Controls authorized/unauthorized domain behavior, network JWKS fetching, and offline key pre-seeding. +- **`AuthorizedReceiptBehavior`** — `VerifyAnyMatching`, `VerifyAllMatching`, or `RequireAll` (default). +- **`UnauthorizedReceiptBehavior`** — `VerifyAll` (default), `IgnoreAll`, or `FailIfPresent`. + +## Usage + +### Signing with Transparency + +```rust +use cose_sign1_transparent_mst::signing::MstTransparencyProvider; +use code_transparency_client::{CodeTransparencyClient, CodeTransparencyClientOptions}; +use cose_sign1_signing::transparency::TransparencyProvider; + +// Create a Code Transparency client for the service endpoint +let options = CodeTransparencyClientOptions::default(); +let client = CodeTransparencyClient::new("https://myservice.codetrsp.azure.net", options); + +// Create the transparency provider +let provider = MstTransparencyProvider::new(client); + +// Submit a signed COSE message and get back the message with embedded receipts +let transparent_bytes = provider.add_transparency_proof(&signed_cose_bytes)?; + +// Verify that the receipt is valid +let result = provider.verify_transparency_proof(&transparent_bytes)?; +assert!(result.is_success()); +``` + +### Validating with the MST Trust Pack + +```rust +use cose_sign1_transparent_mst::validation::MstTrustPack; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +// Online mode: fetches JWKS signing keys from receipt issuers +let pack = MstTrustPack::online(); + +// Use the default trust plan (requires a trusted receipt) +let validator = ValidatorBuilder::new() + .with_trust_pack(Arc::new(pack)) + .build()?; + +let result = validator.validate(&cose_bytes_with_receipts, None)?; +``` + +### Offline Verification (No Network) + +```rust +use cose_sign1_transparent_mst::validation::MstTrustPack; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +// Pre-seed JWKS signing keys for offline verification +let jwks_json = r#"{"keys":[...]}"#; +let pack = MstTrustPack::offline_with_jwks(jwks_json); + +let validator = ValidatorBuilder::new() + .with_trust_pack(Arc::new(pack)) + .build()?; + +let result = validator.validate(&cose_bytes, None)?; +``` + +### Custom Trust Policies with the Fluent DSL + +```rust +use cose_sign1_transparent_mst::validation::MstTrustPack; +use cose_sign1_transparent_mst::validation::pack::fluent_ext::*; +use cose_sign1_transparent_mst::validation::facts::*; +use cose_sign1_validation::fluent::*; +use std::sync::Arc; + +let pack = Arc::new(MstTrustPack::online()); + +let plan = TrustPlanBuilder::new(vec![pack.clone()]) + .for_counter_signature(|cs| { + cs.require_mst_receipt_trusted_from_issuer("myservice.codetrsp.azure.net") + }) + .compile()?; +``` + +### Issuer Allowlisting + +```rust +use cose_sign1_transparent_mst::validation::pack::fluent_ext::*; +use cose_sign1_transparent_mst::validation::facts::*; +use cose_sign1_validation::fluent::*; + +// Require a specific issuer domain +let plan = TrustPlanBuilder::new(vec![pack.clone()]) + .for_counter_signature(|cs| { + cs.require::(|w| w.require_receipt_trusted()) + .and() + .require::(|w| { + w.require_receipt_issuer_eq("myservice.codetrsp.azure.net") + }) + }) + .compile()?; +``` + +### Advanced Verification Options + +```rust +use cose_sign1_transparent_mst::validation::verification_options::{ + CodeTransparencyVerificationOptions, + AuthorizedReceiptBehavior, + UnauthorizedReceiptBehavior, +}; +use std::collections::HashMap; + +let options = CodeTransparencyVerificationOptions { + // Only trust receipts from these domains + authorized_domains: vec!["myservice.codetrsp.azure.net".into()], + // All authorized domains must have valid receipts + authorized_receipt_behavior: AuthorizedReceiptBehavior::RequireAll, + // Fail if unauthorized receipts are present + unauthorized_receipt_behavior: UnauthorizedReceiptBehavior::FailIfPresent, + // Allow fetching JWKS from the network + allow_network_fetch: true, + jwks_cache: None, + client_factory: None, +}; + +// Pre-seed offline keys into the options +let options = options.with_offline_keys(HashMap::from([ + ("issuer.example.com".into(), jwks_document), +])); +``` + +## Trust Facts Produced + +The `MstTrustPack` produces the following facts during validation: + +| Fact Type | Scope | Description | +|-----------|-------|-------------| +| `MstReceiptPresentFact` | Counter-signature | Whether an MST receipt is present | +| `MstReceiptTrustedFact` | Counter-signature | Whether the receipt verified successfully | +| `MstReceiptIssuerFact` | Counter-signature | The `iss` claim from the receipt | +| `MstReceiptKidFact` | Counter-signature | The `kid` used to resolve the signing key | +| `MstReceiptStatementSha256Fact` | Counter-signature | SHA-256 digest of the bound statement | +| `MstReceiptStatementCoverageFact` | Counter-signature | Description of what bytes are covered | +| `MstReceiptSignatureVerifiedFact` | Counter-signature | Whether the COSE signature on the receipt verified | + +Additionally, standard counter-signature projection facts are emitted at the message scope: + +| Fact Type | Scope | Description | +|-----------|-------|-------------| +| `CounterSignatureSubjectFact` | Message | Projects each receipt as a counter-signature subject | +| `CounterSignatureSigningKeySubjectFact` | Message | Counter-signature signing key subject | +| `UnknownCounterSignatureBytesFact` | Message | Raw receipt bytes for downstream consumers | +| `CounterSignatureEnvelopeIntegrityFact` | Counter-signature | Envelope integrity check result | + +## Configuration + +### MstTrustPack + +```rust +pub struct MstTrustPack { + /// Allow network JWKS fetching when offline keys are missing. + pub allow_network: bool, + /// Offline JWKS JSON for deterministic verification. + pub offline_jwks_json: Option, + /// Optional api-version for the CodeTransparency /jwks endpoint. + pub jwks_api_version: Option, +} +``` + +**Constructors:** + +| Method | Network | Offline Keys | Use Case | +|--------|---------|-------------|----------| +| `MstTrustPack::online()` | ✅ | None | Production with network access | +| `MstTrustPack::offline_with_jwks(json)` | ❌ | Provided | Air-gapped or test environments | +| `MstTrustPack::new(allow, jwks, api_ver)` | Custom | Custom | Full control | + +## Error Handling + +Receipt verification errors are reported through `ReceiptVerifyError`: + +```rust +pub enum ReceiptVerifyError { + ReceiptDecode(String), + MissingAlg, + UnsupportedVds(String), + // ... additional variants for JWKS, signature, and claims errors +} +``` + +Non-MST receipts (e.g., different VDS types) produce `UnsupportedVds` errors, +which are treated as non-fatal — allowing other trust packs to process their +own receipt types alongside MST receipts. + +## Dependencies -## Example +- `cose_sign1_primitives` — Core COSE types +- `cose_sign1_signing` — `TransparencyProvider` trait +- `cose_sign1_validation` — Validation framework +- `cose_sign1_validation_primitives` — Trust fact types +- `cose_sign1_crypto_openssl` — JWK verification via OpenSSL +- `code_transparency_client` — Azure Code Transparency SDK client +- `sha2` — Statement digest computation +- `serde` / `serde_json` — JWKS document parsing -- `cargo run -p cose_sign1_transparent_mst --example mst_receipt_present` +## See Also -Docs: [native/rust/docs/transparent-mst-pack.md](../docs/transparent-mst-pack.md). +- [Transparent MST Pack documentation](../../docs/transparent-mst-pack.md) +- [cose_sign1_signing](../../signing/core/) — TransparencyProvider trait +- [cose_sign1_validation](../../validation/core/) — Validation framework +- [cose_sign1_certificates](../certificates/) — Certificate trust pack (often combined with MST) diff --git a/native/rust/primitives/cose/sign1/src/builder.rs b/native/rust/primitives/cose/sign1/src/builder.rs index c848c1f2..8666f3e8 100644 --- a/native/rust/primitives/cose/sign1/src/builder.rs +++ b/native/rust/primitives/cose/sign1/src/builder.rs @@ -37,6 +37,7 @@ pub const MAX_EMBED_PAYLOAD_SIZE: u64 = 2 * 1024 * 1024 * 1024; /// .protected(protected) /// .sign(&signer, b"Hello, World!")?; /// ``` +#[must_use = "builders do nothing unless consumed"] #[derive(Clone, Debug, Default)] pub struct CoseSign1Builder { protected: CoseHeaderMap, diff --git a/native/rust/signing/core/ffi/src/lib.rs b/native/rust/signing/core/ffi/src/lib.rs index b0f83043..8a158ccb 100644 --- a/native/rust/signing/core/ffi/src/lib.rs +++ b/native/rust/signing/core/ffi/src/lib.rs @@ -1,2940 +1,2940 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] -#![deny(unsafe_op_in_unsafe_fn)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -//! C/C++ FFI for COSE_Sign1 message signing operations. -//! -//! This crate (`cose_sign1_signing_ffi`) provides FFI-safe wrappers for creating and signing -//! COSE_Sign1 messages from C and C++ code. It uses `cose_sign1_primitives` for types and -//! `cbor_primitives_everparse` for CBOR encoding. -//! -//! For verification operations, see `cose_sign1_primitives_ffi`. -//! -//! ## Error Handling -//! -//! All functions follow a consistent error handling pattern: -//! - Return value: 0 = success, negative = error code -//! - `out_error` parameter: Set to error handle on failure (caller must free) -//! - Output parameters: Only valid if return is 0 -//! -//! ## Memory Management -//! -//! Handles returned by this library must be freed using the corresponding `*_free` function: -//! - `cose_sign1_builder_free` for builder handles -//! - `cose_headermap_free` for header map handles -//! - `cose_key_free` for key handles -//! - `cose_sign1_signing_service_free` for signing service handles -//! - `cose_sign1_factory_free` for factory handles -//! - `cose_sign1_signing_error_free` for error handles -//! - `cose_sign1_string_free` for string pointers -//! - `cose_sign1_bytes_free` for byte buffer pointers -//! - `cose_sign1_cose_bytes_free` for COSE message bytes returned by factory functions -//! -//! ## Thread Safety -//! -//! All handles are thread-safe and can be used from multiple threads. However, handles -//! are not internally synchronized, so concurrent mutation requires external synchronization. - -pub mod error; -pub mod provider; -pub mod types; - -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::ptr; -use std::slice; -use std::sync::Arc; - -use cose_sign1_primitives::{ - CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Builder, CoseSign1Message, - CryptoError, CryptoSigner, -}; - -use crate::error::{ - set_error, ErrorInner, FFI_ERR_FACTORY_FAILED, FFI_ERR_INVALID_ARGUMENT, FFI_ERR_NULL_POINTER, - FFI_ERR_PANIC, FFI_ERR_SIGN_FAILED, FFI_OK, -}; -use crate::types::{ - builder_handle_to_inner_mut, builder_inner_to_handle, factory_handle_to_inner, - factory_inner_to_handle, headermap_handle_to_inner, headermap_handle_to_inner_mut, - headermap_inner_to_handle, key_handle_to_inner, key_inner_to_handle, message_inner_to_handle, - signing_service_handle_to_inner, signing_service_inner_to_handle, BuilderInner, FactoryInner, - HeaderMapInner, KeyInner, MessageInner, SigningServiceInner, -}; - -// Re-export handle types for library users -pub use crate::types::{ - CoseHeaderMapHandle, CoseKeyHandle, CoseSign1BuilderHandle, CoseSign1FactoryHandle, - CoseSign1MessageHandle, CoseSign1SigningServiceHandle, -}; - -// Re-export error types for library users -pub use crate::error::{ - CoseSign1SigningErrorHandle, FFI_ERR_FACTORY_FAILED as COSE_SIGN1_SIGNING_ERR_FACTORY_FAILED, - FFI_ERR_INVALID_ARGUMENT as COSE_SIGN1_SIGNING_ERR_INVALID_ARGUMENT, - FFI_ERR_NULL_POINTER as COSE_SIGN1_SIGNING_ERR_NULL_POINTER, - FFI_ERR_PANIC as COSE_SIGN1_SIGNING_ERR_PANIC, - FFI_ERR_SIGN_FAILED as COSE_SIGN1_SIGNING_ERR_SIGN_FAILED, FFI_OK as COSE_SIGN1_SIGNING_OK, -}; - -pub use crate::error::{ - cose_sign1_signing_error_code, cose_sign1_signing_error_free, cose_sign1_signing_error_message, - cose_sign1_string_free, -}; - -/// ABI version for this library. -/// -/// Increment when making breaking changes to the FFI interface. -pub const ABI_VERSION: u32 = 1; - -/// Returns the ABI version for this library. -#[no_mangle] -pub extern "C" fn cose_sign1_signing_abi_version() -> u32 { - ABI_VERSION -} - -/// Records a panic error and returns the panic status code. -/// This is only reachable when `catch_unwind` catches a panic, which cannot -/// be triggered reliably in tests. -#[cfg_attr(coverage_nightly, coverage(off))] -fn handle_panic(out_error: *mut *mut crate::error::CoseSign1SigningErrorHandle, msg: &str) -> i32 { - set_error(out_error, ErrorInner::new(msg, FFI_ERR_PANIC)); - FFI_ERR_PANIC -} - -/// Writes signed bytes to the caller's output pointers. This path is unreachable -/// through the FFI because SimpleSigningService::verify_signature always returns Err, -/// and the factory mandatorily verifies after signing. -#[cfg_attr(coverage_nightly, coverage(off))] -unsafe fn write_signed_bytes( - bytes: Vec, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, -) -> i32 { - let len = bytes.len(); - let boxed = bytes.into_boxed_slice(); - let raw = Box::into_raw(boxed); - unsafe { - *out_cose_bytes = raw as *mut u8; - *out_cose_len = len as u32; - } - FFI_OK -} - -/// Parses signed COSE bytes into a `CoseSign1MessageHandle` and writes it to the -/// caller's output pointer. -/// -/// On success the handle owns the parsed message; free it with -/// `cose_sign1_message_free` from `cose_sign1_primitives_ffi`. -#[cfg_attr(coverage_nightly, coverage(off))] -unsafe fn write_signed_message( - bytes: Vec, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let _provider = crate::provider::ffi_cbor_provider(); - match CoseSign1Message::parse(&bytes) { - Ok(message) => { - unsafe { - *out_message = message_inner_to_handle(MessageInner { message }); - } - FFI_OK - } - Err(err) => { - set_error( - out_error, - ErrorInner::new( - format!("failed to parse signed message: {}", err), - FFI_ERR_SIGN_FAILED, - ), - ); - FFI_ERR_SIGN_FAILED - } - } -} - -// ============================================================================ -// Header map creation and manipulation -// ============================================================================ - -/// Inner implementation for cose_headermap_new. -pub fn impl_headermap_new_inner(out_headers: *mut *mut CoseHeaderMapHandle) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_headers.is_null() { - return FFI_ERR_NULL_POINTER; - } - - let inner = HeaderMapInner { - headers: CoseHeaderMap::new(), - }; - - unsafe { - *out_headers = headermap_inner_to_handle(inner); - } - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Creates a new empty header map. -/// -/// # Safety -/// -/// - `out_headers` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_headermap_free` -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_new(out_headers: *mut *mut CoseHeaderMapHandle) -> i32 { - impl_headermap_new_inner(out_headers) -} - -/// Inner implementation for cose_headermap_set_int. -pub fn impl_headermap_set_int_inner( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: i64, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { - return FFI_ERR_NULL_POINTER; - }; - - inner - .headers - .insert(CoseHeaderLabel::Int(label), CoseHeaderValue::Int(value)); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets an integer value in a header map by integer label. -/// -/// # Safety -/// -/// - `headers` must be a valid header map handle -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_set_int( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: i64, -) -> i32 { - impl_headermap_set_int_inner(headers, label, value) -} - -/// Inner implementation for cose_headermap_set_bytes. -pub fn impl_headermap_set_bytes_inner( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: *const u8, - value_len: usize, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { - return FFI_ERR_NULL_POINTER; - }; - - if value.is_null() && value_len > 0 { - return FFI_ERR_NULL_POINTER; - } - - let bytes = if value.is_null() { - Vec::new() - } else { - unsafe { slice::from_raw_parts(value, value_len) }.to_vec() - }; - - inner.headers.insert( - CoseHeaderLabel::Int(label), - CoseHeaderValue::Bytes(bytes.into()), - ); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets a byte string value in a header map by integer label. -/// -/// # Safety -/// -/// - `headers` must be a valid header map handle -/// - `value` must be valid for reads of `value_len` bytes -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_set_bytes( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: *const u8, - value_len: usize, -) -> i32 { - impl_headermap_set_bytes_inner(headers, label, value, value_len) -} - -/// Inner implementation for cose_headermap_set_text. -pub fn impl_headermap_set_text_inner( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: *const libc::c_char, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { - return FFI_ERR_NULL_POINTER; - }; - - if value.is_null() { - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(value) }; - let text = match c_str.to_str() { - Ok(s) => s.to_string(), - Err(_) => return FFI_ERR_INVALID_ARGUMENT, - }; - - inner.headers.insert( - CoseHeaderLabel::Int(label), - CoseHeaderValue::Text(text.into()), - ); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets a text string value in a header map by integer label. -/// -/// # Safety -/// -/// - `headers` must be a valid header map handle -/// - `value` must be a valid null-terminated C string -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_set_text( - headers: *mut CoseHeaderMapHandle, - label: i64, - value: *const libc::c_char, -) -> i32 { - impl_headermap_set_text_inner(headers, label, value) -} - -/// Inner implementation for cose_headermap_len. -pub fn impl_headermap_len_inner(headers: *const CoseHeaderMapHandle) -> usize { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { headermap_handle_to_inner(headers) }) else { - return 0; - }; - inner.headers.len() - })); - - result.unwrap_or(0) -} - -/// Returns the number of headers in the map. -/// -/// # Safety -/// -/// - `headers` must be a valid header map handle -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_len(headers: *const CoseHeaderMapHandle) -> usize { - impl_headermap_len_inner(headers) -} - -/// Frees a header map handle. -/// -/// # Safety -/// -/// - `headers` must be a valid header map handle or NULL -/// - The handle must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_headermap_free(headers: *mut CoseHeaderMapHandle) { - if headers.is_null() { - return; - } - unsafe { - drop(Box::from_raw(headers as *mut HeaderMapInner)); - } -} - -// ============================================================================ -// Builder functions -// ============================================================================ - -/// Inner implementation for cose_sign1_builder_new. -pub fn impl_builder_new_inner(out_builder: *mut *mut CoseSign1BuilderHandle) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_builder.is_null() { - return FFI_ERR_NULL_POINTER; - } - - let inner = BuilderInner { - protected: CoseHeaderMap::new(), - unprotected: None, - external_aad: None, - tagged: true, - detached: false, - }; - - unsafe { - *out_builder = builder_inner_to_handle(inner); - } - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Creates a new CoseSign1 message builder. -/// -/// # Safety -/// -/// - `out_builder` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_builder_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_new( - out_builder: *mut *mut CoseSign1BuilderHandle, -) -> i32 { - impl_builder_new_inner(out_builder) -} - -/// Inner implementation for cose_sign1_builder_set_tagged. -pub fn impl_builder_set_tagged_inner(builder: *mut CoseSign1BuilderHandle, tagged: bool) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - inner.tagged = tagged; - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets whether the builder produces tagged COSE_Sign1 output. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_set_tagged( - builder: *mut CoseSign1BuilderHandle, - tagged: bool, -) -> i32 { - impl_builder_set_tagged_inner(builder, tagged) -} - -/// Inner implementation for cose_sign1_builder_set_detached. -pub fn impl_builder_set_detached_inner( - builder: *mut CoseSign1BuilderHandle, - detached: bool, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - inner.detached = detached; - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets whether the builder produces a detached payload. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_set_detached( - builder: *mut CoseSign1BuilderHandle, - detached: bool, -) -> i32 { - impl_builder_set_detached_inner(builder, detached) -} - -/// Inner implementation for cose_sign1_builder_set_protected. -pub fn impl_builder_set_protected_inner( - builder: *mut CoseSign1BuilderHandle, - headers: *const CoseHeaderMapHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - - let Some(hdr_inner) = (unsafe { headermap_handle_to_inner(headers) }) else { - return FFI_ERR_NULL_POINTER; - }; - - builder_inner.protected = hdr_inner.headers.clone(); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets the protected headers for the builder. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -/// - `headers` must be a valid header map handle -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_set_protected( - builder: *mut CoseSign1BuilderHandle, - headers: *const CoseHeaderMapHandle, -) -> i32 { - impl_builder_set_protected_inner(builder, headers) -} - -/// Inner implementation for cose_sign1_builder_set_unprotected. -pub fn impl_builder_set_unprotected_inner( - builder: *mut CoseSign1BuilderHandle, - headers: *const CoseHeaderMapHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - - let Some(hdr_inner) = (unsafe { headermap_handle_to_inner(headers) }) else { - return FFI_ERR_NULL_POINTER; - }; - - builder_inner.unprotected = Some(hdr_inner.headers.clone()); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets the unprotected headers for the builder. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -/// - `headers` must be a valid header map handle -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_set_unprotected( - builder: *mut CoseSign1BuilderHandle, - headers: *const CoseHeaderMapHandle, -) -> i32 { - impl_builder_set_unprotected_inner(builder, headers) -} - -/// Inner implementation for cose_sign1_builder_consume_protected. -pub fn impl_builder_consume_protected_inner( - builder: *mut CoseSign1BuilderHandle, - headers: *mut CoseHeaderMapHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - - if headers.is_null() { - return FFI_ERR_NULL_POINTER; - } - - // Take ownership and move — no clone needed - let hdr_inner = unsafe { Box::from_raw(headers as *mut HeaderMapInner) }; - builder_inner.protected = hdr_inner.headers; - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets the protected headers for the builder by consuming the header map handle. -/// -/// Zero-copy alternative to `cose_sign1_builder_set_protected`. The header map -/// handle is consumed and must NOT be used or freed after this call. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -/// - `headers` must be a valid, owned header map handle (consumed by this call) -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_consume_protected( - builder: *mut CoseSign1BuilderHandle, - headers: *mut CoseHeaderMapHandle, -) -> i32 { - impl_builder_consume_protected_inner(builder, headers) -} - -/// Inner implementation for cose_sign1_builder_consume_unprotected. -pub fn impl_builder_consume_unprotected_inner( - builder: *mut CoseSign1BuilderHandle, - headers: *mut CoseHeaderMapHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - - if headers.is_null() { - return FFI_ERR_NULL_POINTER; - } - - // Take ownership and move — no clone needed - let hdr_inner = unsafe { Box::from_raw(headers as *mut HeaderMapInner) }; - builder_inner.unprotected = Some(hdr_inner.headers); - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets the unprotected headers for the builder by consuming the header map handle. -/// -/// Zero-copy alternative to `cose_sign1_builder_set_unprotected`. The header map -/// handle is consumed and must NOT be used or freed after this call. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -/// - `headers` must be a valid, owned header map handle (consumed by this call) -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_consume_unprotected( - builder: *mut CoseSign1BuilderHandle, - headers: *mut CoseHeaderMapHandle, -) -> i32 { - impl_builder_consume_unprotected_inner(builder, headers) -} - -/// Inner implementation for cose_sign1_builder_set_external_aad. -pub fn impl_builder_set_external_aad_inner( - builder: *mut CoseSign1BuilderHandle, - aad: *const u8, - aad_len: usize, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { - return FFI_ERR_NULL_POINTER; - }; - - if aad.is_null() { - inner.external_aad = None; - } else { - inner.external_aad = Some(unsafe { slice::from_raw_parts(aad, aad_len) }.to_vec()); - } - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Sets the external additional authenticated data for the builder. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle -/// - `aad` must be valid for reads of `aad_len` bytes, or NULL -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_set_external_aad( - builder: *mut CoseSign1BuilderHandle, - aad: *const u8, - aad_len: usize, -) -> i32 { - impl_builder_set_external_aad_inner(builder, aad, aad_len) -} - -/// Inner implementation for cose_sign1_builder_sign (coverable by LLVM). -pub fn impl_builder_sign_inner( - builder: *mut CoseSign1BuilderHandle, - key: *const CoseKeyHandle, - payload: *const u8, - payload_len: usize, - out_bytes: *mut *mut u8, - out_len: *mut usize, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_bytes.is_null() || out_len.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_bytes/out_len")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_bytes = ptr::null_mut(); - *out_len = 0; - } - - if builder.is_null() { - set_error(out_error, ErrorInner::null_pointer("builder")); - return FFI_ERR_NULL_POINTER; - } - - let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { - set_error(out_error, ErrorInner::null_pointer("key")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - // Take ownership of builder - let builder_inner = unsafe { Box::from_raw(builder as *mut BuilderInner) }; - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len) } - }; - - // Move fields out of the consumed builder (no cloning needed) - let mut rust_builder = CoseSign1Builder::new() - .protected(builder_inner.protected) - .tagged(builder_inner.tagged) - .detached(builder_inner.detached); - - if let Some(unprotected) = builder_inner.unprotected { - rust_builder = rust_builder.unprotected(unprotected); - } - - if let Some(aad) = builder_inner.external_aad { - rust_builder = rust_builder.external_aad(aad); - } - - match rust_builder.sign(key_inner.key.as_ref(), payload_bytes) { - Ok(bytes) => { - let len = bytes.len(); - let boxed = bytes.into_boxed_slice(); - let raw = Box::into_raw(boxed); - unsafe { - *out_bytes = raw as *mut u8; - *out_len = len; - } - FFI_OK - } - Err(err) => { - set_error(out_error, ErrorInner::from_cose_error(&err)); - FFI_ERR_SIGN_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during signing"), - } -} - -/// Signs a payload using the builder configuration and a key. -/// -/// The builder is consumed by this call and must not be used afterwards. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle; it is freed on success or failure -/// - `key` must be a valid key handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `out_bytes` and `out_len` must be valid for writes -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_sign( - builder: *mut CoseSign1BuilderHandle, - key: *const CoseKeyHandle, - payload: *const u8, - payload_len: usize, - out_bytes: *mut *mut u8, - out_len: *mut usize, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_builder_sign_inner( - builder, - key, - payload, - payload_len, - out_bytes, - out_len, - out_error, - ) -} - -/// Inner implementation for cose_sign1_builder_sign_to_message. -pub fn impl_builder_sign_to_message_inner( - builder: *mut CoseSign1BuilderHandle, - key: *const CoseKeyHandle, - payload: *const u8, - payload_len: usize, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - if builder.is_null() { - set_error(out_error, ErrorInner::null_pointer("builder")); - return FFI_ERR_NULL_POINTER; - } - - let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { - set_error(out_error, ErrorInner::null_pointer("key")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - // Take ownership of builder - let builder_inner = unsafe { Box::from_raw(builder as *mut BuilderInner) }; - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len) } - }; - - // Move fields out of the consumed builder (no cloning needed) - let mut rust_builder = CoseSign1Builder::new() - .protected(builder_inner.protected) - .tagged(builder_inner.tagged) - .detached(builder_inner.detached); - - if let Some(unprotected) = builder_inner.unprotected { - rust_builder = rust_builder.unprotected(unprotected); - } - - if let Some(aad) = builder_inner.external_aad { - rust_builder = rust_builder.external_aad(aad); - } - - match rust_builder.sign(key_inner.key.as_ref(), payload_bytes) { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error(out_error, ErrorInner::from_cose_error(&err)); - FFI_ERR_SIGN_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during signing"), - } -} - -/// Signs a payload and returns an opaque message handle instead of raw bytes. -/// -/// The returned handle can be inspected with `cose_sign1_message_as_bytes`, -/// `cose_sign1_message_payload`, `cose_sign1_message_signature`, etc. from -/// `cose_sign1_primitives_ffi`, and must be freed with `cose_sign1_message_free`. -/// -/// The builder is consumed by this call and must not be used afterwards. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle; it is freed on success or failure -/// - `key` must be a valid key handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_sign_to_message( - builder: *mut CoseSign1BuilderHandle, - key: *const CoseKeyHandle, - payload: *const u8, - payload_len: usize, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_builder_sign_to_message_inner(builder, key, payload, payload_len, out_message, out_error) -} - -/// Frees a builder handle. -/// -/// # Safety -/// -/// - `builder` must be a valid builder handle or NULL -/// - The handle must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_builder_free(builder: *mut CoseSign1BuilderHandle) { - if builder.is_null() { - return; - } - unsafe { - drop(Box::from_raw(builder as *mut BuilderInner)); - } -} - -/// Frees bytes previously returned by signing operations. -/// -/// # Safety -/// -/// - `bytes` must have been returned by `cose_sign1_builder_sign` or be NULL -/// - `len` must be the length returned alongside the bytes -/// - The bytes must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_bytes_free(bytes: *mut u8, len: usize) { - if bytes.is_null() { - return; - } - unsafe { - drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( - bytes, len, - ))); - } -} - -// ============================================================================ -// Key creation via callback -// ============================================================================ - -/// Callback function type for signing operations. -/// -/// The callback receives the complete Sig_structure (RFC 9052) that needs to be signed. -/// -/// # Parameters -/// -/// - `sig_structure`: The CBOR-encoded Sig_structure bytes to sign -/// - `sig_structure_len`: Length of sig_structure -/// - `out_sig`: Output pointer for signature bytes (caller frees with libc::free) -/// - `out_sig_len`: Output pointer for signature length -/// - `user_data`: User-provided context pointer -/// -/// # Returns -/// -/// - `0` on success -/// - Non-zero on error -pub type CoseSignCallback = unsafe extern "C" fn( - sig_structure: *const u8, - sig_structure_len: usize, - out_sig: *mut *mut u8, - out_sig_len: *mut usize, - user_data: *mut libc::c_void, -) -> i32; - -/// Inner implementation for cose_key_from_callback. -pub fn impl_key_from_callback_inner( - algorithm: i64, - key_type: *const libc::c_char, - sign_fn: CoseSignCallback, - user_data: *mut libc::c_void, - out_key: *mut *mut CoseKeyHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_key.is_null() { - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_key = ptr::null_mut(); - } - - if key_type.is_null() { - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(key_type) }; - let key_type_str = match c_str.to_str() { - Ok(s) => s.to_string(), - Err(_) => return FFI_ERR_INVALID_ARGUMENT, - }; - - let callback_key = CallbackKey { - algorithm, - key_type: key_type_str, - sign_fn, - user_data, - }; - - let inner = KeyInner { - key: std::sync::Arc::new(callback_key), - }; - - unsafe { - *out_key = key_inner_to_handle(inner); - } - FFI_OK - })); - - result.unwrap_or(FFI_ERR_PANIC) -} - -/// Creates a key handle from a signing callback. -/// -/// # Safety -/// -/// - `key_type` must be a valid null-terminated C string -/// - `sign_fn` must be a valid function pointer -/// - `out_key` must be valid for writes -/// - `user_data` must remain valid for the lifetime of the key handle -/// - Caller owns the returned handle and must free it with `cose_key_free` -#[no_mangle] -pub unsafe extern "C" fn cose_key_from_callback( - algorithm: i64, - key_type: *const libc::c_char, - sign_fn: CoseSignCallback, - user_data: *mut libc::c_void, - out_key: *mut *mut CoseKeyHandle, -) -> i32 { - impl_key_from_callback_inner(algorithm, key_type, sign_fn, user_data, out_key) -} - -/// Frees a key handle. -/// -/// # Safety -/// -/// - `key` must be a valid key handle or NULL -/// - The handle must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_key_free(key: *mut CoseKeyHandle) { - if key.is_null() { - return; - } - unsafe { - drop(Box::from_raw(key as *mut KeyInner)); - } -} - -// ============================================================================ -// Signing Service and Factory functions -// ============================================================================ - -/// Inner implementation for cose_sign1_signing_service_create. -pub fn impl_signing_service_create_inner( - key: *const CoseKeyHandle, - out_service: *mut *mut CoseSign1SigningServiceHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_service.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_service")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_service = ptr::null_mut(); - } - - let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { - set_error(out_error, ErrorInner::null_pointer("key")); - return FFI_ERR_NULL_POINTER; - }; - - let service = SimpleSigningService::new(key_inner.key.clone()); - let inner = SigningServiceInner { - service: std::sync::Arc::new(service), - }; - - unsafe { - *out_service = signing_service_inner_to_handle(inner); - } - FFI_OK - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during signing service creation"), - } -} - -/// Creates a signing service from a key handle. -/// -/// # Safety -/// -/// - `key` must be a valid key handle -/// - `out_service` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_signing_service_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_signing_service_create( - key: *const CoseKeyHandle, - out_service: *mut *mut CoseSign1SigningServiceHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_signing_service_create_inner(key, out_service, out_error) -} - -/// Frees a signing service handle. -/// -/// # Safety -/// -/// - `service` must be a valid signing service handle or NULL -/// - The handle must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_signing_service_free( - service: *mut CoseSign1SigningServiceHandle, -) { - if service.is_null() { - return; - } - unsafe { - drop(Box::from_raw(service as *mut SigningServiceInner)); - } -} - -// ============================================================================ -// CryptoSigner-based signing service creation -// ============================================================================ - -/// Opaque handle type for CryptoSigner (from cose_sign1_crypto_openssl_ffi). -/// This is the same type as `cose_crypto_signer_t` from crypto_openssl_ffi. -#[repr(C)] -pub struct CryptoSignerHandle { - _private: [u8; 0], -} - -/// Inner implementation for cose_sign1_signing_service_from_crypto_signer. -pub fn impl_signing_service_from_crypto_signer_inner( - signer_handle: *mut CryptoSignerHandle, - out_service: *mut *mut CoseSign1SigningServiceHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_service.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_service")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_service = ptr::null_mut(); - } - - if signer_handle.is_null() { - set_error(out_error, ErrorInner::null_pointer("signer_handle")); - return FFI_ERR_NULL_POINTER; - } - - let signer_box = unsafe { - Box::from_raw(signer_handle as *mut Box) - }; - let signer_arc: std::sync::Arc = (*signer_box).into(); - - let service = SimpleSigningService::new(signer_arc); - let inner = SigningServiceInner { - service: std::sync::Arc::new(service), - }; - - unsafe { - *out_service = signing_service_inner_to_handle(inner); - } - FFI_OK - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic( - out_error, - "panic during signing service creation from crypto signer", - ), - } -} - -/// Creates a signing service from a CryptoSigner handle. -/// -/// This eliminates the need for `cose_key_from_callback`. -/// The signer handle comes from `cose_crypto_openssl_signer_from_der` (or similar). -/// Ownership of the signer handle is transferred to the signing service. -/// -/// # Safety -/// -/// - `signer_handle` must be a valid CryptoSigner handle (from crypto_openssl_ffi) -/// - `out_service` must be valid for writes -/// - `signer_handle` must not be used after this call (ownership transferred) -/// - Caller owns the returned handle and must free it with `cose_sign1_signing_service_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_signing_service_from_crypto_signer( - signer_handle: *mut CryptoSignerHandle, - out_service: *mut *mut CoseSign1SigningServiceHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_signing_service_from_crypto_signer_inner(signer_handle, out_service, out_error) -} - -/// Inner implementation for cose_sign1_factory_from_crypto_signer. -pub fn impl_factory_from_crypto_signer_inner( - signer_handle: *mut CryptoSignerHandle, - out_factory: *mut *mut CoseSign1FactoryHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_factory.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_factory")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_factory = ptr::null_mut(); - } - - if signer_handle.is_null() { - set_error(out_error, ErrorInner::null_pointer("signer_handle")); - return FFI_ERR_NULL_POINTER; - } - - let signer_box = unsafe { - Box::from_raw(signer_handle as *mut Box) - }; - let signer_arc: std::sync::Arc = (*signer_box).into(); - - let service = SimpleSigningService::new(signer_arc); - let service_arc = std::sync::Arc::new(service); - - let factory = cose_sign1_factories::CoseSign1MessageFactory::new(service_arc); - - let inner = FactoryInner { factory }; - - unsafe { - *out_factory = factory_inner_to_handle(inner); - } - FFI_OK - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic( - out_error, - "panic during factory creation from crypto signer", - ), - } -} - -/// Creates a signature factory directly from a CryptoSigner handle. -/// -/// This combines `cose_sign1_signing_service_from_crypto_signer` and -/// `cose_sign1_factory_create` in a single call for convenience. -/// Ownership of the signer handle is transferred to the factory. -/// -/// # Safety -/// -/// - `signer_handle` must be a valid CryptoSigner handle (from crypto_openssl_ffi) -/// - `out_factory` must be valid for writes -/// - `signer_handle` must not be used after this call (ownership transferred) -/// - Caller owns the returned handle and must free it with `cose_sign1_factory_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_from_crypto_signer( - signer_handle: *mut CryptoSignerHandle, - out_factory: *mut *mut CoseSign1FactoryHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_from_crypto_signer_inner(signer_handle, out_factory, out_error) -} - -/// Inner implementation for cose_sign1_factory_create. -pub fn impl_factory_create_inner( - service: *const CoseSign1SigningServiceHandle, - out_factory: *mut *mut CoseSign1FactoryHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_factory.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_factory")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_factory = ptr::null_mut(); - } - - let Some(service_inner) = (unsafe { signing_service_handle_to_inner(service) }) else { - set_error(out_error, ErrorInner::null_pointer("service")); - return FFI_ERR_NULL_POINTER; - }; - - let factory = - cose_sign1_factories::CoseSign1MessageFactory::new(service_inner.service.clone()); - let inner = FactoryInner { factory }; - - unsafe { - *out_factory = factory_inner_to_handle(inner); - } - FFI_OK - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during factory creation"), - } -} - -/// Creates a factory from a signing service handle. -/// -/// # Safety -/// -/// - `service` must be a valid signing service handle -/// - `out_factory` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_factory_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_create( - service: *const CoseSign1SigningServiceHandle, - out_factory: *mut *mut CoseSign1FactoryHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_create_inner(service, out_factory, out_error) -} - -/// Inner implementation for cose_sign1_factory_sign_direct. -pub fn impl_factory_sign_direct_inner( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len as usize) } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - match factory_inner - .factory - .create_direct_bytes(payload_bytes, content_type_str, None) - { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during direct signing"), - } -} - -/// Signs payload with direct signature (embedded payload). -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_inner( - factory, - payload, - payload_len, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect. -pub fn impl_factory_sign_indirect_inner( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len as usize) } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - match factory_inner - .factory - .create_indirect_bytes(payload_bytes, content_type_str, None) - { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during indirect signing"), - } -} - -/// Signs payload with indirect signature (hash envelope). -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_inner( - factory, - payload, - payload_len, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -// ============================================================================ -// Streaming signature functions -// ============================================================================ - -/// Callback type for streaming payload reading. -/// -/// The callback is invoked repeatedly with a buffer to fill. -/// Returns the number of bytes read (0 = EOF), or negative on error. -/// -/// # Safety -/// -/// - `buffer` must be valid for writes of `buffer_len` bytes -/// - `user_data` is the opaque pointer passed to the signing function -pub type CoseReadCallback = - unsafe extern "C" fn(buffer: *mut u8, buffer_len: usize, user_data: *mut libc::c_void) -> i64; - -/// Adapter for callback-based streaming payload. -struct CallbackStreamingPayload { - callback: CoseReadCallback, - user_data: *mut libc::c_void, - total_len: u64, -} - -// SAFETY: The callback is assumed to be thread-safe. -// FFI callers are responsible for ensuring thread safety. -unsafe impl Send for CallbackStreamingPayload {} -unsafe impl Sync for CallbackStreamingPayload {} - -impl cose_sign1_primitives::StreamingPayload for CallbackStreamingPayload { - fn size(&self) -> u64 { - self.total_len - } - - fn open( - &self, - ) -> Result< - Box, - cose_sign1_primitives::error::PayloadError, - > { - Ok(Box::new(CallbackReader { - callback: self.callback, - user_data: self.user_data, - total_len: self.total_len, - bytes_read: 0, - })) - } -} - -/// Reader implementation that wraps the callback. -struct CallbackReader { - callback: CoseReadCallback, - user_data: *mut libc::c_void, - total_len: u64, - bytes_read: u64, -} - -// SAFETY: The callback is assumed to be thread-safe. -// FFI callers are responsible for ensuring thread safety. -unsafe impl Send for CallbackReader {} - -impl std::io::Read for CallbackReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - if self.bytes_read >= self.total_len { - return Ok(0); - } - - let remaining = (self.total_len - self.bytes_read) as usize; - let to_read = buf.len().min(remaining); - - let result = unsafe { (self.callback)(buf.as_mut_ptr(), to_read, self.user_data) }; - - if result < 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("callback read error: {}", result), - )); - } - - let bytes_read = result as usize; - self.bytes_read += bytes_read as u64; - Ok(bytes_read) - } -} - -impl cose_sign1_primitives::sig_structure::SizedRead for CallbackReader { - fn len(&self) -> Result { - Ok(self.total_len) - } -} - -/// Inner implementation for cose_sign1_factory_sign_direct_file. -pub fn impl_factory_sign_direct_file_inner( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if file_path.is_null() { - set_error(out_error, ErrorInner::null_pointer("file_path")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; - let path_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - // Create FilePayload - let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { - Ok(p) => p, - Err(e) => { - set_error( - out_error, - ErrorInner::new( - format!("failed to open file: {}", e), - FFI_ERR_INVALID_ARGUMENT, - ), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let payload_arc: Arc = Arc::new(file_payload); - - // Create options with detached=true - let options = cose_sign1_factories::direct::DirectSignatureOptions { - embed_payload: false, // Force detached for streaming - ..Default::default() - }; - - match factory_inner.factory.create_direct_streaming_bytes( - payload_arc, - content_type_str, - Some(options), - ) { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during file signing"), - } -} - -/// Signs a file directly without loading it into memory (direct signature). -/// -/// Creates a detached COSE_Sign1 signature over the file content. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `file_path` must be a valid null-terminated UTF-8 string -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct_file( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_file_inner( - factory, - file_path, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect_file. -pub fn impl_factory_sign_indirect_file_inner( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if file_path.is_null() { - set_error(out_error, ErrorInner::null_pointer("file_path")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; - let path_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - // Create FilePayload - let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { - Ok(p) => p, - Err(e) => { - set_error( - out_error, - ErrorInner::new( - format!("failed to open file: {}", e), - FFI_ERR_INVALID_ARGUMENT, - ), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let payload_arc: Arc = Arc::new(file_payload); - - match factory_inner.factory.create_indirect_streaming_bytes( - payload_arc, - content_type_str, - None, - ) { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during file signing"), - } -} - -/// Signs a file directly without loading it into memory (indirect signature). -/// -/// Creates a detached COSE_Sign1 signature over the file content hash. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `file_path` must be a valid null-terminated UTF-8 string -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_file( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_file_inner( - factory, - file_path, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_direct_streaming. -#[allow(clippy::too_many_arguments)] -pub fn impl_factory_sign_direct_streaming_inner( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - // Create callback payload - let callback_payload = CallbackStreamingPayload { - callback: read_callback, - user_data, - total_len: payload_len, - }; - - let payload_arc: Arc = - Arc::new(callback_payload); - - // Create options with detached=true - let options = cose_sign1_factories::direct::DirectSignatureOptions { - embed_payload: false, // Force detached for streaming - ..Default::default() - }; - - match factory_inner.factory.create_direct_streaming_bytes( - payload_arc, - content_type_str, - Some(options), - ) { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during streaming signing"), - } -} - -/// Signs with a streaming payload via callback (direct signature). -/// -/// The callback is invoked repeatedly with a buffer to fill. -/// payload_len must be the total payload size (for CBOR bstr header). -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `read_callback` must be a valid callback function -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct_streaming( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_streaming_inner( - factory, - read_callback, - payload_len, - user_data, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect_streaming. -#[allow(clippy::too_many_arguments)] -pub fn impl_factory_sign_indirect_streaming_inner( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_cose_bytes.is_null() || out_cose_len.is_null() { - set_error( - out_error, - ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), - ); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_cose_bytes = ptr::null_mut(); - *out_cose_len = 0; - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - // Create callback payload - let callback_payload = CallbackStreamingPayload { - callback: read_callback, - user_data, - total_len: payload_len, - }; - - let payload_arc: Arc = - Arc::new(callback_payload); - - match factory_inner.factory.create_indirect_streaming_bytes( - payload_arc, - content_type_str, - None, - ) { - Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during streaming signing"), - } -} - -/// Signs with a streaming payload via callback (indirect signature). -/// -/// The callback is invoked repeatedly with a buffer to fill. -/// payload_len must be the total payload size (for CBOR bstr header). -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `read_callback` must be a valid callback function -/// - `content_type` must be a valid null-terminated C string -/// - `out_cose_bytes` and `out_cose_len` must be valid for writes -/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_streaming( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_cose_bytes: *mut *mut u8, - out_cose_len: *mut u32, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_streaming_inner( - factory, - read_callback, - payload_len, - user_data, - content_type, - out_cose_bytes, - out_cose_len, - out_error, - ) -} - -// ============================================================================ -// Factory _to_message variants — return CoseSign1MessageHandle -// ============================================================================ - -/// Inner implementation for cose_sign1_factory_sign_direct_to_message. -pub fn impl_factory_sign_direct_to_message_inner( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len as usize) } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - match factory_inner - .factory - .create_direct_bytes(payload_bytes, content_type_str, None) - { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during direct signing"), - } -} - -/// Signs payload with direct signature, returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct_to_message( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_to_message_inner( - factory, - payload, - payload_len, - content_type, - out_message, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect_to_message. -pub fn impl_factory_sign_indirect_to_message_inner( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if payload.is_null() && payload_len > 0 { - set_error(out_error, ErrorInner::null_pointer("payload")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let payload_bytes = if payload.is_null() { - &[] as &[u8] - } else { - unsafe { slice::from_raw_parts(payload, payload_len as usize) } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - match factory_inner - .factory - .create_indirect_bytes(payload_bytes, content_type_str, None) - { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during indirect signing"), - } -} - -/// Signs payload with indirect signature, returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `payload` must be valid for reads of `payload_len` bytes -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_to_message( - factory: *const CoseSign1FactoryHandle, - payload: *const u8, - payload_len: u32, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_to_message_inner( - factory, - payload, - payload_len, - content_type, - out_message, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_direct_file_to_message. -pub fn impl_factory_sign_direct_file_to_message_inner( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if file_path.is_null() { - set_error(out_error, ErrorInner::null_pointer("file_path")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; - let path_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { - Ok(p) => p, - Err(e) => { - set_error( - out_error, - ErrorInner::new( - format!("failed to open file: {}", e), - FFI_ERR_INVALID_ARGUMENT, - ), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let payload_arc: Arc = Arc::new(file_payload); - - let options = cose_sign1_factories::direct::DirectSignatureOptions { - embed_payload: false, - ..Default::default() - }; - - match factory_inner.factory.create_direct_streaming_bytes( - payload_arc, - content_type_str, - Some(options), - ) { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during file signing"), - } -} - -/// Signs a file directly, returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `file_path` must be a valid null-terminated UTF-8 string -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct_file_to_message( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_file_to_message_inner( - factory, - file_path, - content_type, - out_message, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect_file_to_message. -pub fn impl_factory_sign_indirect_file_to_message_inner( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if file_path.is_null() { - set_error(out_error, ErrorInner::null_pointer("file_path")); - return FFI_ERR_NULL_POINTER; - } - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; - let path_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { - Ok(p) => p, - Err(e) => { - set_error( - out_error, - ErrorInner::new( - format!("failed to open file: {}", e), - FFI_ERR_INVALID_ARGUMENT, - ), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let payload_arc: Arc = Arc::new(file_payload); - - match factory_inner.factory.create_indirect_streaming_bytes( - payload_arc, - content_type_str, - None, - ) { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during file signing"), - } -} - -/// Signs a file with indirect signature, returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `file_path` must be a valid null-terminated UTF-8 string -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_file_to_message( - factory: *const CoseSign1FactoryHandle, - file_path: *const libc::c_char, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_file_to_message_inner( - factory, - file_path, - content_type, - out_message, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_direct_streaming_to_message. -pub fn impl_factory_sign_direct_streaming_to_message_inner( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let callback_payload = CallbackStreamingPayload { - callback: read_callback, - user_data, - total_len: payload_len, - }; - - let payload_arc: Arc = - Arc::new(callback_payload); - - let options = cose_sign1_factories::direct::DirectSignatureOptions { - embed_payload: false, - ..Default::default() - }; - - match factory_inner.factory.create_direct_streaming_bytes( - payload_arc, - content_type_str, - Some(options), - ) { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during streaming signing"), - } -} - -/// Signs with a streaming payload via callback (direct), returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `read_callback` must be a valid callback function -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_direct_streaming_to_message( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_direct_streaming_to_message_inner( - factory, - read_callback, - payload_len, - user_data, - content_type, - out_message, - out_error, - ) -} - -/// Inner implementation for cose_sign1_factory_sign_indirect_streaming_to_message. -pub fn impl_factory_sign_indirect_streaming_to_message_inner( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - let result = catch_unwind(AssertUnwindSafe(|| { - if out_message.is_null() { - set_error(out_error, ErrorInner::null_pointer("out_message")); - return FFI_ERR_NULL_POINTER; - } - - unsafe { - *out_message = ptr::null_mut(); - } - - let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { - set_error(out_error, ErrorInner::null_pointer("factory")); - return FFI_ERR_NULL_POINTER; - }; - - if content_type.is_null() { - set_error(out_error, ErrorInner::null_pointer("content_type")); - return FFI_ERR_NULL_POINTER; - } - - let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; - let content_type_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => { - set_error( - out_error, - ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), - ); - return FFI_ERR_INVALID_ARGUMENT; - } - }; - - let callback_payload = CallbackStreamingPayload { - callback: read_callback, - user_data, - total_len: payload_len, - }; - - let payload_arc: Arc = - Arc::new(callback_payload); - - match factory_inner.factory.create_indirect_streaming_bytes( - payload_arc, - content_type_str, - None, - ) { - Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, - Err(err) => { - set_error( - out_error, - ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), - ); - FFI_ERR_FACTORY_FAILED - } - } - })); - - match result { - Ok(code) => code, - Err(_) => handle_panic(out_error, "panic during streaming signing"), - } -} - -/// Signs with a streaming payload via callback (indirect), returning an opaque message handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle -/// - `read_callback` must be a valid callback function -/// - `content_type` must be a valid null-terminated C string -/// - `out_message` must be valid for writes -/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_streaming_to_message( - factory: *const CoseSign1FactoryHandle, - read_callback: CoseReadCallback, - payload_len: u64, - user_data: *mut libc::c_void, - content_type: *const libc::c_char, - out_message: *mut *mut CoseSign1MessageHandle, - out_error: *mut *mut CoseSign1SigningErrorHandle, -) -> i32 { - impl_factory_sign_indirect_streaming_to_message_inner( - factory, - read_callback, - payload_len, - user_data, - content_type, - out_message, - out_error, - ) -} - -/// Frees a factory handle. -/// -/// # Safety -/// -/// - `factory` must be a valid factory handle or NULL -/// - The handle must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_factory_free(factory: *mut CoseSign1FactoryHandle) { - if factory.is_null() { - return; - } - unsafe { - drop(Box::from_raw(factory as *mut FactoryInner)); - } -} - -/// Frees COSE bytes allocated by factory functions. -/// -/// # Safety -/// -/// - `ptr` must have been returned by a factory signing function or be NULL -/// - `len` must be the length returned alongside the bytes -/// - The bytes must not be used after this call -#[no_mangle] -pub unsafe extern "C" fn cose_sign1_cose_bytes_free(ptr: *mut u8, len: u32) { - if ptr.is_null() { - return; - } - unsafe { - drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( - ptr, - len as usize, - ))); - } -} - -// ============================================================================ -// Internal: Callback-based key implementation -// ============================================================================ - -struct CallbackKey { - algorithm: i64, - key_type: String, - sign_fn: CoseSignCallback, - user_data: *mut libc::c_void, -} - -// Safety: user_data is opaque and the callback is responsible for thread safety -unsafe impl Send for CallbackKey {} -unsafe impl Sync for CallbackKey {} - -impl CryptoSigner for CallbackKey { - fn sign(&self, data: &[u8]) -> Result, CryptoError> { - let mut out_sig: *mut u8 = ptr::null_mut(); - let mut out_sig_len: usize = 0; - - let rc = unsafe { - (self.sign_fn)( - data.as_ptr(), - data.len(), - &mut out_sig, - &mut out_sig_len, - self.user_data, - ) - }; - - if rc != 0 { - return Err(CryptoError::SigningFailed(format!( - "callback returned error code {}", - rc - ))); - } - - if out_sig.is_null() { - return Err(CryptoError::SigningFailed( - "callback returned null signature".to_string(), - )); - } - - let sig = unsafe { slice::from_raw_parts(out_sig, out_sig_len) }.to_vec(); - - // Free the callback-allocated memory - unsafe { - libc::free(out_sig as *mut libc::c_void); - } - - Ok(sig) - } - - // Accessor methods on CallbackKey are not called during the signing pipeline - // (CoseSigner::sign_payload only invokes signer.sign), and CallbackKey is a - // private type that cannot be constructed from external tests. - fn algorithm(&self) -> i64 { - self.algorithm - } - - fn key_type(&self) -> &str { - &self.key_type - } - - fn key_id(&self) -> Option<&[u8]> { - None - } -} - -// ============================================================================ -// Internal: Simple signing service implementation -// ============================================================================ - -/// Simple signing service that wraps a single key. -/// -/// Used to bridge between the key-based FFI and the factory pattern. -struct SimpleSigningService { - key: std::sync::Arc, -} - -impl SimpleSigningService { - pub fn new(key: std::sync::Arc) -> Self { - Self { key } - } -} - -impl cose_sign1_signing::SigningService for SimpleSigningService { - fn get_cose_signer( - &self, - _context: &cose_sign1_signing::SigningContext, - ) -> Result { - Ok(cose_sign1_signing::CoseSigner::new( - Box::new(ArcCryptoSignerWrapper { - key: self.key.clone(), - }), - CoseHeaderMap::new(), - CoseHeaderMap::new(), - )) - } - - // SimpleSigningService methods below are unreachable through the FFI: - // - is_remote/service_metadata: factory does not query these through FFI - // - verify_signature: always returns Err, making the factory Ok branches unreachable - fn is_remote(&self) -> bool { - false - } - - fn service_metadata(&self) -> &cose_sign1_signing::SigningServiceMetadata { - static METADATA: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(|| { - cose_sign1_signing::SigningServiceMetadata::new( - "FFI Signing Service".to_string(), - "1.0.0".to_string(), - ) - }); - &METADATA - } - - fn verify_signature( - &self, - _message_bytes: &[u8], - _context: &cose_sign1_signing::SigningContext, - ) -> Result { - Err(cose_sign1_signing::SigningError::VerificationFailed( - "verification not supported by FFI signing service".to_string(), - )) - } -} - -/// Wrapper to convert Arc to Box. -struct ArcCryptoSignerWrapper { - key: std::sync::Arc, -} - -impl CryptoSigner for ArcCryptoSignerWrapper { - fn sign(&self, data: &[u8]) -> Result, CryptoError> { - self.key.sign(data) - } - - // ArcCryptoSignerWrapper accessor methods are not called during the signing - // pipeline (CoseSigner::sign_payload only invokes signer.sign), and this is - // a private type that cannot be constructed from external tests. - fn algorithm(&self) -> i64 { - self.key.algorithm() - } - - fn key_type(&self) -> &str { - self.key.key_type() - } - - fn key_id(&self) -> Option<&[u8]> { - self.key.key_id() - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![deny(unsafe_op_in_unsafe_fn)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +//! C/C++ FFI for COSE_Sign1 message signing operations. +//! +//! This crate (`cose_sign1_signing_ffi`) provides FFI-safe wrappers for creating and signing +//! COSE_Sign1 messages from C and C++ code. It uses `cose_sign1_primitives` for types and +//! `cbor_primitives_everparse` for CBOR encoding. +//! +//! For verification operations, see `cose_sign1_primitives_ffi`. +//! +//! ## Error Handling +//! +//! All functions follow a consistent error handling pattern: +//! - Return value: 0 = success, negative = error code +//! - `out_error` parameter: Set to error handle on failure (caller must free) +//! - Output parameters: Only valid if return is 0 +//! +//! ## Memory Management +//! +//! Handles returned by this library must be freed using the corresponding `*_free` function: +//! - `cose_sign1_builder_free` for builder handles +//! - `cose_headermap_free` for header map handles +//! - `cose_key_free` for key handles +//! - `cose_sign1_signing_service_free` for signing service handles +//! - `cose_sign1_factory_free` for factory handles +//! - `cose_sign1_signing_error_free` for error handles +//! - `cose_sign1_string_free` for string pointers +//! - `cose_sign1_bytes_free` for byte buffer pointers +//! - `cose_sign1_cose_bytes_free` for COSE message bytes returned by factory functions +//! +//! ## Thread Safety +//! +//! All handles are thread-safe and can be used from multiple threads. However, handles +//! are not internally synchronized, so concurrent mutation requires external synchronization. + +pub mod error; +pub mod provider; +pub mod types; + +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::ptr; +use std::slice; +use std::sync::Arc; + +use cose_sign1_primitives::{ + CoseHeaderLabel, CoseHeaderMap, CoseHeaderValue, CoseSign1Builder, CoseSign1Message, + CryptoError, CryptoSigner, +}; + +use crate::error::{ + set_error, ErrorInner, FFI_ERR_FACTORY_FAILED, FFI_ERR_INVALID_ARGUMENT, FFI_ERR_NULL_POINTER, + FFI_ERR_PANIC, FFI_ERR_SIGN_FAILED, FFI_OK, +}; +use crate::types::{ + builder_handle_to_inner_mut, builder_inner_to_handle, factory_handle_to_inner, + factory_inner_to_handle, headermap_handle_to_inner, headermap_handle_to_inner_mut, + headermap_inner_to_handle, key_handle_to_inner, key_inner_to_handle, message_inner_to_handle, + signing_service_handle_to_inner, signing_service_inner_to_handle, BuilderInner, FactoryInner, + HeaderMapInner, KeyInner, MessageInner, SigningServiceInner, +}; + +// Re-export handle types for library users +pub use crate::types::{ + CoseHeaderMapHandle, CoseKeyHandle, CoseSign1BuilderHandle, CoseSign1FactoryHandle, + CoseSign1MessageHandle, CoseSign1SigningServiceHandle, +}; + +// Re-export error types for library users +pub use crate::error::{ + CoseSign1SigningErrorHandle, FFI_ERR_FACTORY_FAILED as COSE_SIGN1_SIGNING_ERR_FACTORY_FAILED, + FFI_ERR_INVALID_ARGUMENT as COSE_SIGN1_SIGNING_ERR_INVALID_ARGUMENT, + FFI_ERR_NULL_POINTER as COSE_SIGN1_SIGNING_ERR_NULL_POINTER, + FFI_ERR_PANIC as COSE_SIGN1_SIGNING_ERR_PANIC, + FFI_ERR_SIGN_FAILED as COSE_SIGN1_SIGNING_ERR_SIGN_FAILED, FFI_OK as COSE_SIGN1_SIGNING_OK, +}; + +pub use crate::error::{ + cose_sign1_signing_error_code, cose_sign1_signing_error_free, cose_sign1_signing_error_message, + cose_sign1_string_free, +}; + +/// ABI version for this library. +/// +/// Increment when making breaking changes to the FFI interface. +pub const ABI_VERSION: u32 = 1; + +/// Returns the ABI version for this library. +#[no_mangle] +pub extern "C" fn cose_sign1_signing_abi_version() -> u32 { + ABI_VERSION +} + +/// Records a panic error and returns the panic status code. +/// This is only reachable when `catch_unwind` catches a panic, which cannot +/// be triggered reliably in tests. +#[cfg_attr(coverage_nightly, coverage(off))] +fn handle_panic(out_error: *mut *mut crate::error::CoseSign1SigningErrorHandle, msg: &str) -> i32 { + set_error(out_error, ErrorInner::new(msg, FFI_ERR_PANIC)); + FFI_ERR_PANIC +} + +/// Writes signed bytes to the caller's output pointers. This path is unreachable +/// through the FFI because SimpleSigningService::verify_signature always returns Err, +/// and the factory mandatorily verifies after signing. +#[cfg_attr(coverage_nightly, coverage(off))] +unsafe fn write_signed_bytes( + bytes: Vec, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, +) -> i32 { + let len = bytes.len(); + let boxed = bytes.into_boxed_slice(); + let raw = Box::into_raw(boxed); + unsafe { + *out_cose_bytes = raw as *mut u8; + *out_cose_len = len as u32; + } + FFI_OK +} + +/// Parses signed COSE bytes into a `CoseSign1MessageHandle` and writes it to the +/// caller's output pointer. +/// +/// On success the handle owns the parsed message; free it with +/// `cose_sign1_message_free` from `cose_sign1_primitives_ffi`. +#[cfg_attr(coverage_nightly, coverage(off))] +unsafe fn write_signed_message( + bytes: Vec, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let _provider = crate::provider::ffi_cbor_provider(); + match CoseSign1Message::parse(&bytes) { + Ok(message) => { + unsafe { + *out_message = message_inner_to_handle(MessageInner { message }); + } + FFI_OK + } + Err(err) => { + set_error( + out_error, + ErrorInner::new( + format!("failed to parse signed message: {}", err), + FFI_ERR_SIGN_FAILED, + ), + ); + FFI_ERR_SIGN_FAILED + } + } +} + +// ============================================================================ +// Header map creation and manipulation +// ============================================================================ + +/// Inner implementation for cose_headermap_new. +pub fn impl_headermap_new_inner(out_headers: *mut *mut CoseHeaderMapHandle) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_headers.is_null() { + return FFI_ERR_NULL_POINTER; + } + + let inner = HeaderMapInner { + headers: CoseHeaderMap::new(), + }; + + unsafe { + *out_headers = headermap_inner_to_handle(inner); + } + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Creates a new empty header map. +/// +/// # Safety +/// +/// - `out_headers` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_headermap_free` +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_new(out_headers: *mut *mut CoseHeaderMapHandle) -> i32 { + impl_headermap_new_inner(out_headers) +} + +/// Inner implementation for cose_headermap_set_int. +pub fn impl_headermap_set_int_inner( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: i64, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { + return FFI_ERR_NULL_POINTER; + }; + + inner + .headers + .insert(CoseHeaderLabel::Int(label), CoseHeaderValue::Int(value)); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets an integer value in a header map by integer label. +/// +/// # Safety +/// +/// - `headers` must be a valid header map handle +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_set_int( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: i64, +) -> i32 { + impl_headermap_set_int_inner(headers, label, value) +} + +/// Inner implementation for cose_headermap_set_bytes. +pub fn impl_headermap_set_bytes_inner( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: *const u8, + value_len: usize, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { + return FFI_ERR_NULL_POINTER; + }; + + if value.is_null() && value_len > 0 { + return FFI_ERR_NULL_POINTER; + } + + let bytes = if value.is_null() { + Vec::new() + } else { + unsafe { slice::from_raw_parts(value, value_len) }.to_vec() + }; + + inner.headers.insert( + CoseHeaderLabel::Int(label), + CoseHeaderValue::Bytes(bytes.into()), + ); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets a byte string value in a header map by integer label. +/// +/// # Safety +/// +/// - `headers` must be a valid header map handle +/// - `value` must be valid for reads of `value_len` bytes +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_set_bytes( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: *const u8, + value_len: usize, +) -> i32 { + impl_headermap_set_bytes_inner(headers, label, value, value_len) +} + +/// Inner implementation for cose_headermap_set_text. +pub fn impl_headermap_set_text_inner( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: *const libc::c_char, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { headermap_handle_to_inner_mut(headers) }) else { + return FFI_ERR_NULL_POINTER; + }; + + if value.is_null() { + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(value) }; + let text = match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => return FFI_ERR_INVALID_ARGUMENT, + }; + + inner.headers.insert( + CoseHeaderLabel::Int(label), + CoseHeaderValue::Text(text.into()), + ); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets a text string value in a header map by integer label. +/// +/// # Safety +/// +/// - `headers` must be a valid header map handle +/// - `value` must be a valid null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_set_text( + headers: *mut CoseHeaderMapHandle, + label: i64, + value: *const libc::c_char, +) -> i32 { + impl_headermap_set_text_inner(headers, label, value) +} + +/// Inner implementation for cose_headermap_len. +pub fn impl_headermap_len_inner(headers: *const CoseHeaderMapHandle) -> usize { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { headermap_handle_to_inner(headers) }) else { + return 0; + }; + inner.headers.len() + })); + + result.unwrap_or(0) +} + +/// Returns the number of headers in the map. +/// +/// # Safety +/// +/// - `headers` must be a valid header map handle +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_len(headers: *const CoseHeaderMapHandle) -> usize { + impl_headermap_len_inner(headers) +} + +/// Frees a header map handle. +/// +/// # Safety +/// +/// - `headers` must be a valid header map handle or NULL +/// - The handle must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_headermap_free(headers: *mut CoseHeaderMapHandle) { + if headers.is_null() { + return; + } + unsafe { + drop(Box::from_raw(headers as *mut HeaderMapInner)); + } +} + +// ============================================================================ +// Builder functions +// ============================================================================ + +/// Inner implementation for cose_sign1_builder_new. +pub fn impl_builder_new_inner(out_builder: *mut *mut CoseSign1BuilderHandle) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_builder.is_null() { + return FFI_ERR_NULL_POINTER; + } + + let inner = BuilderInner { + protected: CoseHeaderMap::new(), + unprotected: None, + external_aad: None, + tagged: true, + detached: false, + }; + + unsafe { + *out_builder = builder_inner_to_handle(inner); + } + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Creates a new CoseSign1 message builder. +/// +/// # Safety +/// +/// - `out_builder` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_builder_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_new( + out_builder: *mut *mut CoseSign1BuilderHandle, +) -> i32 { + impl_builder_new_inner(out_builder) +} + +/// Inner implementation for cose_sign1_builder_set_tagged. +pub fn impl_builder_set_tagged_inner(builder: *mut CoseSign1BuilderHandle, tagged: bool) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + inner.tagged = tagged; + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets whether the builder produces tagged COSE_Sign1 output. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_set_tagged( + builder: *mut CoseSign1BuilderHandle, + tagged: bool, +) -> i32 { + impl_builder_set_tagged_inner(builder, tagged) +} + +/// Inner implementation for cose_sign1_builder_set_detached. +pub fn impl_builder_set_detached_inner( + builder: *mut CoseSign1BuilderHandle, + detached: bool, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + inner.detached = detached; + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets whether the builder produces a detached payload. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_set_detached( + builder: *mut CoseSign1BuilderHandle, + detached: bool, +) -> i32 { + impl_builder_set_detached_inner(builder, detached) +} + +/// Inner implementation for cose_sign1_builder_set_protected. +pub fn impl_builder_set_protected_inner( + builder: *mut CoseSign1BuilderHandle, + headers: *const CoseHeaderMapHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + + let Some(hdr_inner) = (unsafe { headermap_handle_to_inner(headers) }) else { + return FFI_ERR_NULL_POINTER; + }; + + builder_inner.protected = hdr_inner.headers.clone(); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets the protected headers for the builder. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +/// - `headers` must be a valid header map handle +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_set_protected( + builder: *mut CoseSign1BuilderHandle, + headers: *const CoseHeaderMapHandle, +) -> i32 { + impl_builder_set_protected_inner(builder, headers) +} + +/// Inner implementation for cose_sign1_builder_set_unprotected. +pub fn impl_builder_set_unprotected_inner( + builder: *mut CoseSign1BuilderHandle, + headers: *const CoseHeaderMapHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + + let Some(hdr_inner) = (unsafe { headermap_handle_to_inner(headers) }) else { + return FFI_ERR_NULL_POINTER; + }; + + builder_inner.unprotected = Some(hdr_inner.headers.clone()); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets the unprotected headers for the builder. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +/// - `headers` must be a valid header map handle +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_set_unprotected( + builder: *mut CoseSign1BuilderHandle, + headers: *const CoseHeaderMapHandle, +) -> i32 { + impl_builder_set_unprotected_inner(builder, headers) +} + +/// Inner implementation for cose_sign1_builder_consume_protected. +pub fn impl_builder_consume_protected_inner( + builder: *mut CoseSign1BuilderHandle, + headers: *mut CoseHeaderMapHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + + if headers.is_null() { + return FFI_ERR_NULL_POINTER; + } + + // Take ownership and move — no clone needed + let hdr_inner = unsafe { Box::from_raw(headers as *mut HeaderMapInner) }; + builder_inner.protected = hdr_inner.headers; + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets the protected headers for the builder by consuming the header map handle. +/// +/// Zero-copy alternative to `cose_sign1_builder_set_protected`. The header map +/// handle is consumed and must NOT be used or freed after this call. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +/// - `headers` must be a valid, owned header map handle (consumed by this call) +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_consume_protected( + builder: *mut CoseSign1BuilderHandle, + headers: *mut CoseHeaderMapHandle, +) -> i32 { + impl_builder_consume_protected_inner(builder, headers) +} + +/// Inner implementation for cose_sign1_builder_consume_unprotected. +pub fn impl_builder_consume_unprotected_inner( + builder: *mut CoseSign1BuilderHandle, + headers: *mut CoseHeaderMapHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(builder_inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + + if headers.is_null() { + return FFI_ERR_NULL_POINTER; + } + + // Take ownership and move — no clone needed + let hdr_inner = unsafe { Box::from_raw(headers as *mut HeaderMapInner) }; + builder_inner.unprotected = Some(hdr_inner.headers); + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets the unprotected headers for the builder by consuming the header map handle. +/// +/// Zero-copy alternative to `cose_sign1_builder_set_unprotected`. The header map +/// handle is consumed and must NOT be used or freed after this call. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +/// - `headers` must be a valid, owned header map handle (consumed by this call) +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_consume_unprotected( + builder: *mut CoseSign1BuilderHandle, + headers: *mut CoseHeaderMapHandle, +) -> i32 { + impl_builder_consume_unprotected_inner(builder, headers) +} + +/// Inner implementation for cose_sign1_builder_set_external_aad. +pub fn impl_builder_set_external_aad_inner( + builder: *mut CoseSign1BuilderHandle, + aad: *const u8, + aad_len: usize, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + let Some(inner) = (unsafe { builder_handle_to_inner_mut(builder) }) else { + return FFI_ERR_NULL_POINTER; + }; + + if aad.is_null() { + inner.external_aad = None; + } else { + inner.external_aad = Some(unsafe { slice::from_raw_parts(aad, aad_len) }.to_vec()); + } + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Sets the external additional authenticated data for the builder. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle +/// - `aad` must be valid for reads of `aad_len` bytes, or NULL +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_set_external_aad( + builder: *mut CoseSign1BuilderHandle, + aad: *const u8, + aad_len: usize, +) -> i32 { + impl_builder_set_external_aad_inner(builder, aad, aad_len) +} + +/// Inner implementation for cose_sign1_builder_sign (coverable by LLVM). +pub fn impl_builder_sign_inner( + builder: *mut CoseSign1BuilderHandle, + key: *const CoseKeyHandle, + payload: *const u8, + payload_len: usize, + out_bytes: *mut *mut u8, + out_len: *mut usize, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_bytes.is_null() || out_len.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_bytes/out_len")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_bytes = ptr::null_mut(); + *out_len = 0; + } + + if builder.is_null() { + set_error(out_error, ErrorInner::null_pointer("builder")); + return FFI_ERR_NULL_POINTER; + } + + let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { + set_error(out_error, ErrorInner::null_pointer("key")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + // Take ownership of builder + let builder_inner = unsafe { Box::from_raw(builder as *mut BuilderInner) }; + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len) } + }; + + // Move fields out of the consumed builder (no cloning needed) + let mut rust_builder = CoseSign1Builder::new() + .protected(builder_inner.protected) + .tagged(builder_inner.tagged) + .detached(builder_inner.detached); + + if let Some(unprotected) = builder_inner.unprotected { + rust_builder = rust_builder.unprotected(unprotected); + } + + if let Some(aad) = builder_inner.external_aad { + rust_builder = rust_builder.external_aad(aad); + } + + match rust_builder.sign(key_inner.key.as_ref(), payload_bytes) { + Ok(bytes) => { + let len = bytes.len(); + let boxed = bytes.into_boxed_slice(); + let raw = Box::into_raw(boxed); + unsafe { + *out_bytes = raw as *mut u8; + *out_len = len; + } + FFI_OK + } + Err(err) => { + set_error(out_error, ErrorInner::from_cose_error(&err)); + FFI_ERR_SIGN_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during signing"), + } +} + +/// Signs a payload using the builder configuration and a key. +/// +/// The builder is consumed by this call and must not be used afterwards. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle; it is freed on success or failure +/// - `key` must be a valid key handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `out_bytes` and `out_len` must be valid for writes +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_sign( + builder: *mut CoseSign1BuilderHandle, + key: *const CoseKeyHandle, + payload: *const u8, + payload_len: usize, + out_bytes: *mut *mut u8, + out_len: *mut usize, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_builder_sign_inner( + builder, + key, + payload, + payload_len, + out_bytes, + out_len, + out_error, + ) +} + +/// Inner implementation for cose_sign1_builder_sign_to_message. +pub fn impl_builder_sign_to_message_inner( + builder: *mut CoseSign1BuilderHandle, + key: *const CoseKeyHandle, + payload: *const u8, + payload_len: usize, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + if builder.is_null() { + set_error(out_error, ErrorInner::null_pointer("builder")); + return FFI_ERR_NULL_POINTER; + } + + let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { + set_error(out_error, ErrorInner::null_pointer("key")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + // Take ownership of builder + let builder_inner = unsafe { Box::from_raw(builder as *mut BuilderInner) }; + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len) } + }; + + // Move fields out of the consumed builder (no cloning needed) + let mut rust_builder = CoseSign1Builder::new() + .protected(builder_inner.protected) + .tagged(builder_inner.tagged) + .detached(builder_inner.detached); + + if let Some(unprotected) = builder_inner.unprotected { + rust_builder = rust_builder.unprotected(unprotected); + } + + if let Some(aad) = builder_inner.external_aad { + rust_builder = rust_builder.external_aad(aad); + } + + match rust_builder.sign(key_inner.key.as_ref(), payload_bytes) { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error(out_error, ErrorInner::from_cose_error(&err)); + FFI_ERR_SIGN_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during signing"), + } +} + +/// Signs a payload and returns an opaque message handle instead of raw bytes. +/// +/// The returned handle can be inspected with `cose_sign1_message_as_bytes`, +/// `cose_sign1_message_payload`, `cose_sign1_message_signature`, etc. from +/// `cose_sign1_primitives_ffi`, and must be freed with `cose_sign1_message_free`. +/// +/// The builder is consumed by this call and must not be used afterwards. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle; it is freed on success or failure +/// - `key` must be a valid key handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_sign_to_message( + builder: *mut CoseSign1BuilderHandle, + key: *const CoseKeyHandle, + payload: *const u8, + payload_len: usize, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_builder_sign_to_message_inner(builder, key, payload, payload_len, out_message, out_error) +} + +/// Frees a builder handle. +/// +/// # Safety +/// +/// - `builder` must be a valid builder handle or NULL +/// - The handle must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_builder_free(builder: *mut CoseSign1BuilderHandle) { + if builder.is_null() { + return; + } + unsafe { + drop(Box::from_raw(builder as *mut BuilderInner)); + } +} + +/// Frees bytes previously returned by signing operations. +/// +/// # Safety +/// +/// - `bytes` must have been returned by `cose_sign1_builder_sign` or be NULL +/// - `len` must be the length returned alongside the bytes +/// - The bytes must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_bytes_free(bytes: *mut u8, len: usize) { + if bytes.is_null() { + return; + } + unsafe { + drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( + bytes, len, + ))); + } +} + +// ============================================================================ +// Key creation via callback +// ============================================================================ + +/// Callback function type for signing operations. +/// +/// The callback receives the complete Sig_structure (RFC 9052) that needs to be signed. +/// +/// # Parameters +/// +/// - `sig_structure`: The CBOR-encoded Sig_structure bytes to sign +/// - `sig_structure_len`: Length of sig_structure +/// - `out_sig`: Output pointer for signature bytes (caller frees with libc::free) +/// - `out_sig_len`: Output pointer for signature length +/// - `user_data`: User-provided context pointer +/// +/// # Returns +/// +/// - `0` on success +/// - Non-zero on error +pub type CoseSignCallback = unsafe extern "C" fn( + sig_structure: *const u8, + sig_structure_len: usize, + out_sig: *mut *mut u8, + out_sig_len: *mut usize, + user_data: *mut libc::c_void, +) -> i32; + +/// Inner implementation for cose_key_from_callback. +pub fn impl_key_from_callback_inner( + algorithm: i64, + key_type: *const libc::c_char, + sign_fn: CoseSignCallback, + user_data: *mut libc::c_void, + out_key: *mut *mut CoseKeyHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_key.is_null() { + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_key = ptr::null_mut(); + } + + if key_type.is_null() { + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(key_type) }; + let key_type_str = match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => return FFI_ERR_INVALID_ARGUMENT, + }; + + let callback_key = CallbackKey { + algorithm, + key_type: key_type_str, + sign_fn, + user_data, + }; + + let inner = KeyInner { + key: std::sync::Arc::new(callback_key), + }; + + unsafe { + *out_key = key_inner_to_handle(inner); + } + FFI_OK + })); + + result.unwrap_or(FFI_ERR_PANIC) +} + +/// Creates a key handle from a signing callback. +/// +/// # Safety +/// +/// - `key_type` must be a valid null-terminated C string +/// - `sign_fn` must be a valid function pointer +/// - `out_key` must be valid for writes +/// - `user_data` must remain valid for the lifetime of the key handle +/// - Caller owns the returned handle and must free it with `cose_key_free` +#[no_mangle] +pub unsafe extern "C" fn cose_key_from_callback( + algorithm: i64, + key_type: *const libc::c_char, + sign_fn: CoseSignCallback, + user_data: *mut libc::c_void, + out_key: *mut *mut CoseKeyHandle, +) -> i32 { + impl_key_from_callback_inner(algorithm, key_type, sign_fn, user_data, out_key) +} + +/// Frees a key handle. +/// +/// # Safety +/// +/// - `key` must be a valid key handle or NULL +/// - The handle must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_key_free(key: *mut CoseKeyHandle) { + if key.is_null() { + return; + } + unsafe { + drop(Box::from_raw(key as *mut KeyInner)); + } +} + +// ============================================================================ +// Signing Service and Factory functions +// ============================================================================ + +/// Inner implementation for cose_sign1_signing_service_create. +pub fn impl_signing_service_create_inner( + key: *const CoseKeyHandle, + out_service: *mut *mut CoseSign1SigningServiceHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_service.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_service")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_service = ptr::null_mut(); + } + + let Some(key_inner) = (unsafe { key_handle_to_inner(key) }) else { + set_error(out_error, ErrorInner::null_pointer("key")); + return FFI_ERR_NULL_POINTER; + }; + + let service = SimpleSigningService::new(key_inner.key.clone()); + let inner = SigningServiceInner { + service: std::sync::Arc::new(service), + }; + + unsafe { + *out_service = signing_service_inner_to_handle(inner); + } + FFI_OK + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during signing service creation"), + } +} + +/// Creates a signing service from a key handle. +/// +/// # Safety +/// +/// - `key` must be a valid key handle +/// - `out_service` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_signing_service_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_signing_service_create( + key: *const CoseKeyHandle, + out_service: *mut *mut CoseSign1SigningServiceHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_signing_service_create_inner(key, out_service, out_error) +} + +/// Frees a signing service handle. +/// +/// # Safety +/// +/// - `service` must be a valid signing service handle or NULL +/// - The handle must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_signing_service_free( + service: *mut CoseSign1SigningServiceHandle, +) { + if service.is_null() { + return; + } + unsafe { + drop(Box::from_raw(service as *mut SigningServiceInner)); + } +} + +// ============================================================================ +// CryptoSigner-based signing service creation +// ============================================================================ + +/// Opaque handle type for CryptoSigner (from cose_sign1_crypto_openssl_ffi). +/// This is the same type as `cose_crypto_signer_t` from crypto_openssl_ffi. +#[repr(C)] +pub struct CryptoSignerHandle { + _private: [u8; 0], +} + +/// Inner implementation for cose_sign1_signing_service_from_crypto_signer. +pub fn impl_signing_service_from_crypto_signer_inner( + signer_handle: *mut CryptoSignerHandle, + out_service: *mut *mut CoseSign1SigningServiceHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_service.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_service")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_service = ptr::null_mut(); + } + + if signer_handle.is_null() { + set_error(out_error, ErrorInner::null_pointer("signer_handle")); + return FFI_ERR_NULL_POINTER; + } + + let signer_box = unsafe { + Box::from_raw(signer_handle as *mut Box) + }; + let signer_arc: std::sync::Arc = (*signer_box).into(); + + let service = SimpleSigningService::new(signer_arc); + let inner = SigningServiceInner { + service: std::sync::Arc::new(service), + }; + + unsafe { + *out_service = signing_service_inner_to_handle(inner); + } + FFI_OK + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic( + out_error, + "panic during signing service creation from crypto signer", + ), + } +} + +/// Creates a signing service from a CryptoSigner handle. +/// +/// This eliminates the need for `cose_key_from_callback`. +/// The signer handle comes from `cose_crypto_openssl_signer_from_der` (or similar). +/// Ownership of the signer handle is transferred to the signing service. +/// +/// # Safety +/// +/// - `signer_handle` must be a valid CryptoSigner handle (from crypto_openssl_ffi) +/// - `out_service` must be valid for writes +/// - `signer_handle` must not be used after this call (ownership transferred) +/// - Caller owns the returned handle and must free it with `cose_sign1_signing_service_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_signing_service_from_crypto_signer( + signer_handle: *mut CryptoSignerHandle, + out_service: *mut *mut CoseSign1SigningServiceHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_signing_service_from_crypto_signer_inner(signer_handle, out_service, out_error) +} + +/// Inner implementation for cose_sign1_factory_from_crypto_signer. +pub fn impl_factory_from_crypto_signer_inner( + signer_handle: *mut CryptoSignerHandle, + out_factory: *mut *mut CoseSign1FactoryHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_factory.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_factory")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_factory = ptr::null_mut(); + } + + if signer_handle.is_null() { + set_error(out_error, ErrorInner::null_pointer("signer_handle")); + return FFI_ERR_NULL_POINTER; + } + + let signer_box = unsafe { + Box::from_raw(signer_handle as *mut Box) + }; + let signer_arc: std::sync::Arc = (*signer_box).into(); + + let service = SimpleSigningService::new(signer_arc); + let service_arc = std::sync::Arc::new(service); + + let factory = cose_sign1_factories::CoseSign1MessageFactory::new(service_arc); + + let inner = FactoryInner { factory }; + + unsafe { + *out_factory = factory_inner_to_handle(inner); + } + FFI_OK + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic( + out_error, + "panic during factory creation from crypto signer", + ), + } +} + +/// Creates a signature factory directly from a CryptoSigner handle. +/// +/// This combines `cose_sign1_signing_service_from_crypto_signer` and +/// `cose_sign1_factory_create` in a single call for convenience. +/// Ownership of the signer handle is transferred to the factory. +/// +/// # Safety +/// +/// - `signer_handle` must be a valid CryptoSigner handle (from crypto_openssl_ffi) +/// - `out_factory` must be valid for writes +/// - `signer_handle` must not be used after this call (ownership transferred) +/// - Caller owns the returned handle and must free it with `cose_sign1_factory_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_from_crypto_signer( + signer_handle: *mut CryptoSignerHandle, + out_factory: *mut *mut CoseSign1FactoryHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_from_crypto_signer_inner(signer_handle, out_factory, out_error) +} + +/// Inner implementation for cose_sign1_factory_create. +pub fn impl_factory_create_inner( + service: *const CoseSign1SigningServiceHandle, + out_factory: *mut *mut CoseSign1FactoryHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_factory.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_factory")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_factory = ptr::null_mut(); + } + + let Some(service_inner) = (unsafe { signing_service_handle_to_inner(service) }) else { + set_error(out_error, ErrorInner::null_pointer("service")); + return FFI_ERR_NULL_POINTER; + }; + + let factory = + cose_sign1_factories::CoseSign1MessageFactory::new(service_inner.service.clone()); + let inner = FactoryInner { factory }; + + unsafe { + *out_factory = factory_inner_to_handle(inner); + } + FFI_OK + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during factory creation"), + } +} + +/// Creates a factory from a signing service handle. +/// +/// # Safety +/// +/// - `service` must be a valid signing service handle +/// - `out_factory` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_factory_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_create( + service: *const CoseSign1SigningServiceHandle, + out_factory: *mut *mut CoseSign1FactoryHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_create_inner(service, out_factory, out_error) +} + +/// Inner implementation for cose_sign1_factory_sign_direct. +pub fn impl_factory_sign_direct_inner( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len as usize) } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + match factory_inner + .factory + .create_direct_bytes(payload_bytes, content_type_str, None) + { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during direct signing"), + } +} + +/// Signs payload with direct signature (embedded payload). +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_inner( + factory, + payload, + payload_len, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect. +pub fn impl_factory_sign_indirect_inner( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len as usize) } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + match factory_inner + .factory + .create_indirect_bytes(payload_bytes, content_type_str, None) + { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during indirect signing"), + } +} + +/// Signs payload with indirect signature (hash envelope). +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_inner( + factory, + payload, + payload_len, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +// ============================================================================ +// Streaming signature functions +// ============================================================================ + +/// Callback type for streaming payload reading. +/// +/// The callback is invoked repeatedly with a buffer to fill. +/// Returns the number of bytes read (0 = EOF), or negative on error. +/// +/// # Safety +/// +/// - `buffer` must be valid for writes of `buffer_len` bytes +/// - `user_data` is the opaque pointer passed to the signing function +pub type CoseReadCallback = + unsafe extern "C" fn(buffer: *mut u8, buffer_len: usize, user_data: *mut libc::c_void) -> i64; + +/// Adapter for callback-based streaming payload. +struct CallbackStreamingPayload { + callback: CoseReadCallback, + user_data: *mut libc::c_void, + total_len: u64, +} + +// SAFETY: The callback is assumed to be thread-safe. +// FFI callers are responsible for ensuring thread safety. +unsafe impl Send for CallbackStreamingPayload {} +unsafe impl Sync for CallbackStreamingPayload {} + +impl cose_sign1_primitives::StreamingPayload for CallbackStreamingPayload { + fn size(&self) -> u64 { + self.total_len + } + + fn open( + &self, + ) -> Result< + Box, + cose_sign1_primitives::error::PayloadError, + > { + Ok(Box::new(CallbackReader { + callback: self.callback, + user_data: self.user_data, + total_len: self.total_len, + bytes_read: 0, + })) + } +} + +/// Reader implementation that wraps the callback. +struct CallbackReader { + callback: CoseReadCallback, + user_data: *mut libc::c_void, + total_len: u64, + bytes_read: u64, +} + +// SAFETY: The callback is assumed to be thread-safe. +// FFI callers are responsible for ensuring thread safety. +unsafe impl Send for CallbackReader {} + +impl std::io::Read for CallbackReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.bytes_read >= self.total_len { + return Ok(0); + } + + let remaining = (self.total_len - self.bytes_read) as usize; + let to_read = buf.len().min(remaining); + + let result = unsafe { (self.callback)(buf.as_mut_ptr(), to_read, self.user_data) }; + + if result < 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("callback read error: {}", result), + )); + } + + let bytes_read = result as usize; + self.bytes_read += bytes_read as u64; + Ok(bytes_read) + } +} + +impl cose_sign1_primitives::sig_structure::SizedRead for CallbackReader { + fn len(&self) -> Result { + Ok(self.total_len) + } +} + +/// Inner implementation for cose_sign1_factory_sign_direct_file. +pub fn impl_factory_sign_direct_file_inner( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if file_path.is_null() { + set_error(out_error, ErrorInner::null_pointer("file_path")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + // Create FilePayload + let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { + Ok(p) => p, + Err(e) => { + set_error( + out_error, + ErrorInner::new( + format!("failed to open file: {}", e), + FFI_ERR_INVALID_ARGUMENT, + ), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let payload_arc: Arc = Arc::new(file_payload); + + // Create options with detached=true + let options = cose_sign1_factories::direct::DirectSignatureOptions { + embed_payload: false, // Force detached for streaming + ..Default::default() + }; + + match factory_inner.factory.create_direct_streaming_bytes( + payload_arc, + content_type_str, + Some(options), + ) { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during file signing"), + } +} + +/// Signs a file directly without loading it into memory (direct signature). +/// +/// Creates a detached COSE_Sign1 signature over the file content. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `file_path` must be a valid null-terminated UTF-8 string +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct_file( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_file_inner( + factory, + file_path, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect_file. +pub fn impl_factory_sign_indirect_file_inner( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if file_path.is_null() { + set_error(out_error, ErrorInner::null_pointer("file_path")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + // Create FilePayload + let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { + Ok(p) => p, + Err(e) => { + set_error( + out_error, + ErrorInner::new( + format!("failed to open file: {}", e), + FFI_ERR_INVALID_ARGUMENT, + ), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let payload_arc: Arc = Arc::new(file_payload); + + match factory_inner.factory.create_indirect_streaming_bytes( + payload_arc, + content_type_str, + None, + ) { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during file signing"), + } +} + +/// Signs a file directly without loading it into memory (indirect signature). +/// +/// Creates a detached COSE_Sign1 signature over the file content hash. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `file_path` must be a valid null-terminated UTF-8 string +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_file( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_file_inner( + factory, + file_path, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_direct_streaming. +#[allow(clippy::too_many_arguments)] +pub fn impl_factory_sign_direct_streaming_inner( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + // Create callback payload + let callback_payload = CallbackStreamingPayload { + callback: read_callback, + user_data, + total_len: payload_len, + }; + + let payload_arc: Arc = + Arc::new(callback_payload); + + // Create options with detached=true + let options = cose_sign1_factories::direct::DirectSignatureOptions { + embed_payload: false, // Force detached for streaming + ..Default::default() + }; + + match factory_inner.factory.create_direct_streaming_bytes( + payload_arc, + content_type_str, + Some(options), + ) { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during streaming signing"), + } +} + +/// Signs with a streaming payload via callback (direct signature). +/// +/// The callback is invoked repeatedly with a buffer to fill. +/// payload_len must be the total payload size (for CBOR bstr header). +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `read_callback` must be a valid callback function +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct_streaming( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_streaming_inner( + factory, + read_callback, + payload_len, + user_data, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect_streaming. +#[allow(clippy::too_many_arguments)] +pub fn impl_factory_sign_indirect_streaming_inner( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_cose_bytes.is_null() || out_cose_len.is_null() { + set_error( + out_error, + ErrorInner::null_pointer("out_cose_bytes/out_cose_len"), + ); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_cose_bytes = ptr::null_mut(); + *out_cose_len = 0; + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + // Create callback payload + let callback_payload = CallbackStreamingPayload { + callback: read_callback, + user_data, + total_len: payload_len, + }; + + let payload_arc: Arc = + Arc::new(callback_payload); + + match factory_inner.factory.create_indirect_streaming_bytes( + payload_arc, + content_type_str, + None, + ) { + Ok(bytes) => unsafe { write_signed_bytes(bytes, out_cose_bytes, out_cose_len) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during streaming signing"), + } +} + +/// Signs with a streaming payload via callback (indirect signature). +/// +/// The callback is invoked repeatedly with a buffer to fill. +/// payload_len must be the total payload size (for CBOR bstr header). +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `read_callback` must be a valid callback function +/// - `content_type` must be a valid null-terminated C string +/// - `out_cose_bytes` and `out_cose_len` must be valid for writes +/// - Caller must free the returned bytes with `cose_sign1_cose_bytes_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_streaming( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_cose_bytes: *mut *mut u8, + out_cose_len: *mut u32, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_streaming_inner( + factory, + read_callback, + payload_len, + user_data, + content_type, + out_cose_bytes, + out_cose_len, + out_error, + ) +} + +// ============================================================================ +// Factory _to_message variants — return CoseSign1MessageHandle +// ============================================================================ + +/// Inner implementation for cose_sign1_factory_sign_direct_to_message. +pub fn impl_factory_sign_direct_to_message_inner( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len as usize) } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + match factory_inner + .factory + .create_direct_bytes(payload_bytes, content_type_str, None) + { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during direct signing"), + } +} + +/// Signs payload with direct signature, returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct_to_message( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_to_message_inner( + factory, + payload, + payload_len, + content_type, + out_message, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect_to_message. +pub fn impl_factory_sign_indirect_to_message_inner( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if payload.is_null() && payload_len > 0 { + set_error(out_error, ErrorInner::null_pointer("payload")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let payload_bytes = if payload.is_null() { + &[] as &[u8] + } else { + unsafe { slice::from_raw_parts(payload, payload_len as usize) } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + match factory_inner + .factory + .create_indirect_bytes(payload_bytes, content_type_str, None) + { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during indirect signing"), + } +} + +/// Signs payload with indirect signature, returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `payload` must be valid for reads of `payload_len` bytes +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_to_message( + factory: *const CoseSign1FactoryHandle, + payload: *const u8, + payload_len: u32, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_to_message_inner( + factory, + payload, + payload_len, + content_type, + out_message, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_direct_file_to_message. +pub fn impl_factory_sign_direct_file_to_message_inner( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if file_path.is_null() { + set_error(out_error, ErrorInner::null_pointer("file_path")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { + Ok(p) => p, + Err(e) => { + set_error( + out_error, + ErrorInner::new( + format!("failed to open file: {}", e), + FFI_ERR_INVALID_ARGUMENT, + ), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let payload_arc: Arc = Arc::new(file_payload); + + let options = cose_sign1_factories::direct::DirectSignatureOptions { + embed_payload: false, + ..Default::default() + }; + + match factory_inner.factory.create_direct_streaming_bytes( + payload_arc, + content_type_str, + Some(options), + ) { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during file signing"), + } +} + +/// Signs a file directly, returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `file_path` must be a valid null-terminated UTF-8 string +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct_file_to_message( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_file_to_message_inner( + factory, + file_path, + content_type, + out_message, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect_file_to_message. +pub fn impl_factory_sign_indirect_file_to_message_inner( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if file_path.is_null() { + set_error(out_error, ErrorInner::null_pointer("file_path")); + return FFI_ERR_NULL_POINTER; + } + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(file_path) }; + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid file_path UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let file_payload = match cose_sign1_primitives::FilePayload::new(path_str) { + Ok(p) => p, + Err(e) => { + set_error( + out_error, + ErrorInner::new( + format!("failed to open file: {}", e), + FFI_ERR_INVALID_ARGUMENT, + ), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let payload_arc: Arc = Arc::new(file_payload); + + match factory_inner.factory.create_indirect_streaming_bytes( + payload_arc, + content_type_str, + None, + ) { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during file signing"), + } +} + +/// Signs a file with indirect signature, returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `file_path` must be a valid null-terminated UTF-8 string +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_file_to_message( + factory: *const CoseSign1FactoryHandle, + file_path: *const libc::c_char, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_file_to_message_inner( + factory, + file_path, + content_type, + out_message, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_direct_streaming_to_message. +pub fn impl_factory_sign_direct_streaming_to_message_inner( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let callback_payload = CallbackStreamingPayload { + callback: read_callback, + user_data, + total_len: payload_len, + }; + + let payload_arc: Arc = + Arc::new(callback_payload); + + let options = cose_sign1_factories::direct::DirectSignatureOptions { + embed_payload: false, + ..Default::default() + }; + + match factory_inner.factory.create_direct_streaming_bytes( + payload_arc, + content_type_str, + Some(options), + ) { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during streaming signing"), + } +} + +/// Signs with a streaming payload via callback (direct), returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `read_callback` must be a valid callback function +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_direct_streaming_to_message( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_direct_streaming_to_message_inner( + factory, + read_callback, + payload_len, + user_data, + content_type, + out_message, + out_error, + ) +} + +/// Inner implementation for cose_sign1_factory_sign_indirect_streaming_to_message. +pub fn impl_factory_sign_indirect_streaming_to_message_inner( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + let result = catch_unwind(AssertUnwindSafe(|| { + if out_message.is_null() { + set_error(out_error, ErrorInner::null_pointer("out_message")); + return FFI_ERR_NULL_POINTER; + } + + unsafe { + *out_message = ptr::null_mut(); + } + + let Some(factory_inner) = (unsafe { factory_handle_to_inner(factory) }) else { + set_error(out_error, ErrorInner::null_pointer("factory")); + return FFI_ERR_NULL_POINTER; + }; + + if content_type.is_null() { + set_error(out_error, ErrorInner::null_pointer("content_type")); + return FFI_ERR_NULL_POINTER; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(content_type) }; + let content_type_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_error( + out_error, + ErrorInner::new("invalid content_type UTF-8", FFI_ERR_INVALID_ARGUMENT), + ); + return FFI_ERR_INVALID_ARGUMENT; + } + }; + + let callback_payload = CallbackStreamingPayload { + callback: read_callback, + user_data, + total_len: payload_len, + }; + + let payload_arc: Arc = + Arc::new(callback_payload); + + match factory_inner.factory.create_indirect_streaming_bytes( + payload_arc, + content_type_str, + None, + ) { + Ok(bytes) => unsafe { write_signed_message(bytes, out_message, out_error) }, + Err(err) => { + set_error( + out_error, + ErrorInner::new(format!("factory failed: {}", err), FFI_ERR_FACTORY_FAILED), + ); + FFI_ERR_FACTORY_FAILED + } + } + })); + + match result { + Ok(code) => code, + Err(_) => handle_panic(out_error, "panic during streaming signing"), + } +} + +/// Signs with a streaming payload via callback (indirect), returning an opaque message handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle +/// - `read_callback` must be a valid callback function +/// - `content_type` must be a valid null-terminated C string +/// - `out_message` must be valid for writes +/// - Caller owns the returned handle and must free it with `cose_sign1_message_free` +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_sign_indirect_streaming_to_message( + factory: *const CoseSign1FactoryHandle, + read_callback: CoseReadCallback, + payload_len: u64, + user_data: *mut libc::c_void, + content_type: *const libc::c_char, + out_message: *mut *mut CoseSign1MessageHandle, + out_error: *mut *mut CoseSign1SigningErrorHandle, +) -> i32 { + impl_factory_sign_indirect_streaming_to_message_inner( + factory, + read_callback, + payload_len, + user_data, + content_type, + out_message, + out_error, + ) +} + +/// Frees a factory handle. +/// +/// # Safety +/// +/// - `factory` must be a valid factory handle or NULL +/// - The handle must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_factory_free(factory: *mut CoseSign1FactoryHandle) { + if factory.is_null() { + return; + } + unsafe { + drop(Box::from_raw(factory as *mut FactoryInner)); + } +} + +/// Frees COSE bytes allocated by factory functions. +/// +/// # Safety +/// +/// - `ptr` must have been returned by a factory signing function or be NULL +/// - `len` must be the length returned alongside the bytes +/// - The bytes must not be used after this call +#[no_mangle] +pub unsafe extern "C" fn cose_sign1_cose_bytes_free(ptr: *mut u8, len: u32) { + if ptr.is_null() { + return; + } + unsafe { + drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( + ptr, + len as usize, + ))); + } +} + +// ============================================================================ +// Internal: Callback-based key implementation +// ============================================================================ + +struct CallbackKey { + algorithm: i64, + key_type: String, + sign_fn: CoseSignCallback, + user_data: *mut libc::c_void, +} + +// Safety: user_data is opaque and the callback is responsible for thread safety +unsafe impl Send for CallbackKey {} +unsafe impl Sync for CallbackKey {} + +impl CryptoSigner for CallbackKey { + fn sign(&self, data: &[u8]) -> Result, CryptoError> { + let mut out_sig: *mut u8 = ptr::null_mut(); + let mut out_sig_len: usize = 0; + + let rc = unsafe { + (self.sign_fn)( + data.as_ptr(), + data.len(), + &mut out_sig, + &mut out_sig_len, + self.user_data, + ) + }; + + if rc != 0 { + return Err(CryptoError::SigningFailed(format!( + "callback returned error code {}", + rc + ))); + } + + if out_sig.is_null() { + return Err(CryptoError::SigningFailed( + "callback returned null signature".to_string(), + )); + } + + let sig = unsafe { slice::from_raw_parts(out_sig, out_sig_len) }.to_vec(); + + // Free the callback-allocated memory + unsafe { + libc::free(out_sig as *mut libc::c_void); + } + + Ok(sig) + } + + // Accessor methods on CallbackKey are not called during the signing pipeline + // (CoseSigner::sign_payload only invokes signer.sign), and CallbackKey is a + // private type that cannot be constructed from external tests. + fn algorithm(&self) -> i64 { + self.algorithm + } + + fn key_type(&self) -> &str { + &self.key_type + } + + fn key_id(&self) -> Option<&[u8]> { + None + } +} + +// ============================================================================ +// Internal: Simple signing service implementation +// ============================================================================ + +/// Simple signing service that wraps a single key. +/// +/// Used to bridge between the key-based FFI and the factory pattern. +struct SimpleSigningService { + key: std::sync::Arc, +} + +impl SimpleSigningService { + pub fn new(key: std::sync::Arc) -> Self { + Self { key } + } +} + +impl cose_sign1_signing::SigningService for SimpleSigningService { + fn get_cose_signer( + &self, + _context: &cose_sign1_signing::SigningContext, + ) -> Result { + Ok(cose_sign1_signing::CoseSigner::new( + Box::new(ArcCryptoSignerWrapper { + key: self.key.clone(), + }), + CoseHeaderMap::new(), + CoseHeaderMap::new(), + )) + } + + // SimpleSigningService methods below are unreachable through the FFI: + // - is_remote/service_metadata: factory does not query these through FFI + // - verify_signature: always returns Err, making the factory Ok branches unreachable + fn is_remote(&self) -> bool { + false + } + + fn service_metadata(&self) -> &cose_sign1_signing::SigningServiceMetadata { + static METADATA: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| { + cose_sign1_signing::SigningServiceMetadata::new( + "FFI Signing Service".to_string(), + "1.0.0".to_string(), + ) + }); + &METADATA + } + + fn verify_signature( + &self, + _message_bytes: &[u8], + _context: &cose_sign1_signing::SigningContext, + ) -> Result { + Err(cose_sign1_signing::SigningError::VerificationFailed { + detail: std::borrow::Cow::Borrowed("verification not supported by FFI signing service"), + }) + } +} + +/// Wrapper to convert Arc to Box. +struct ArcCryptoSignerWrapper { + key: std::sync::Arc, +} + +impl CryptoSigner for ArcCryptoSignerWrapper { + fn sign(&self, data: &[u8]) -> Result, CryptoError> { + self.key.sign(data) + } + + // ArcCryptoSignerWrapper accessor methods are not called during the signing + // pipeline (CoseSigner::sign_payload only invokes signer.sign), and this is + // a private type that cannot be constructed from external tests. + fn algorithm(&self) -> i64 { + self.key.algorithm() + } + + fn key_type(&self) -> &str { + self.key.key_type() + } + + fn key_id(&self) -> Option<&[u8]> { + self.key.key_id() + } +} diff --git a/native/rust/signing/core/ffi/tests/unit_test_internal_types.rs b/native/rust/signing/core/ffi/tests/unit_test_internal_types.rs index 825596da..8434272f 100644 --- a/native/rust/signing/core/ffi/tests/unit_test_internal_types.rs +++ b/native/rust/signing/core/ffi/tests/unit_test_internal_types.rs @@ -159,9 +159,9 @@ impl cose_sign1_signing::SigningService for TestableSimpleSigningService { _message_bytes: &[u8], _context: &cose_sign1_signing::SigningContext, ) -> Result { - Err(cose_sign1_signing::SigningError::VerificationFailed( - "verification not supported by FFI signing service".to_string(), - )) + Err(cose_sign1_signing::SigningError::VerificationFailed { + detail: std::borrow::Cow::Borrowed("verification not supported by FFI signing service"), + }) } } @@ -225,8 +225,8 @@ fn test_simple_signing_service_verify_signature() { assert!(result.is_err()); match result.unwrap_err() { - cose_sign1_signing::SigningError::VerificationFailed(msg) => { - assert!(msg.contains("verification not supported")); + cose_sign1_signing::SigningError::VerificationFailed { detail } => { + assert!(detail.contains("verification not supported")); } _ => panic!("Expected VerificationFailed error"), } diff --git a/native/rust/signing/core/src/error.rs b/native/rust/signing/core/src/error.rs index cd60eb6c..8fa3ae27 100644 --- a/native/rust/signing/core/src/error.rs +++ b/native/rust/signing/core/src/error.rs @@ -3,33 +3,39 @@ //! Signing errors. +use std::borrow::Cow; + /// Error type for signing operations. #[derive(Debug)] pub enum SigningError { /// Error related to key operations. - KeyError(String), + KeyError { detail: Cow<'static, str> }, /// Header contribution failed. - HeaderContributionFailed(String), + HeaderContributionFailed { detail: Cow<'static, str> }, /// Signing operation failed. - SigningFailed(String), + SigningFailed { detail: Cow<'static, str> }, /// Signature verification failed. - VerificationFailed(String), + VerificationFailed { detail: Cow<'static, str> }, /// Invalid configuration. - InvalidConfiguration(String), + InvalidConfiguration { detail: Cow<'static, str> }, } impl std::fmt::Display for SigningError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::KeyError(msg) => write!(f, "Key error: {}", msg), - Self::HeaderContributionFailed(msg) => write!(f, "Header contribution failed: {}", msg), - Self::SigningFailed(msg) => write!(f, "Signing failed: {}", msg), - Self::VerificationFailed(msg) => write!(f, "Verification failed: {}", msg), - Self::InvalidConfiguration(msg) => write!(f, "Invalid configuration: {}", msg), + Self::KeyError { detail } => write!(f, "Key error: {}", detail), + Self::HeaderContributionFailed { detail } => { + write!(f, "Header contribution failed: {}", detail) + } + Self::SigningFailed { detail } => write!(f, "Signing failed: {}", detail), + Self::VerificationFailed { detail } => write!(f, "Verification failed: {}", detail), + Self::InvalidConfiguration { detail } => { + write!(f, "Invalid configuration: {}", detail) + } } } } diff --git a/native/rust/signing/core/src/options.rs b/native/rust/signing/core/src/options.rs index 357057f4..fd257dff 100644 --- a/native/rust/signing/core/src/options.rs +++ b/native/rust/signing/core/src/options.rs @@ -6,6 +6,7 @@ /// Options for signing operations. /// /// Maps V2 `DirectSignatureOptions` and related options classes. +#[must_use = "builders do nothing unless consumed"] #[derive(Debug, Clone)] pub struct SigningOptions { /// Additional header contributors for this signing operation. diff --git a/native/rust/signing/core/src/signer.rs b/native/rust/signing/core/src/signer.rs index e181e790..a44db31d 100644 --- a/native/rust/signing/core/src/signer.rs +++ b/native/rust/signing/core/src/signer.rs @@ -95,17 +95,24 @@ impl CoseSigner { ) -> Result, SigningError> { use cose_sign1_primitives::build_sig_structure; - let protected_bytes = self.protected_headers.encode().map_err(|e| { - SigningError::SigningFailed(format!("Failed to encode protected headers: {}", e)) - })?; + let protected_bytes = + self.protected_headers + .encode() + .map_err(|e| SigningError::SigningFailed { + detail: format!("Failed to encode protected headers: {}", e).into(), + })?; let sig_structure = build_sig_structure(&protected_bytes, external_aad, payload).map_err(|e| { - SigningError::SigningFailed(format!("Failed to build Sig_structure: {}", e)) + SigningError::SigningFailed { + detail: format!("Failed to build Sig_structure: {}", e).into(), + } })?; self.signer .sign(&sig_structure) - .map_err(|e| SigningError::SigningFailed(format!("Signing failed: {}", e))) + .map_err(|e| SigningError::SigningFailed { + detail: format!("Signing failed: {}", e).into(), + }) } } diff --git a/native/rust/signing/core/tests/error_tests.rs b/native/rust/signing/core/tests/error_tests.rs index da48ae04..b4a0ae49 100644 --- a/native/rust/signing/core/tests/error_tests.rs +++ b/native/rust/signing/core/tests/error_tests.rs @@ -7,28 +7,40 @@ use cose_sign1_signing::SigningError; #[test] fn test_signing_error_variants() { - let key_err = SigningError::KeyError("test key error".to_string()); + let key_err = SigningError::KeyError { + detail: "test key error".into(), + }; assert!(key_err.to_string().contains("Key error")); assert!(key_err.to_string().contains("test key error")); - let header_err = SigningError::HeaderContributionFailed("header fail".to_string()); + let header_err = SigningError::HeaderContributionFailed { + detail: "header fail".into(), + }; assert!(header_err .to_string() .contains("Header contribution failed")); - let signing_err = SigningError::SigningFailed("signing fail".to_string()); + let signing_err = SigningError::SigningFailed { + detail: "signing fail".into(), + }; assert!(signing_err.to_string().contains("Signing failed")); - let verify_err = SigningError::VerificationFailed("verify fail".to_string()); + let verify_err = SigningError::VerificationFailed { + detail: "verify fail".into(), + }; assert!(verify_err.to_string().contains("Verification failed")); - let config_err = SigningError::InvalidConfiguration("config fail".to_string()); + let config_err = SigningError::InvalidConfiguration { + detail: "config fail".into(), + }; assert!(config_err.to_string().contains("Invalid configuration")); } #[test] fn test_signing_error_debug() { - let err = SigningError::KeyError("test".to_string()); + let err = SigningError::KeyError { + detail: "test".into(), + }; let debug_str = format!("{:?}", err); assert!(debug_str.contains("KeyError")); } diff --git a/native/rust/signing/factories/ffi/tests/comprehensive_ffi_new_coverage.rs b/native/rust/signing/factories/ffi/tests/comprehensive_ffi_new_coverage.rs index 7488f77e..ef3f059d 100644 --- a/native/rust/signing/factories/ffi/tests/comprehensive_ffi_new_coverage.rs +++ b/native/rust/signing/factories/ffi/tests/comprehensive_ffi_new_coverage.rs @@ -228,7 +228,9 @@ fn error_inner_null_pointer_message() { #[test] fn error_inner_from_factory_error() { - let factory_err = cose_sign1_factories::FactoryError::SigningFailed("boom".into()); + let factory_err = cose_sign1_factories::FactoryError::SigningFailed { + detail: "boom".into(), + }; let e = ErrorInner::from_factory_error(&factory_err); assert_eq!(e.code, FFI_ERR_FACTORY_FAILED); assert!(!e.message.is_empty()); diff --git a/native/rust/signing/factories/src/direct/factory.rs b/native/rust/signing/factories/src/direct/factory.rs index 41462c42..7c6c87c8 100644 --- a/native/rust/signing/factories/src/direct/factory.rs +++ b/native/rust/signing/factories/src/direct/factory.rs @@ -121,9 +121,9 @@ impl DirectSignatureFactory { .verify_signature(&message_bytes, &context)?; if !verification_result { - return Err(FactoryError::VerificationFailed( - "Post-sign verification failed".to_string(), - )); + return Err(FactoryError::VerificationFailed { + detail: std::borrow::Cow::Borrowed("Post-sign verification failed"), + }); } // Apply transparency providers if configured @@ -133,7 +133,9 @@ impl DirectSignatureFactory { let mut current_bytes = message_bytes; for provider in &self.transparency_providers { current_bytes = add_proof_with_receipt_merge(provider.as_ref(), ¤t_bytes) - .map_err(|e| FactoryError::TransparencyFailed(e.to_string()))?; + .map_err(|e| FactoryError::TransparencyFailed { + detail: e.to_string().into(), + })?; } return Ok(current_bytes); } @@ -160,7 +162,9 @@ impl DirectSignatureFactory { options: Option, ) -> Result { let bytes = self.create_bytes(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } /// Creates a COSE_Sign1 message with a direct signature from a streaming payload and returns it as bytes. @@ -187,10 +191,10 @@ impl DirectSignatureFactory { // Enforce embed size limit if options.embed_payload && payload.size() > max_embed_size { - return Err(FactoryError::PayloadTooLargeForEmbedding( - payload.size(), - max_embed_size, - )); + return Err(FactoryError::PayloadTooLargeForEmbedding { + actual: payload.size(), + max: max_embed_size, + }); } // Create signing context (use empty vec for context since we'll stream) @@ -244,9 +248,9 @@ impl DirectSignatureFactory { .verify_signature(&message_bytes, &context)?; if !verification_result { - return Err(FactoryError::VerificationFailed( - "Post-sign verification failed".to_string(), - )); + return Err(FactoryError::VerificationFailed { + detail: std::borrow::Cow::Borrowed("Post-sign verification failed"), + }); } // Apply transparency providers if configured @@ -256,7 +260,9 @@ impl DirectSignatureFactory { let mut current_bytes = message_bytes; for provider in &self.transparency_providers { current_bytes = add_proof_with_receipt_merge(provider.as_ref(), ¤t_bytes) - .map_err(|e| FactoryError::TransparencyFailed(e.to_string()))?; + .map_err(|e| FactoryError::TransparencyFailed { + detail: e.to_string().into(), + })?; } return Ok(current_bytes); } @@ -283,6 +289,8 @@ impl DirectSignatureFactory { options: Option, ) -> Result { let bytes = self.create_streaming_bytes(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } } diff --git a/native/rust/signing/factories/src/direct/options.rs b/native/rust/signing/factories/src/direct/options.rs index c6a0ce2d..ae95de74 100644 --- a/native/rust/signing/factories/src/direct/options.rs +++ b/native/rust/signing/factories/src/direct/options.rs @@ -8,6 +8,7 @@ use cose_sign1_signing::HeaderContributor; /// Options for creating direct signatures. /// /// Maps V2 `DirectSignatureOptions`. +#[must_use = "builders do nothing unless consumed"] #[derive(Default)] pub struct DirectSignatureOptions { /// Whether to embed the payload in the COSE_Sign1 message. diff --git a/native/rust/signing/factories/src/error.rs b/native/rust/signing/factories/src/error.rs index e3432d77..5ca63388 100644 --- a/native/rust/signing/factories/src/error.rs +++ b/native/rust/signing/factories/src/error.rs @@ -3,41 +3,47 @@ //! Factory errors. +use std::borrow::Cow; + /// Error type for factory operations. #[derive(Debug)] pub enum FactoryError { /// Signing operation failed. - SigningFailed(String), + SigningFailed { detail: Cow<'static, str> }, /// Post-sign verification failed. - VerificationFailed(String), + VerificationFailed { detail: Cow<'static, str> }, /// Invalid input provided to factory. - InvalidInput(String), + InvalidInput { detail: Cow<'static, str> }, /// CBOR encoding/decoding error. - CborError(String), + CborError { detail: Cow<'static, str> }, /// Transparency provider failed. - TransparencyFailed(String), + TransparencyFailed { detail: Cow<'static, str> }, /// Payload exceeds maximum size for embedding. - PayloadTooLargeForEmbedding(u64, u64), + PayloadTooLargeForEmbedding { actual: u64, max: u64 }, } impl std::fmt::Display for FactoryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::SigningFailed(msg) => write!(f, "Signing failed: {}", msg), - Self::VerificationFailed(msg) => write!(f, "Verification failed: {}", msg), - Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), - Self::CborError(msg) => write!(f, "CBOR error: {}", msg), - Self::TransparencyFailed(msg) => write!(f, "Transparency failed: {}", msg), - Self::PayloadTooLargeForEmbedding(size, max) => { + Self::SigningFailed { detail } => write!(f, "Signing failed: {}", detail), + Self::VerificationFailed { detail } => { + write!(f, "Verification failed: {}", detail) + } + Self::InvalidInput { detail } => write!(f, "Invalid input: {}", detail), + Self::CborError { detail } => write!(f, "CBOR error: {}", detail), + Self::TransparencyFailed { detail } => { + write!(f, "Transparency failed: {}", detail) + } + Self::PayloadTooLargeForEmbedding { actual, max } => { write!( f, "Payload too large for embedding: {} bytes (max {})", - size, max + actual, max ) } } @@ -49,16 +55,20 @@ impl std::error::Error for FactoryError {} impl From for FactoryError { fn from(err: cose_sign1_signing::SigningError) -> Self { match err { - cose_sign1_signing::SigningError::VerificationFailed(msg) => { - FactoryError::VerificationFailed(msg) + cose_sign1_signing::SigningError::VerificationFailed { detail } => { + FactoryError::VerificationFailed { detail } } - _ => FactoryError::SigningFailed(err.to_string()), + _ => FactoryError::SigningFailed { + detail: err.to_string().into(), + }, } } } impl From for FactoryError { fn from(err: cose_sign1_primitives::CoseSign1Error) -> Self { - FactoryError::SigningFailed(err.to_string()) + FactoryError::SigningFailed { + detail: err.to_string().into(), + } } } diff --git a/native/rust/signing/factories/src/factory.rs b/native/rust/signing/factories/src/factory.rs index 2124a251..5455ddc0 100644 --- a/native/rust/signing/factories/src/factory.rs +++ b/native/rust/signing/factories/src/factory.rs @@ -295,12 +295,16 @@ impl CoseSign1MessageFactory { content_type: &str, options: &T, ) -> Result { - let factory = self.factories.get(&TypeId::of::()).ok_or_else(|| { - FactoryError::SigningFailed(format!( - "No factory registered for options type {:?}", - std::any::type_name::() - )) - })?; + let factory = + self.factories + .get(&TypeId::of::()) + .ok_or_else(|| FactoryError::SigningFailed { + detail: format!( + "No factory registered for options type {:?}", + std::any::type_name::() + ) + .into(), + })?; factory.create_dyn(payload, content_type, options) } } diff --git a/native/rust/signing/factories/src/indirect/factory.rs b/native/rust/signing/factories/src/indirect/factory.rs index cf6b1edd..6696028c 100644 --- a/native/rust/signing/factories/src/indirect/factory.rs +++ b/native/rust/signing/factories/src/indirect/factory.rs @@ -141,7 +141,9 @@ impl IndirectSignatureFactory { options: Option, ) -> Result { let bytes = self.create_bytes(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } /// Creates a COSE_Sign1 message with an indirect signature from a streaming payload and returns it as bytes. @@ -171,9 +173,9 @@ impl IndirectSignatureFactory { let options = options.unwrap_or_default(); // Hash the streaming payload - let mut reader = payload - .open() - .map_err(|e| FactoryError::SigningFailed(format!("Failed to open payload: {}", e)))?; + let mut reader = payload.open().map_err(|e| FactoryError::SigningFailed { + detail: format!("Failed to open payload: {}", e).into(), + })?; let hash_bytes = match options.payload_hash_algorithm { HashAlgorithm::Sha256 => { @@ -181,7 +183,9 @@ impl IndirectSignatureFactory { let mut buf = vec![0u8; 65536]; loop { let n = std::io::Read::read(reader.as_mut(), &mut buf).map_err(|e| { - FactoryError::SigningFailed(format!("Failed to read payload: {}", e)) + FactoryError::SigningFailed { + detail: format!("Failed to read payload: {}", e).into(), + } })?; if n == 0 { break; @@ -195,7 +199,9 @@ impl IndirectSignatureFactory { let mut buf = vec![0u8; 65536]; loop { let n = std::io::Read::read(reader.as_mut(), &mut buf).map_err(|e| { - FactoryError::SigningFailed(format!("Failed to read payload: {}", e)) + FactoryError::SigningFailed { + detail: format!("Failed to read payload: {}", e).into(), + } })?; if n == 0 { break; @@ -209,7 +215,9 @@ impl IndirectSignatureFactory { let mut buf = vec![0u8; 65536]; loop { let n = std::io::Read::read(reader.as_mut(), &mut buf).map_err(|e| { - FactoryError::SigningFailed(format!("Failed to read payload: {}", e)) + FactoryError::SigningFailed { + detail: format!("Failed to read payload: {}", e).into(), + } })?; if n == 0 { break; @@ -260,6 +268,8 @@ impl IndirectSignatureFactory { options: Option, ) -> Result { let bytes = self.create_streaming_bytes(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } } diff --git a/native/rust/signing/factories/src/indirect/options.rs b/native/rust/signing/factories/src/indirect/options.rs index fc8394c4..ef40c522 100644 --- a/native/rust/signing/factories/src/indirect/options.rs +++ b/native/rust/signing/factories/src/indirect/options.rs @@ -42,6 +42,7 @@ impl HashAlgorithm { /// Options for creating indirect signatures. /// /// Maps V2 `IndirectSignatureOptions`. +#[must_use = "builders do nothing unless consumed"] #[derive(Default, Debug)] pub struct IndirectSignatureOptions { /// Base options for the underlying direct signature. diff --git a/native/rust/signing/factories/tests/coverage_boost.rs b/native/rust/signing/factories/tests/coverage_boost.rs index 06bc7e56..664ccda5 100644 --- a/native/rust/signing/factories/tests/coverage_boost.rs +++ b/native/rust/signing/factories/tests/coverage_boost.rs @@ -406,7 +406,9 @@ impl SignatureFactoryProvider for SimpleCustomFactory { ) -> Result { let bytes: Vec = self.create_bytes_dyn(payload, content_type, options)?; // This will fail to parse as valid COSE, which is fine — we test the error path - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } } @@ -434,21 +436,34 @@ fn router_create_with_custom_factory_invoked() { /// Exercises Display on all FactoryError variants. #[test] fn factory_error_display_variants() { - let e1 = FactoryError::SigningFailed("sign err".to_string()); + let e1 = FactoryError::SigningFailed { + detail: "sign err".into(), + }; assert!(format!("{}", e1).contains("Signing failed")); - let e2 = FactoryError::VerificationFailed("verify err".to_string()); + let e2 = FactoryError::VerificationFailed { + detail: "verify err".into(), + }; assert!(format!("{}", e2).contains("Verification failed")); - let e3 = FactoryError::InvalidInput("bad input".to_string()); + let e3 = FactoryError::InvalidInput { + detail: "bad input".into(), + }; assert!(format!("{}", e3).contains("Invalid input")); - let e4 = FactoryError::CborError("cbor err".to_string()); + let e4 = FactoryError::CborError { + detail: "cbor err".into(), + }; assert!(format!("{}", e4).contains("CBOR error")); - let e5 = FactoryError::TransparencyFailed("tp err".to_string()); + let e5 = FactoryError::TransparencyFailed { + detail: "tp err".into(), + }; assert!(format!("{}", e5).contains("Transparency failed")); - let e6 = FactoryError::PayloadTooLargeForEmbedding(200, 100); + let e6 = FactoryError::PayloadTooLargeForEmbedding { + actual: 200, + max: 100, + }; assert!(format!("{}", e6).contains("too large")); } diff --git a/native/rust/signing/factories/tests/deep_factory_coverage.rs b/native/rust/signing/factories/tests/deep_factory_coverage.rs index aa4e9ead..94d9af12 100644 --- a/native/rust/signing/factories/tests/deep_factory_coverage.rs +++ b/native/rust/signing/factories/tests/deep_factory_coverage.rs @@ -77,7 +77,9 @@ impl TestSigningService { impl SigningService for TestSigningService { fn get_cose_signer(&self, _ctx: &SigningContext) -> Result { if self.fail_signer { - return Err(SigningError::SigningFailed("mock fail".into())); + return Err(SigningError::SigningFailed { + detail: "mock fail".into(), + }); } Ok(CoseSigner::new( Box::new(MockKey), @@ -329,7 +331,7 @@ fn create_streaming_bytes_payload_too_large() { let result = factory.create_streaming_bytes(payload, "text/plain", Some(opts)); assert!(result.is_err()); match result.unwrap_err() { - FactoryError::PayloadTooLargeForEmbedding(actual, max) => { + FactoryError::PayloadTooLargeForEmbedding { actual, max } => { assert_eq!(actual, 2000); assert_eq!(max, 1000); } @@ -351,8 +353,8 @@ fn create_streaming_bytes_verification_failure() { let result = factory.create_streaming_bytes(payload, "text/plain", Some(opts)); assert!(result.is_err()); match result.unwrap_err() { - FactoryError::VerificationFailed(msg) => { - assert!(msg.contains("Post-sign verification failed")); + FactoryError::VerificationFailed { detail } => { + assert!(detail.contains("Post-sign verification failed")); } other => panic!("expected VerificationFailed, got: {other}"), } diff --git a/native/rust/signing/factories/tests/direct_factory_happy_path.rs b/native/rust/signing/factories/tests/direct_factory_happy_path.rs index 740b9c82..e983523e 100644 --- a/native/rust/signing/factories/tests/direct_factory_happy_path.rs +++ b/native/rust/signing/factories/tests/direct_factory_happy_path.rs @@ -75,9 +75,9 @@ impl MockSigningService { impl SigningService for MockSigningService { fn get_cose_signer(&self, _context: &SigningContext) -> Result { if self.should_fail_signer { - return Err(SigningError::SigningFailed( - "Mock signer creation failed".to_string(), - )); + return Err(SigningError::SigningFailed { + detail: "Mock signer creation failed".into(), + }); } let key = Box::new(MockKey); @@ -376,9 +376,9 @@ fn test_direct_factory_streaming_max_embed_size() { ); match result.unwrap_err() { - FactoryError::PayloadTooLargeForEmbedding(size, max_size) => { - assert_eq!(size, 1000); - assert_eq!(max_size, 500); + FactoryError::PayloadTooLargeForEmbedding { actual, max } => { + assert_eq!(actual, 1000); + assert_eq!(max, 500); } _ => panic!("Expected PayloadTooLargeForEmbedding error"), } @@ -396,7 +396,7 @@ fn test_direct_factory_error_from_signing_service() { assert!(result.is_err(), "Should fail when signing service fails"); match result.unwrap_err() { - FactoryError::SigningFailed(_) => { + FactoryError::SigningFailed { .. } => { // Expected } _ => panic!("Expected SigningFailed error"), @@ -415,8 +415,8 @@ fn test_direct_factory_verification_failure() { assert!(result.is_err(), "Should fail when verification fails"); match result.unwrap_err() { - FactoryError::VerificationFailed(msg) => { - assert!(msg.contains("Post-sign verification failed")); + FactoryError::VerificationFailed { detail } => { + assert!(detail.contains("Post-sign verification failed")); } _ => panic!("Expected VerificationFailed error"), } @@ -425,35 +425,47 @@ fn test_direct_factory_verification_failure() { #[test] fn test_factory_error_display() { // Test all FactoryError variants for Display implementation - let signing_failed = FactoryError::SigningFailed("test signing error".to_string()); + let signing_failed = FactoryError::SigningFailed { + detail: "test signing error".into(), + }; assert_eq!( format!("{}", signing_failed), "Signing failed: test signing error" ); - let verification_failed = FactoryError::VerificationFailed("test verify error".to_string()); + let verification_failed = FactoryError::VerificationFailed { + detail: "test verify error".into(), + }; assert_eq!( format!("{}", verification_failed), "Verification failed: test verify error" ); - let invalid_input = FactoryError::InvalidInput("test input error".to_string()); + let invalid_input = FactoryError::InvalidInput { + detail: "test input error".into(), + }; assert_eq!( format!("{}", invalid_input), "Invalid input: test input error" ); - let cbor_error = FactoryError::CborError("test cbor error".to_string()); + let cbor_error = FactoryError::CborError { + detail: "test cbor error".into(), + }; assert_eq!(format!("{}", cbor_error), "CBOR error: test cbor error"); - let transparency_failed = - FactoryError::TransparencyFailed("test transparency error".to_string()); + let transparency_failed = FactoryError::TransparencyFailed { + detail: "test transparency error".into(), + }; assert_eq!( format!("{}", transparency_failed), "Transparency failed: test transparency error" ); - let payload_too_large = FactoryError::PayloadTooLargeForEmbedding(1000, 500); + let payload_too_large = FactoryError::PayloadTooLargeForEmbedding { + actual: 1000, + max: 500, + }; assert_eq!( format!("{}", payload_too_large), "Payload too large for embedding: 1000 bytes (max 500)" diff --git a/native/rust/signing/factories/tests/error_tests.rs b/native/rust/signing/factories/tests/error_tests.rs index f7a9267d..845b04df 100644 --- a/native/rust/signing/factories/tests/error_tests.rs +++ b/native/rust/signing/factories/tests/error_tests.rs @@ -9,13 +9,17 @@ use cose_sign1_signing::SigningError; #[test] fn test_factory_error_display_signing_failed() { - let error = FactoryError::SigningFailed("Test signing failure".to_string()); + let error = FactoryError::SigningFailed { + detail: "Test signing failure".into(), + }; assert_eq!(error.to_string(), "Signing failed: Test signing failure"); } #[test] fn test_factory_error_display_verification_failed() { - let error = FactoryError::VerificationFailed("Test verification failure".to_string()); + let error = FactoryError::VerificationFailed { + detail: "Test verification failure".into(), + }; assert_eq!( error.to_string(), "Verification failed: Test verification failure" @@ -24,19 +28,25 @@ fn test_factory_error_display_verification_failed() { #[test] fn test_factory_error_display_invalid_input() { - let error = FactoryError::InvalidInput("Test invalid input".to_string()); + let error = FactoryError::InvalidInput { + detail: "Test invalid input".into(), + }; assert_eq!(error.to_string(), "Invalid input: Test invalid input"); } #[test] fn test_factory_error_display_cbor_error() { - let error = FactoryError::CborError("Test CBOR error".to_string()); + let error = FactoryError::CborError { + detail: "Test CBOR error".into(), + }; assert_eq!(error.to_string(), "CBOR error: Test CBOR error"); } #[test] fn test_factory_error_display_transparency_failed() { - let error = FactoryError::TransparencyFailed("Test transparency failure".to_string()); + let error = FactoryError::TransparencyFailed { + detail: "Test transparency failure".into(), + }; assert_eq!( error.to_string(), "Transparency failed: Test transparency failure" @@ -45,7 +55,10 @@ fn test_factory_error_display_transparency_failed() { #[test] fn test_factory_error_display_payload_too_large() { - let error = FactoryError::PayloadTooLargeForEmbedding(100, 50); + let error = FactoryError::PayloadTooLargeForEmbedding { + actual: 100, + max: 50, + }; assert_eq!( error.to_string(), "Payload too large for embedding: 100 bytes (max 50)" @@ -54,18 +67,22 @@ fn test_factory_error_display_payload_too_large() { #[test] fn test_factory_error_is_error_trait() { - let error = FactoryError::SigningFailed("test".to_string()); + let error = FactoryError::SigningFailed { + detail: "test".into(), + }; assert!(std::error::Error::source(&error).is_none()); } #[test] fn test_from_signing_error_verification_failed() { - let signing_error = SigningError::VerificationFailed("verification failed".to_string()); + let signing_error = SigningError::VerificationFailed { + detail: "verification failed".into(), + }; let factory_error: FactoryError = signing_error.into(); match factory_error { - FactoryError::VerificationFailed(msg) => { - assert_eq!(msg, "verification failed"); + FactoryError::VerificationFailed { detail } => { + assert_eq!(detail, "verification failed"); } _ => panic!("Expected VerificationFailed variant"), } @@ -73,12 +90,14 @@ fn test_from_signing_error_verification_failed() { #[test] fn test_from_signing_error_other_variants() { - let signing_error = SigningError::InvalidConfiguration("test context error".to_string()); + let signing_error = SigningError::InvalidConfiguration { + detail: "test context error".into(), + }; let factory_error: FactoryError = signing_error.into(); match factory_error { - FactoryError::SigningFailed(msg) => { - assert!(msg.contains("Invalid configuration")); + FactoryError::SigningFailed { detail } => { + assert!(detail.contains("Invalid configuration")); } _ => panic!("Expected SigningFailed variant"), } @@ -90,8 +109,8 @@ fn test_from_cose_sign1_error() { let factory_error: FactoryError = cose_error.into(); match factory_error { - FactoryError::SigningFailed(msg) => { - assert!(msg.contains("invalid message")); + FactoryError::SigningFailed { detail } => { + assert!(detail.contains("invalid message")); } _ => panic!("Expected SigningFailed variant"), } @@ -99,7 +118,10 @@ fn test_from_cose_sign1_error() { #[test] fn test_factory_error_debug_formatting() { - let error = FactoryError::PayloadTooLargeForEmbedding(1024, 512); + let error = FactoryError::PayloadTooLargeForEmbedding { + actual: 1024, + max: 512, + }; let debug_str = format!("{:?}", error); assert!(debug_str.contains("PayloadTooLargeForEmbedding")); assert!(debug_str.contains("1024")); diff --git a/native/rust/signing/factories/tests/extensible_factory_test.rs b/native/rust/signing/factories/tests/extensible_factory_test.rs index b1e8428e..b8bfb619 100644 --- a/native/rust/signing/factories/tests/extensible_factory_test.rs +++ b/native/rust/signing/factories/tests/extensible_factory_test.rs @@ -100,9 +100,12 @@ impl SignatureFactoryProvider for CustomFactory { options: &dyn Any, ) -> Result, FactoryError> { // Downcast options to CustomOptions - let custom_opts = options - .downcast_ref::() - .ok_or_else(|| FactoryError::InvalidInput("Expected CustomOptions".to_string()))?; + let custom_opts = + options + .downcast_ref::() + .ok_or_else(|| FactoryError::InvalidInput { + detail: "Expected CustomOptions".into(), + })?; // For testing, just use direct signature with the custom field in AAD let mut context = SigningContext::from_bytes(payload.to_vec()); @@ -124,9 +127,9 @@ impl SignatureFactoryProvider for CustomFactory { .verify_signature(&message_bytes, &context)?; if !verification_result { - return Err(FactoryError::VerificationFailed( - "Post-sign verification failed".to_string(), - )); + return Err(FactoryError::VerificationFailed { + detail: "Post-sign verification failed".into(), + }); } Ok(message_bytes) @@ -139,7 +142,9 @@ impl SignatureFactoryProvider for CustomFactory { options: &dyn Any, ) -> Result { let bytes = self.create_bytes_dyn(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } } @@ -264,9 +269,9 @@ fn test_create_with_unregistered_type_fails() { ); match result.unwrap_err() { - FactoryError::SigningFailed(msg) => { + FactoryError::SigningFailed { detail } => { assert!( - msg.contains("No factory registered"), + detail.contains("No factory registered"), "Error should mention unregistered factory" ); } diff --git a/native/rust/signing/factories/tests/factory_tests.rs b/native/rust/signing/factories/tests/factory_tests.rs index d44cf5b5..a83c561b 100644 --- a/native/rust/signing/factories/tests/factory_tests.rs +++ b/native/rust/signing/factories/tests/factory_tests.rs @@ -281,9 +281,11 @@ fn test_factory_register_custom_factory() { _content_type: &str, options: &dyn Any, ) -> Result, FactoryError> { - let _opts = options - .downcast_ref::() - .ok_or_else(|| FactoryError::InvalidInput("Expected TestOptions".to_string()))?; + let _opts = options.downcast_ref::().ok_or_else(|| { + FactoryError::InvalidInput { + detail: "Expected TestOptions".into(), + } + })?; let context = SigningContext::from_bytes(payload.to_vec()); let signer = self.signing_service.get_cose_signer(&context)?; @@ -301,9 +303,9 @@ fn test_factory_register_custom_factory() { .verify_signature(&message_bytes, &context)?; if !verification_result { - return Err(FactoryError::VerificationFailed( - "Post-sign verification failed".to_string(), - )); + return Err(FactoryError::VerificationFailed { + detail: "Post-sign verification failed".into(), + }); } Ok(message_bytes) @@ -316,7 +318,9 @@ fn test_factory_register_custom_factory() { options: &dyn Any, ) -> Result { let bytes = self.create_bytes_dyn(payload, content_type, options)?; - CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed(e.to_string())) + CoseSign1Message::parse(&bytes).map_err(|e| FactoryError::SigningFailed { + detail: e.to_string().into(), + }) } } @@ -347,9 +351,9 @@ fn test_factory_create_with_unregistered_type_error_message() { assert!(result.is_err()); match result.unwrap_err() { - FactoryError::SigningFailed(msg) => { - assert!(msg.contains("No factory registered")); - assert!(msg.contains("UnregisteredOptions")); + FactoryError::SigningFailed { detail } => { + assert!(detail.contains("No factory registered")); + assert!(detail.contains("UnregisteredOptions")); } _ => panic!("Expected SigningFailed error with type name"), } diff --git a/native/rust/signing/factories/tests/new_factory_coverage.rs b/native/rust/signing/factories/tests/new_factory_coverage.rs index 9c98df10..3260fa8c 100644 --- a/native/rust/signing/factories/tests/new_factory_coverage.rs +++ b/native/rust/signing/factories/tests/new_factory_coverage.rs @@ -14,19 +14,31 @@ use cose_sign1_factories::FactoryError; #[test] fn error_display_all_variants() { let cases: Vec<(FactoryError, &str)> = vec![ - (FactoryError::SigningFailed("s".into()), "Signing failed: s"), ( - FactoryError::VerificationFailed("v".into()), + FactoryError::SigningFailed { detail: "s".into() }, + "Signing failed: s", + ), + ( + FactoryError::VerificationFailed { detail: "v".into() }, "Verification failed: v", ), - (FactoryError::InvalidInput("i".into()), "Invalid input: i"), - (FactoryError::CborError("c".into()), "CBOR error: c"), ( - FactoryError::TransparencyFailed("t".into()), + FactoryError::InvalidInput { detail: "i".into() }, + "Invalid input: i", + ), + ( + FactoryError::CborError { detail: "c".into() }, + "CBOR error: c", + ), + ( + FactoryError::TransparencyFailed { detail: "t".into() }, "Transparency failed: t", ), ( - FactoryError::PayloadTooLargeForEmbedding(200, 100), + FactoryError::PayloadTooLargeForEmbedding { + actual: 200, + max: 100, + }, "Payload too large for embedding: 200 bytes (max 100)", ), ]; @@ -37,7 +49,7 @@ fn error_display_all_variants() { #[test] fn error_implements_std_error() { - let err = FactoryError::CborError("x".into()); + let err = FactoryError::CborError { detail: "x".into() }; let trait_obj: &dyn std::error::Error = &err; assert!(trait_obj.source().is_none()); } diff --git a/native/rust/validation/core/ffi/src/lib.rs b/native/rust/validation/core/ffi/src/lib.rs index 079d6be5..93407f39 100644 --- a/native/rust/validation/core/ffi/src/lib.rs +++ b/native/rust/validation/core/ffi/src/lib.rs @@ -59,19 +59,22 @@ pub enum cose_status_t { COSE_INVALID_ARG = 3, } -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_sign1_validator_builder_t { pub packs: Vec>, pub compiled_plan: Option, } -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_sign1_validator_t { pub packs: Vec>, pub compiled_plan: Option, } -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_sign1_validation_result_t { pub ok: bool, pub failure_message: Option, @@ -82,7 +85,8 @@ pub struct cose_sign1_validation_result_t { /// This lives in the base FFI crate so optional pack FFI crates (certificates/MST/AKV) /// can add policy helper exports without depending on (and thereby statically duplicating) /// the trust FFI library. -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_trust_policy_builder_t { pub builder: Option, } diff --git a/native/rust/validation/primitives/ffi/src/lib.rs b/native/rust/validation/primitives/ffi/src/lib.rs index ead5979d..533f5621 100644 --- a/native/rust/validation/primitives/ffi/src/lib.rs +++ b/native/rust/validation/primitives/ffi/src/lib.rs @@ -38,13 +38,15 @@ use std::ffi::{c_char, CStr}; use std::ptr; use std::sync::Arc; -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_sign1_trust_plan_builder_t { packs: Vec>, selected_plans: Vec, } -#[repr(C)] +/// Opaque type used behind `*mut` pointers in FFI. Not intended to cross the ABI boundary by value. +#[allow(non_camel_case_types)] pub struct cose_sign1_compiled_trust_plan_t { bundled: CoseSign1CompiledTrustPlan, } diff --git a/native/rust/validation/primitives/src/audit.rs b/native/rust/validation/primitives/src/audit.rs index 3fe977a9..f8029d11 100644 --- a/native/rust/validation/primitives/src/audit.rs +++ b/native/rust/validation/primitives/src/audit.rs @@ -29,6 +29,7 @@ impl TrustDecisionAudit { } } +#[must_use = "builders do nothing unless consumed"] #[derive(Debug, Default)] pub struct TrustDecisionAuditBuilder { audit: TrustDecisionAudit, diff --git a/native/rust/validation/primitives/src/policy.rs b/native/rust/validation/primitives/src/policy.rs index b058a7fd..6ae815e1 100644 --- a/native/rust/validation/primitives/src/policy.rs +++ b/native/rust/validation/primitives/src/policy.rs @@ -28,6 +28,7 @@ impl TrustPolicy { } } +#[must_use = "builders do nothing unless consumed"] #[derive(Default)] pub struct TrustPolicyBuilder { policy: TrustPolicy, From 420c8c95e57d7ef99d71ffd228c05d0f23ca33cf Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Tue, 7 Apr 2026 15:34:53 -0700 Subject: [PATCH 7/8] Complete Cow<'static, str> migration: headers, MST, validation, AKV error types - HeaderError: All String variants -> Cow<'static, str> (39 call sites in cwt_claims.rs) - ReceiptVerifyError: All 7 String variants -> Cow<'static, str> (38 call sites) - ValidationFailure: message/error_code/property_name/attempted_value/exception -> Cow - CoseSign1ValidationError: String variants -> Cow<'static, str> - AKV: key_type/curve_name fields -> Cow<'static, str> with static lookups - AKV: Service metadata literals use Cow::Borrowed - Validation FFI: .into_owned() at boundary for FFI String requirement - All 7,886 tests pass, clippy clean, rustfmt clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/common/akv_key_client.rs | 27 +-- .../src/signing/akv_signing_key.rs | 2 +- .../src/signing/akv_signing_service.rs | 4 +- .../azure_key_vault/src/validation/pack.rs | 12 +- .../mst/src/validation/receipt_verify.rs | 170 +++++++++--------- .../mst/tests/deep_mst_coverage.rs | 15 +- .../mst/tests/final_targeted_mst_coverage.rs | 11 +- .../receipt_verify_comprehensive_coverage.rs | 17 +- .../mst/tests/receipt_verify_coverage.rs | 15 +- .../mst/tests/receipt_verify_extended.rs | 14 +- .../headers/ffi/tests/coverage_boost.rs | 14 +- native/rust/signing/headers/src/cwt_claims.rs | 123 ++++++------- native/rust/signing/headers/src/error.rs | 16 +- .../headers/tests/cwt_claims_deep_coverage.rs | 16 +- .../rust/signing/headers/tests/error_tests.rs | 16 +- native/rust/validation/core/ffi/src/lib.rs | 2 +- .../validation/core/src/indirect_signature.rs | 29 +-- .../core/src/message_fact_producer.rs | 6 +- .../rust/validation/core/src/message_facts.rs | 2 +- native/rust/validation/core/src/validator.rs | 72 +++++--- .../tests/additional_validator_coverage.rs | 8 - .../tests/async_and_streaming_coverage.rs | 4 +- .../core/tests/final_targeted_coverage.rs | 4 +- .../core/tests/final_validator_coverage.rs | 32 ++-- .../core/tests/targeted_coverage_gaps.rs | 4 +- .../core/tests/v2_validator_parity.rs | 16 +- .../validation_result_helper_coverage.rs | 34 ++-- .../tests/validator_additional_coverage.rs | 65 ++++--- .../core/tests/validator_async_tests.rs | 24 +-- .../tests/validator_comprehensive_coverage.rs | 3 +- .../core/tests/validator_deep_coverage.rs | 12 +- .../core/tests/validator_error_paths.rs | 32 ++-- .../tests/validator_final_coverage_gaps.rs | 42 ++--- .../core/tests/validator_pipeline_tests.rs | 16 +- .../tests/validator_simple_coverage_gaps.rs | 18 +- 35 files changed, 462 insertions(+), 435 deletions(-) diff --git a/native/rust/extension_packs/azure_key_vault/src/common/akv_key_client.rs b/native/rust/extension_packs/azure_key_vault/src/common/akv_key_client.rs index 332967af..c1a863f6 100644 --- a/native/rust/extension_packs/azure_key_vault/src/common/akv_key_client.rs +++ b/native/rust/extension_packs/azure_key_vault/src/common/akv_key_client.rs @@ -10,6 +10,7 @@ use azure_security_keyvault_keys::{ models::{CurveName, KeyClientGetKeyOptions, KeyType, SignParameters, SignatureAlgorithm}, KeyClient, }; +use std::borrow::Cow; use std::sync::Arc; /// Concrete AKV crypto client wrapping `azure_security_keyvault_keys::KeyClient`. @@ -17,9 +18,9 @@ pub struct AkvKeyClient { client: KeyClient, key_name: String, key_version: Option, - key_type: String, + key_type: Cow<'static, str>, key_size: Option, - curve_name: Option, + curve_name: Option>, key_id: String, is_hsm: bool, /// EC public key x-coordinate (base64url-decoded). @@ -102,18 +103,18 @@ impl AkvKeyClient { // Map JWK key type and curve to canonical strings via pattern matching. // This avoids Debug-formatting key-response fields (cleartext-logging). - let key_type = match jwk.kty.as_ref() { - Some(KeyType::Ec | KeyType::EcHsm) => "EC".to_string(), - Some(KeyType::Rsa | KeyType::RsaHsm) => "RSA".to_string(), - Some(KeyType::Oct | KeyType::OctHsm) => "Oct".to_string(), - _ => String::new(), + let key_type: Cow<'static, str> = match jwk.kty.as_ref() { + Some(KeyType::Ec | KeyType::EcHsm) => Cow::Borrowed("EC"), + Some(KeyType::Rsa | KeyType::RsaHsm) => Cow::Borrowed("RSA"), + Some(KeyType::Oct | KeyType::OctHsm) => Cow::Borrowed("Oct"), + _ => Cow::Borrowed(""), }; - let curve_name = jwk.crv.as_ref().map(|c| match c { - CurveName::P256 => "P-256".to_string(), - CurveName::P256K => "P-256K".to_string(), - CurveName::P384 => "P-384".to_string(), - CurveName::P521 => "P-521".to_string(), - _ => "Unknown".to_string(), + let curve_name: Option> = jwk.crv.as_ref().map(|c| match c { + CurveName::P256 => Cow::Borrowed("P-256"), + CurveName::P256K => Cow::Borrowed("P-256K"), + CurveName::P384 => Cow::Borrowed("P-384"), + CurveName::P521 => Cow::Borrowed("P-521"), + _ => Cow::Borrowed("Unknown"), }); // Extract key version: prefer caller-supplied, fall back to the last // segment of the kid URL in the response. The version string is diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs index e5523828..1677a787 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_key.rs @@ -28,7 +28,7 @@ fn determine_cose_algorithm(key_type: &str, curve: Option<&str>) -> Result { let curve_name = curve - .ok_or_else(|| AkvError::InvalidKeyType("EC key missing curve name".to_string()))?; + .ok_or_else(|| AkvError::InvalidKeyType("EC key missing curve name".into()))?; curve_to_cose_algorithm(curve_name).ok_or_else(|| { AkvError::InvalidKeyType(format!("Unsupported EC curve: {}", curve_name)) }) diff --git a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs index a2a07ef3..6bfcd85b 100644 --- a/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs +++ b/native/rust/extension_packs/azure_key_vault/src/signing/akv_signing_service.rs @@ -43,8 +43,8 @@ impl AzureKeyVaultSigningService { let signing_key = AzureKeyVaultSigningKey::new(crypto_client)?; let service_metadata = SigningServiceMetadata::new( - "AzureKeyVault".to_string(), - "Azure Key Vault signing service".to_string(), + "AzureKeyVault".into(), + "Azure Key Vault signing service".into(), ); let kid_contributor = KeyIdHeaderContributor::new(key_id); diff --git a/native/rust/extension_packs/azure_key_vault/src/validation/pack.rs b/native/rust/extension_packs/azure_key_vault/src/validation/pack.rs index d2b5b418..589c842d 100644 --- a/native/rust/extension_packs/azure_key_vault/src/validation/pack.rs +++ b/native/rust/extension_packs/azure_key_vault/src/validation/pack.rs @@ -33,8 +33,8 @@ impl Default for AzureKeyVaultTrustOptions { // Secure-by-default: only allow Microsoft-owned Key Vault namespaces. Self { allowed_kid_patterns: vec![ - "https://*.vault.azure.net/keys/*".to_string(), - "https://*.managedhsm.azure.net/keys/*".to_string(), + "https://*.vault.azure.net/keys/*".into(), + "https://*.managedhsm.azure.net/keys/*".into(), ], require_azure_key_vault_kid: true, } @@ -213,9 +213,9 @@ impl TrustFactProducer for AzureKeyVaultTrustPack { })?; let (is_allowed, details) = if self.options.require_azure_key_vault_kid && !is_akv { - (false, Some("NoPatternMatch".to_string())) + (false, Some("NoPatternMatch".into())) } else if self.compiled_patterns.is_none() { - (false, Some("NoAllowedPatterns".to_string())) + (false, Some("NoAllowedPatterns".into())) } else { let matched = self .compiled_patterns @@ -224,9 +224,9 @@ impl TrustFactProducer for AzureKeyVaultTrustPack { ( matched, Some(if matched { - "PatternMatched".to_string() + "PatternMatched".into() } else { - "NoPatternMatch".to_string() + "NoPatternMatch".into() }), ) }; diff --git a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs index c0024420..66b8c782 100644 --- a/native/rust/extension_packs/mst/src/validation/receipt_verify.rs +++ b/native/rust/extension_packs/mst/src/validation/receipt_verify.rs @@ -49,7 +49,7 @@ pub fn base64url_decode(input: &str) -> Result, String> { #[derive(Debug)] pub enum ReceiptVerifyError { - ReceiptDecode(String), + ReceiptDecode(Cow<'static, str>), MissingAlg, MissingKid, UnsupportedAlg(i64), @@ -57,12 +57,12 @@ pub enum ReceiptVerifyError { MissingVdp, MissingProof, MissingIssuer, - JwksParse(String), - JwksFetch(String), - JwkNotFound(String), - JwkUnsupported(String), - StatementReencode(String), - SigStructureEncode(String), + JwksParse(Cow<'static, str>), + JwksFetch(Cow<'static, str>), + JwkNotFound(Cow<'static, str>), + JwkUnsupported(Cow<'static, str>), + StatementReencode(Cow<'static, str>), + SigStructureEncode(Cow<'static, str>), DataHashMismatch, SignatureInvalid, } @@ -160,7 +160,7 @@ pub fn verify_mst_receipt( input: ReceiptVerifyInput<'_>, ) -> Result { let receipt = CoseSign1Message::parse(input.receipt_bytes) - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; // Extract receipt headers using typed CoseHeaderMap accessors. let alg = receipt @@ -216,7 +216,7 @@ pub fn verify_mst_receipt( let verifier = input .jwk_verifier_factory .verifier_from_ec_jwk(&ec_jwk, alg) - .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}")))?; + .map_err(|e| ReceiptVerifyError::JwkUnsupported(format!("jwk_verifier: {e}").into()))?; // Convert to Arc for cheap cloning in fact production. let issuer: Arc = Arc::from(issuer); @@ -259,7 +259,7 @@ pub fn verify_mst_receipt( let sig_structure = receipt .sig_structure_bytes(acc.as_slice(), None) - .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::SigStructureEncode(e.to_string().into()))?; if let Ok(true) = verifier.verify(sig_structure.as_slice(), receipt.signature()) { return Ok(ReceiptVerifyOutput { trusted: true, @@ -301,11 +301,11 @@ pub fn sha256_concat_slices(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { pub fn reencode_statement_with_cleared_unprotected_headers( statement_bytes: &[u8], ) -> Result, ReceiptVerifyError> { - let was_tagged = - is_cose_sign1_tagged_18(statement_bytes).map_err(ReceiptVerifyError::StatementReencode)?; + let was_tagged = is_cose_sign1_tagged_18(statement_bytes) + .map_err(|e| ReceiptVerifyError::StatementReencode(e.into()))?; let msg = CoseSign1Message::parse(statement_bytes) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; // Match .NET verifier behavior: clear *all* unprotected headers. @@ -315,30 +315,30 @@ pub fn reencode_statement_with_cleared_unprotected_headers( if was_tagged { // tag(18) is a single-byte CBOR tag header: 0xD2. enc.encode_tag(18) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; } enc.encode_array(4) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; // protected header bytes are a bstr (containing map bytes) enc.encode_bstr(msg.protected.as_bytes()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; // unprotected header: empty map enc.encode_map(0) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; // payload: bstr / nil match msg.payload() { Some(p) => enc.encode_bstr(p), None => enc.encode_null(), } - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; // signature: bstr enc.encode_bstr(msg.signature()) - .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::StatementReencode(e.to_string().into()))?; Ok(enc.into_bytes()) } @@ -372,9 +372,9 @@ pub(crate) fn resolve_receipt_signing_key( } if !allow_network_fetch { - return Err(ReceiptVerifyError::JwksParse( - "MissingOfflineJwks".to_string(), - )); + return Err(ReceiptVerifyError::JwksParse(Cow::Borrowed( + "MissingOfflineJwks", + ))); } let jwks_json = fetch_jwks_for_issuer(issuer, jwks_api_version, client)?; @@ -390,7 +390,7 @@ pub(crate) fn fetch_jwks_for_issuer( if let Some(ct_client) = client { return ct_client .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())); + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string().into())); } // Create a temporary client for the issuer endpoint @@ -401,7 +401,7 @@ pub(crate) fn fetch_jwks_for_issuer( }; let endpoint = - url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string()))?; + url::Url::parse(&base).map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string().into()))?; let mut config = code_transparency_client::CodeTransparencyClientConfig::default(); if let Some(v) = jwks_api_version { @@ -411,7 +411,7 @@ pub(crate) fn fetch_jwks_for_issuer( let temp_client = code_transparency_client::CodeTransparencyClient::new(endpoint, config); temp_client .get_public_keys() - .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string())) + .map_err(|e| ReceiptVerifyError::JwksFetch(e.to_string().into())) } #[derive(Clone, Debug)] @@ -432,7 +432,7 @@ impl MstCcfInclusionProof { let mut d = cose_sign1_primitives::provider::decoder(proof_blob); let map_len = d .decode_map_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; let mut leaf_raw: Option> = None; let mut path: Option> = None; @@ -440,23 +440,23 @@ impl MstCcfInclusionProof { for _ in 0..map_len.unwrap_or(usize::MAX) { let k = d .decode_i64() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; if k == 1 { leaf_raw = Some( d.decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))? .to_vec(), ); } else if k == 2 { let v_raw = d .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))? .to_vec(); path = Some(parse_path(&v_raw)?); } else { // Skip unknown keys d.skip() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; } } @@ -477,33 +477,37 @@ pub fn parse_leaf(leaf_bytes: &[u8]) -> Result<([u8; 32], String, [u8; 32]), Rec let mut d = cose_sign1_primitives::provider::decoder(leaf_bytes); let _arr_len = d .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; let internal_txn_hash_slice = d.decode_bstr().map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e)) + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_txn_hash: {}", e).into()) })?; let internal_txn_hash: [u8; 32] = internal_txn_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_internal_txn_hash_len: {}", - internal_txn_hash_slice.len() - )) + ReceiptVerifyError::ReceiptDecode( + format!( + "unexpected_internal_txn_hash_len: {}", + internal_txn_hash_slice.len() + ) + .into(), + ) })?; let internal_evidence = d .decode_tstr() .map_err(|e| { - ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_internal_evidence: {}", e)) + ReceiptVerifyError::ReceiptDecode( + format!("leaf_missing_internal_evidence: {}", e).into(), + ) })? .to_string(); - let data_hash_slice = d - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e)))?; + let data_hash_slice = d.decode_bstr().map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("leaf_missing_data_hash: {}", e).into()) + })?; let data_hash: [u8; 32] = data_hash_slice.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_data_hash_len: {}", - data_hash_slice.len() - )) + ReceiptVerifyError::ReceiptDecode( + format!("unexpected_data_hash_len: {}", data_hash_slice.len()).into(), + ) })?; Ok((internal_txn_hash, internal_evidence, data_hash)) @@ -514,31 +518,30 @@ pub fn parse_path(bytes: &[u8]) -> Result, ReceiptVerifyEr let mut d = cose_sign1_primitives::provider::decoder(bytes); let arr_len = d .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; let mut out = Vec::new(); for _ in 0..arr_len.unwrap_or(usize::MAX) { let item_raw = d .decode_raw() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))? + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))? .to_vec(); let mut vd = cose_sign1_primitives::provider::decoder(&item_raw); let _pair_len = vd .decode_array_len() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::ReceiptDecode(e.to_string().into()))?; - let is_left = vd - .decode_bool() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e)))?; + let is_left = vd.decode_bool().map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("path_missing_dir: {}", e).into()) + })?; - let bytes_item = vd - .decode_bstr() - .map_err(|e| ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e)))?; + let bytes_item = vd.decode_bstr().map_err(|e| { + ReceiptVerifyError::ReceiptDecode(format!("path_missing_hash: {}", e).into()) + })?; let hash: [u8; 32] = bytes_item.try_into().map_err(|_| { - ReceiptVerifyError::ReceiptDecode(format!( - "unexpected_path_hash_len: {}", - bytes_item.len() - )) + ReceiptVerifyError::ReceiptDecode( + format!("unexpected_path_hash_len: {}", bytes_item.len()).into(), + ) })?; out.push((is_left, hash)); @@ -557,9 +560,9 @@ pub fn extract_proof_blobs( let pairs = match vdp_value { CoseHeaderValue::Map(pairs) => pairs, _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "vdp_not_a_map".to_string(), - )) + return Err(ReceiptVerifyError::ReceiptDecode(Cow::Borrowed( + "vdp_not_a_map", + ))) } }; @@ -571,9 +574,9 @@ pub fn extract_proof_blobs( let arr = match value { CoseHeaderValue::Array(arr) => arr, _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_not_array".to_string(), - )) + return Err(ReceiptVerifyError::ReceiptDecode(Cow::Borrowed( + "proof_not_array", + ))) } }; @@ -582,9 +585,9 @@ pub fn extract_proof_blobs( match item { CoseHeaderValue::Bytes(b) => out.push(b.clone()), _ => { - return Err(ReceiptVerifyError::ReceiptDecode( - "proof_item_not_bstr".to_string(), - )) + return Err(ReceiptVerifyError::ReceiptDecode(Cow::Borrowed( + "proof_item_not_bstr", + ))) } } } @@ -608,9 +611,9 @@ pub fn validate_cose_alg_supported(alg: i64) -> Result<(), ReceiptVerifyError> { /// Validate that the receipt `alg` is compatible with the JWK curve. pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), ReceiptVerifyError> { let Some(crv) = jwk.crv.as_deref() else { - return Err(ReceiptVerifyError::JwkUnsupported( - "missing_crv".to_string(), - )); + return Err(ReceiptVerifyError::JwkUnsupported(Cow::Borrowed( + "missing_crv", + ))); }; let ok = matches!( @@ -619,9 +622,9 @@ pub fn validate_receipt_alg_against_jwk(jwk: &Jwk, alg: i64) -> Result<(), Recei ); if !ok { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "alg_curve_mismatch: alg={alg} crv={crv}" - ))); + return Err(ReceiptVerifyError::JwkUnsupported( + format!("alg_curve_mismatch: alg={alg} crv={crv}").into(), + )); } Ok(()) } @@ -665,7 +668,7 @@ pub struct Jwk { pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result { let jwks: Jwks = serde_json::from_str(jwks_json) - .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string()))?; + .map_err(|e| ReceiptVerifyError::JwksParse(e.to_string().into()))?; for k in jwks.keys { if k.kid.as_deref() == Some(kid) { @@ -673,7 +676,7 @@ pub fn find_jwk_for_kid(jwks_json: &str, kid: &str) -> Result Result(jwk: &'a Jwk) -> Result, ReceiptVerifyError> { if jwk.kty != "EC" { - return Err(ReceiptVerifyError::JwkUnsupported(format!( - "kty={}", - jwk.kty - ))); + return Err(ReceiptVerifyError::JwkUnsupported( + format!("kty={}", jwk.kty).into(), + )); } let crv = jwk .crv .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_crv".to_string()))?; + .ok_or(ReceiptVerifyError::JwkUnsupported(Cow::Borrowed( + "missing_crv", + )))?; let x = jwk .x .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_x".to_string()))?; + .ok_or(ReceiptVerifyError::JwkUnsupported(Cow::Borrowed( + "missing_x", + )))?; let y = jwk .y .as_deref() - .ok_or_else(|| ReceiptVerifyError::JwkUnsupported("missing_y".to_string()))?; + .ok_or(ReceiptVerifyError::JwkUnsupported(Cow::Borrowed( + "missing_y", + )))?; Ok(EcJwk { kty: Cow::Borrowed(&jwk.kty), diff --git a/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs b/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs index 5822be43..a93e5833 100644 --- a/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/deep_mst_coverage.rs @@ -22,6 +22,7 @@ extern crate cbor_primitives_everparse; use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderValue}; use cose_sign1_transparent_mst::validation::receipt_verify::*; use crypto_primitives::EcJwk; +use std::borrow::Cow; // ========================================================================= // ReceiptVerifyError Display coverage @@ -29,7 +30,7 @@ use crypto_primitives::EcJwk; #[test] fn error_display_receipt_decode() { - let e = ReceiptVerifyError::ReceiptDecode("bad cbor".to_string()); + let e = ReceiptVerifyError::ReceiptDecode(Cow::Borrowed("bad cbor")); let s = format!("{}", e); assert!(s.contains("receipt_decode_failed")); assert!(s.contains("bad cbor")); @@ -90,38 +91,38 @@ fn error_display_missing_issuer() { #[test] fn error_display_jwks_parse() { - let e = ReceiptVerifyError::JwksParse("bad json".to_string()); + let e = ReceiptVerifyError::JwksParse(Cow::Borrowed("bad json")); assert!(format!("{}", e).contains("jwks_parse_failed")); } #[test] fn error_display_jwks_fetch() { - let e = ReceiptVerifyError::JwksFetch("network error".to_string()); + let e = ReceiptVerifyError::JwksFetch(Cow::Borrowed("network error")); assert!(format!("{}", e).contains("jwks_fetch_failed")); } #[test] fn error_display_jwk_not_found() { - let e = ReceiptVerifyError::JwkNotFound("kid123".to_string()); + let e = ReceiptVerifyError::JwkNotFound(Cow::Borrowed("kid123")); assert!(format!("{}", e).contains("jwk_not_found_for_kid")); assert!(format!("{}", e).contains("kid123")); } #[test] fn error_display_jwk_unsupported() { - let e = ReceiptVerifyError::JwkUnsupported("rsa".to_string()); + let e = ReceiptVerifyError::JwkUnsupported(Cow::Borrowed("rsa")); assert!(format!("{}", e).contains("jwk_unsupported")); } #[test] fn error_display_statement_reencode() { - let e = ReceiptVerifyError::StatementReencode("cbor fail".to_string()); + let e = ReceiptVerifyError::StatementReencode(Cow::Borrowed("cbor fail")); assert!(format!("{}", e).contains("statement_reencode_failed")); } #[test] fn error_display_sig_structure_encode() { - let e = ReceiptVerifyError::SigStructureEncode("sig fail".to_string()); + let e = ReceiptVerifyError::SigStructureEncode(Cow::Borrowed("sig fail")); assert!(format!("{}", e).contains("sig_structure_encode_failed")); } diff --git a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs index 125deddd..81550269 100644 --- a/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs +++ b/native/rust/extension_packs/mst/tests/final_targeted_mst_coverage.rs @@ -765,23 +765,26 @@ fn test_receipt_verify_error_display_all_variants() { assert_eq!( format!( "{}", - ReceiptVerifyError::SigStructureEncode("err".to_string()) + ReceiptVerifyError::SigStructureEncode(Cow::Borrowed("err")) ), "sig_structure_encode_failed: err" ); assert_eq!( format!( "{}", - ReceiptVerifyError::StatementReencode("re".to_string()) + ReceiptVerifyError::StatementReencode(Cow::Borrowed("re")) ), "statement_reencode_failed: re" ); assert_eq!( - format!("{}", ReceiptVerifyError::JwkUnsupported("un".to_string())), + format!( + "{}", + ReceiptVerifyError::JwkUnsupported(Cow::Borrowed("un")) + ), "jwk_unsupported: un" ); assert_eq!( - format!("{}", ReceiptVerifyError::JwksFetch("fetch".to_string())), + format!("{}", ReceiptVerifyError::JwksFetch(Cow::Borrowed("fetch"))), "jwks_fetch_failed: fetch" ); } diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs b/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs index db5a834e..75665b23 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_comprehensive_coverage.rs @@ -11,6 +11,7 @@ use cose_sign1_primitives::{CoseHeaderLabel, CoseHeaderValue}; use cose_sign1_transparent_mst::validation::*; use sha2::{Digest, Sha256}; +use std::borrow::Cow; // Test validate_cose_alg_supported function #[test] @@ -300,7 +301,7 @@ fn test_extract_proof_blobs_multiple_labels() { #[test] fn test_receipt_verify_error_display() { let errors = vec![ - ReceiptVerifyError::ReceiptDecode("test decode".to_string()), + ReceiptVerifyError::ReceiptDecode(Cow::Borrowed("test decode")), ReceiptVerifyError::MissingAlg, ReceiptVerifyError::MissingKid, ReceiptVerifyError::UnsupportedAlg(-999), @@ -308,12 +309,12 @@ fn test_receipt_verify_error_display() { ReceiptVerifyError::MissingVdp, ReceiptVerifyError::MissingProof, ReceiptVerifyError::MissingIssuer, - ReceiptVerifyError::JwksParse("parse error".to_string()), - ReceiptVerifyError::JwksFetch("fetch error".to_string()), - ReceiptVerifyError::JwkNotFound("test_kid".to_string()), - ReceiptVerifyError::JwkUnsupported("unsupported".to_string()), - ReceiptVerifyError::StatementReencode("reencode error".to_string()), - ReceiptVerifyError::SigStructureEncode("sig error".to_string()), + ReceiptVerifyError::JwksParse(Cow::Borrowed("parse error")), + ReceiptVerifyError::JwksFetch(Cow::Borrowed("fetch error")), + ReceiptVerifyError::JwkNotFound(Cow::Borrowed("test_kid")), + ReceiptVerifyError::JwkUnsupported(Cow::Borrowed("unsupported")), + ReceiptVerifyError::StatementReencode(Cow::Borrowed("reencode error")), + ReceiptVerifyError::SigStructureEncode(Cow::Borrowed("sig error")), ReceiptVerifyError::DataHashMismatch, ReceiptVerifyError::SignatureInvalid, ]; @@ -324,7 +325,7 @@ fn test_receipt_verify_error_display() { // Verify each error type has expected content in display string match &error { - ReceiptVerifyError::ReceiptDecode(msg) => assert!(display_str.contains(msg)), + ReceiptVerifyError::ReceiptDecode(msg) => assert!(display_str.contains(msg.as_ref())), ReceiptVerifyError::MissingAlg => assert!(display_str.contains("missing_alg")), ReceiptVerifyError::UnsupportedAlg(alg) => { assert!(display_str.contains(&alg.to_string())) diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs b/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs index 09449c44..f71626c4 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_coverage.rs @@ -7,6 +7,7 @@ use cose_sign1_crypto_openssl::jwk_verifier::OpenSslJwkVerifierFactory; use cose_sign1_transparent_mst::validation::receipt_verify::{ verify_mst_receipt, ReceiptVerifyError, ReceiptVerifyInput, ReceiptVerifyOutput, }; +use std::borrow::Cow; use std::sync::Arc; #[test] @@ -57,7 +58,7 @@ fn test_verify_mst_receipt_empty_bytes() { #[test] fn test_receipt_verify_error_display_receipt_decode() { - let error = ReceiptVerifyError::ReceiptDecode("invalid format".to_string()); + let error = ReceiptVerifyError::ReceiptDecode(Cow::Borrowed("invalid format")); let display = format!("{}", error); assert_eq!(display, "receipt_decode_failed: invalid format"); } @@ -113,42 +114,42 @@ fn test_receipt_verify_error_display_missing_issuer() { #[test] fn test_receipt_verify_error_display_jwks_parse() { - let error = ReceiptVerifyError::JwksParse("malformed json".to_string()); + let error = ReceiptVerifyError::JwksParse(Cow::Borrowed("malformed json")); let display = format!("{}", error); assert_eq!(display, "jwks_parse_failed: malformed json"); } #[test] fn test_receipt_verify_error_display_jwks_fetch() { - let error = ReceiptVerifyError::JwksFetch("network error".to_string()); + let error = ReceiptVerifyError::JwksFetch(Cow::Borrowed("network error")); let display = format!("{}", error); assert_eq!(display, "jwks_fetch_failed: network error"); } #[test] fn test_receipt_verify_error_display_jwk_not_found() { - let error = ReceiptVerifyError::JwkNotFound("key123".to_string()); + let error = ReceiptVerifyError::JwkNotFound(Cow::Borrowed("key123")); let display = format!("{}", error); assert_eq!(display, "jwk_not_found_for_kid: key123"); } #[test] fn test_receipt_verify_error_display_jwk_unsupported() { - let error = ReceiptVerifyError::JwkUnsupported("unsupported curve".to_string()); + let error = ReceiptVerifyError::JwkUnsupported(Cow::Borrowed("unsupported curve")); let display = format!("{}", error); assert_eq!(display, "jwk_unsupported: unsupported curve"); } #[test] fn test_receipt_verify_error_display_statement_reencode() { - let error = ReceiptVerifyError::StatementReencode("encoding failed".to_string()); + let error = ReceiptVerifyError::StatementReencode(Cow::Borrowed("encoding failed")); let display = format!("{}", error); assert_eq!(display, "statement_reencode_failed: encoding failed"); } #[test] fn test_receipt_verify_error_display_sig_structure_encode() { - let error = ReceiptVerifyError::SigStructureEncode("structure error".to_string()); + let error = ReceiptVerifyError::SigStructureEncode(Cow::Borrowed("structure error")); let display = format!("{}", error); assert_eq!(display, "sig_structure_encode_failed: structure error"); } diff --git a/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs b/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs index e536be2f..b4f7038c 100644 --- a/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs +++ b/native/rust/extension_packs/mst/tests/receipt_verify_extended.rs @@ -18,7 +18,7 @@ use cose_sign1_transparent_mst::validation::receipt_verify::{ #[test] fn test_receipt_verify_error_debug_all_variants() { let errors = vec![ - ReceiptVerifyError::ReceiptDecode("test".to_string()), + ReceiptVerifyError::ReceiptDecode(Cow::Borrowed("test")), ReceiptVerifyError::MissingAlg, ReceiptVerifyError::MissingKid, ReceiptVerifyError::UnsupportedAlg(-100), @@ -26,12 +26,12 @@ fn test_receipt_verify_error_debug_all_variants() { ReceiptVerifyError::MissingVdp, ReceiptVerifyError::MissingProof, ReceiptVerifyError::MissingIssuer, - ReceiptVerifyError::JwksParse("parse error".to_string()), - ReceiptVerifyError::JwksFetch("fetch error".to_string()), - ReceiptVerifyError::JwkNotFound("kid123".to_string()), - ReceiptVerifyError::JwkUnsupported("unsupported".to_string()), - ReceiptVerifyError::StatementReencode("reencode".to_string()), - ReceiptVerifyError::SigStructureEncode("sigstruct".to_string()), + ReceiptVerifyError::JwksParse(Cow::Borrowed("parse error")), + ReceiptVerifyError::JwksFetch(Cow::Borrowed("fetch error")), + ReceiptVerifyError::JwkNotFound(Cow::Borrowed("kid123")), + ReceiptVerifyError::JwkUnsupported(Cow::Borrowed("unsupported")), + ReceiptVerifyError::StatementReencode(Cow::Borrowed("reencode")), + ReceiptVerifyError::SigStructureEncode(Cow::Borrowed("sigstruct")), ReceiptVerifyError::DataHashMismatch, ReceiptVerifyError::SignatureInvalid, ]; diff --git a/native/rust/signing/headers/ffi/tests/coverage_boost.rs b/native/rust/signing/headers/ffi/tests/coverage_boost.rs index fba7abbb..b01bd3d0 100644 --- a/native/rust/signing/headers/ffi/tests/coverage_boost.rs +++ b/native/rust/signing/headers/ffi/tests/coverage_boost.rs @@ -52,7 +52,7 @@ fn free_error(err: *mut CoseCwtErrorHandle) { fn error_inner_from_header_error_cbor_encoding() { use cose_sign1_headers::HeaderError; - let err = HeaderError::CborEncodingError("test encode error".to_string()); + let err = HeaderError::CborEncodingError("test encode error".into()); let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_CBOR_ENCODE_FAILED); assert!(inner.message.contains("CBOR encoding error")); @@ -63,7 +63,7 @@ fn error_inner_from_header_error_cbor_encoding() { fn error_inner_from_header_error_cbor_decoding() { use cose_sign1_headers::HeaderError; - let err = HeaderError::CborDecodingError("test decode error".to_string()); + let err = HeaderError::CborDecodingError("test decode error".into()); let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_CBOR_DECODE_FAILED); assert!(inner.message.contains("CBOR decoding error")); @@ -76,8 +76,8 @@ fn error_inner_from_header_error_invalid_claim_type() { let err = HeaderError::InvalidClaimType { label: 42, - expected: "string".to_string(), - actual: "integer".to_string(), + expected: "string".into(), + actual: "integer".into(), }; let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_INVALID_ARGUMENT); @@ -89,7 +89,7 @@ fn error_inner_from_header_error_invalid_claim_type() { fn error_inner_from_header_error_missing_required_claim() { use cose_sign1_headers::HeaderError; - let err = HeaderError::MissingRequiredClaim("subject".to_string()); + let err = HeaderError::MissingRequiredClaim("subject".into()); let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_INVALID_ARGUMENT); assert!(inner.message.contains("subject")); @@ -100,7 +100,7 @@ fn error_inner_from_header_error_missing_required_claim() { fn error_inner_from_header_error_invalid_timestamp() { use cose_sign1_headers::HeaderError; - let err = HeaderError::InvalidTimestamp("not a number".to_string()); + let err = HeaderError::InvalidTimestamp("not a number".into()); let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_INVALID_ARGUMENT); assert!(inner.message.contains("timestamp")); @@ -111,7 +111,7 @@ fn error_inner_from_header_error_invalid_timestamp() { fn error_inner_from_header_error_complex_claim_value() { use cose_sign1_headers::HeaderError; - let err = HeaderError::ComplexClaimValue("nested array".to_string()); + let err = HeaderError::ComplexClaimValue("nested array".into()); let inner: ErrorInner = ErrorInner::from_header_error(&err); assert_eq!(inner.code, FFI_ERR_INVALID_ARGUMENT); assert!(inner.message.contains("complex")); diff --git a/native/rust/signing/headers/src/cwt_claims.rs b/native/rust/signing/headers/src/cwt_claims.rs index 8261b4d6..cac7bd70 100644 --- a/native/rust/signing/headers/src/cwt_claims.rs +++ b/native/rust/signing/headers/src/cwt_claims.rs @@ -94,70 +94,70 @@ impl CwtClaims { encoder .encode_map(count) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; // Encode standard claims (in label order per CBOR deterministic encoding) if let Some(issuer) = &self.issuer { encoder .encode_i64(CWTClaimsHeaderLabels::ISSUER) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_tstr(issuer) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(subject) = &self.subject { encoder .encode_i64(CWTClaimsHeaderLabels::SUBJECT) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_tstr(subject) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(audience) = &self.audience { encoder .encode_i64(CWTClaimsHeaderLabels::AUDIENCE) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_tstr(audience) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(exp) = self.expiration_time { encoder .encode_i64(CWTClaimsHeaderLabels::EXPIRATION_TIME) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_i64(exp) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(nbf) = self.not_before { encoder .encode_i64(CWTClaimsHeaderLabels::NOT_BEFORE) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_i64(nbf) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(iat) = self.issued_at { encoder .encode_i64(CWTClaimsHeaderLabels::ISSUED_AT) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_i64(iat) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } if let Some(cti) = &self.cwt_id { encoder .encode_i64(CWTClaimsHeaderLabels::CWT_ID) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; encoder .encode_bstr(cti) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } // Encode custom claims (sorted by label for deterministic encoding) @@ -168,33 +168,33 @@ impl CwtClaims { if let Some(value) = self.custom_claims.get(&label) { encoder .encode_i64(label) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; match value { CwtClaimValue::Text(s) => { encoder .encode_tstr(s) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } CwtClaimValue::Integer(i) => { encoder .encode_i64(*i) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } CwtClaimValue::Bytes(b) => { encoder .encode_bstr(b) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } CwtClaimValue::Bool(b) => { encoder .encode_bool(*b) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } CwtClaimValue::Float(f) => { encoder .encode_f64(*f) - .map_err(|e| HeaderError::CborEncodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborEncodingError(e.to_string().into()))?; } } } @@ -210,20 +210,19 @@ impl CwtClaims { // Expect a map let cbor_type = decoder .peek_type() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?; if cbor_type != CborType::Map { - return Err(HeaderError::CborDecodingError(format!( - "Expected CBOR map, got {:?}", - cbor_type - ))); + return Err(HeaderError::CborDecodingError( + format!("Expected CBOR map, got {:?}", cbor_type).into(), + )); } let map_len = decoder .decode_map_len() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))? + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))? .ok_or_else(|| { - HeaderError::CborDecodingError("Indefinite-length maps not supported".to_string()) + HeaderError::CborDecodingError("Indefinite-length maps not supported".into()) })?; let mut claims = CwtClaims::new(); @@ -232,17 +231,16 @@ impl CwtClaims { // Read the label (must be an integer) let label_type = decoder .peek_type() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?; let label = match label_type { CborType::UnsignedInt | CborType::NegativeInt => decoder .decode_i64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, _ => { - return Err(HeaderError::CborDecodingError(format!( - "CWT claim label must be integer, got {:?}", - label_type - ))); + return Err(HeaderError::CborDecodingError( + format!("CWT claim label must be integer, got {:?}", label_type).into(), + )); } }; @@ -252,86 +250,86 @@ impl CwtClaims { claims.issuer = Some( decoder .decode_tstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::SUBJECT => { claims.subject = Some( decoder .decode_tstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::AUDIENCE => { claims.audience = Some( decoder .decode_tstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::EXPIRATION_TIME => { claims.expiration_time = Some( decoder .decode_i64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::NOT_BEFORE => { claims.not_before = Some( decoder .decode_i64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::ISSUED_AT => { claims.issued_at = Some( decoder .decode_i64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } CWTClaimsHeaderLabels::CWT_ID => { claims.cwt_id = Some( decoder .decode_bstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?, + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?, ); } _ => { // Custom claim - peek type and decode appropriately let value_type = decoder .peek_type() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + .map_err(|e| HeaderError::CborDecodingError(e.to_string().into()))?; let claim_value = match value_type { CborType::TextString => { - let s = decoder - .decode_tstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + let s = decoder.decode_tstr_owned().map_err(|e| { + HeaderError::CborDecodingError(e.to_string().into()) + })?; CwtClaimValue::Text(s) } CborType::UnsignedInt | CborType::NegativeInt => { - let i = decoder - .decode_i64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + let i = decoder.decode_i64().map_err(|e| { + HeaderError::CborDecodingError(e.to_string().into()) + })?; CwtClaimValue::Integer(i) } CborType::ByteString => { - let b = decoder - .decode_bstr_owned() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + let b = decoder.decode_bstr_owned().map_err(|e| { + HeaderError::CborDecodingError(e.to_string().into()) + })?; CwtClaimValue::Bytes(b) } CborType::Bool => { - let b = decoder - .decode_bool() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + let b = decoder.decode_bool().map_err(|e| { + HeaderError::CborDecodingError(e.to_string().into()) + })?; CwtClaimValue::Bool(b) } CborType::Float64 | CborType::Float32 | CborType::Float16 => { - let f = decoder - .decode_f64() - .map_err(|e| HeaderError::CborDecodingError(e.to_string()))?; + let f = decoder.decode_f64().map_err(|e| { + HeaderError::CborDecodingError(e.to_string().into()) + })?; CwtClaimValue::Float(f) } _ => { @@ -374,10 +372,13 @@ impl CwtClaims { } _ => { // Other complex types - just fail for now as we can't handle them properly - return Err(HeaderError::CborDecodingError(format!( - "Unsupported CWT claim value type: {:?}", - value_type - ))); + return Err(HeaderError::CborDecodingError( + format!( + "Unsupported CWT claim value type: {:?}", + value_type + ) + .into(), + )); } } continue; diff --git a/native/rust/signing/headers/src/error.rs b/native/rust/signing/headers/src/error.rs index 3243c883..5f481587 100644 --- a/native/rust/signing/headers/src/error.rs +++ b/native/rust/signing/headers/src/error.rs @@ -1,24 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::borrow::Cow; + /// Errors that can occur when working with COSE headers and CWT claims. #[derive(Debug)] pub enum HeaderError { - CborEncodingError(String), + CborEncodingError(Cow<'static, str>), - CborDecodingError(String), + CborDecodingError(Cow<'static, str>), InvalidClaimType { label: i64, - expected: String, - actual: String, + expected: Cow<'static, str>, + actual: Cow<'static, str>, }, - MissingRequiredClaim(String), + MissingRequiredClaim(Cow<'static, str>), - InvalidTimestamp(String), + InvalidTimestamp(Cow<'static, str>), - ComplexClaimValue(String), + ComplexClaimValue(Cow<'static, str>), } impl std::fmt::Display for HeaderError { diff --git a/native/rust/signing/headers/tests/cwt_claims_deep_coverage.rs b/native/rust/signing/headers/tests/cwt_claims_deep_coverage.rs index c0e89f4c..112684be 100644 --- a/native/rust/signing/headers/tests/cwt_claims_deep_coverage.rs +++ b/native/rust/signing/headers/tests/cwt_claims_deep_coverage.rs @@ -639,7 +639,7 @@ fn roundtrip_large_negative_timestamp() { #[test] fn header_error_display_cbor_encoding() { - let e = HeaderError::CborEncodingError("test-enc".to_string()); + let e = HeaderError::CborEncodingError("test-enc".into()); let msg = format!("{}", e); assert!(msg.contains("CBOR encoding error")); assert!(msg.contains("test-enc")); @@ -647,7 +647,7 @@ fn header_error_display_cbor_encoding() { #[test] fn header_error_display_cbor_decoding() { - let e = HeaderError::CborDecodingError("test-dec".to_string()); + let e = HeaderError::CborDecodingError("test-dec".into()); let msg = format!("{}", e); assert!(msg.contains("CBOR decoding error")); assert!(msg.contains("test-dec")); @@ -657,8 +657,8 @@ fn header_error_display_cbor_decoding() { fn header_error_display_invalid_claim_type() { let e = HeaderError::InvalidClaimType { label: 1, - expected: "text".to_string(), - actual: "integer".to_string(), + expected: "text".into(), + actual: "integer".into(), }; let msg = format!("{}", e); assert!(msg.contains("Invalid CWT claim type")); @@ -667,7 +667,7 @@ fn header_error_display_invalid_claim_type() { #[test] fn header_error_display_missing_required_claim() { - let e = HeaderError::MissingRequiredClaim("subject".to_string()); + let e = HeaderError::MissingRequiredClaim("subject".into()); let msg = format!("{}", e); assert!(msg.contains("Missing required claim")); assert!(msg.contains("subject")); @@ -675,21 +675,21 @@ fn header_error_display_missing_required_claim() { #[test] fn header_error_display_invalid_timestamp() { - let e = HeaderError::InvalidTimestamp("negative".to_string()); + let e = HeaderError::InvalidTimestamp("negative".into()); let msg = format!("{}", e); assert!(msg.contains("Invalid timestamp")); } #[test] fn header_error_display_complex_claim_value() { - let e = HeaderError::ComplexClaimValue("nested".to_string()); + let e = HeaderError::ComplexClaimValue("nested".into()); let msg = format!("{}", e); assert!(msg.contains("Custom claim value too complex")); } #[test] fn header_error_is_std_error() { - let e = HeaderError::CborEncodingError("x".to_string()); + let e = HeaderError::CborEncodingError("x".into()); let _: &dyn std::error::Error = &e; } diff --git a/native/rust/signing/headers/tests/error_tests.rs b/native/rust/signing/headers/tests/error_tests.rs index 202eea26..6176a801 100644 --- a/native/rust/signing/headers/tests/error_tests.rs +++ b/native/rust/signing/headers/tests/error_tests.rs @@ -5,7 +5,7 @@ use cose_sign1_headers::HeaderError; #[test] fn test_cbor_encoding_error_display() { - let error = HeaderError::CborEncodingError("test encoding error".to_string()); + let error = HeaderError::CborEncodingError("test encoding error".into()); assert_eq!( error.to_string(), "CBOR encoding error: test encoding error" @@ -14,7 +14,7 @@ fn test_cbor_encoding_error_display() { #[test] fn test_cbor_decoding_error_display() { - let error = HeaderError::CborDecodingError("test decoding error".to_string()); + let error = HeaderError::CborDecodingError("test decoding error".into()); assert_eq!( error.to_string(), "CBOR decoding error: test decoding error" @@ -25,8 +25,8 @@ fn test_cbor_decoding_error_display() { fn test_invalid_claim_type_display() { let error = HeaderError::InvalidClaimType { label: 42, - expected: "string".to_string(), - actual: "integer".to_string(), + expected: "string".into(), + actual: "integer".into(), }; assert_eq!( error.to_string(), @@ -36,13 +36,13 @@ fn test_invalid_claim_type_display() { #[test] fn test_missing_required_claim_display() { - let error = HeaderError::MissingRequiredClaim("issuer".to_string()); + let error = HeaderError::MissingRequiredClaim("issuer".into()); assert_eq!(error.to_string(), "Missing required claim: issuer"); } #[test] fn test_invalid_timestamp_display() { - let error = HeaderError::InvalidTimestamp("timestamp out of range".to_string()); + let error = HeaderError::InvalidTimestamp("timestamp out of range".into()); assert_eq!( error.to_string(), "Invalid timestamp value: timestamp out of range" @@ -51,7 +51,7 @@ fn test_invalid_timestamp_display() { #[test] fn test_complex_claim_value_display() { - let error = HeaderError::ComplexClaimValue("nested object not supported".to_string()); + let error = HeaderError::ComplexClaimValue("nested object not supported".into()); assert_eq!( error.to_string(), "Custom claim value too complex: nested object not supported" @@ -60,6 +60,6 @@ fn test_complex_claim_value_display() { #[test] fn test_header_error_is_error_trait() { - let error = HeaderError::CborEncodingError("test".to_string()); + let error = HeaderError::CborEncodingError("test".into()); assert!(std::error::Error::source(&error).is_none()); } diff --git a/native/rust/validation/core/ffi/src/lib.rs b/native/rust/validation/core/ffi/src/lib.rs index 93407f39..1ca7341d 100644 --- a/native/rust/validation/core/ffi/src/lib.rs +++ b/native/rust/validation/core/ffi/src/lib.rs @@ -332,7 +332,7 @@ pub extern "C" fn cose_sign1_validator_validate_bytes( .overall .failures .first() - .map(|f| f.message.clone()) + .map(|f| f.message.clone().into_owned()) .unwrap_or_else(|| "Validation failed".to_string()); (false, Some(msg)) } diff --git a/native/rust/validation/core/src/indirect_signature.rs b/native/rust/validation/core/src/indirect_signature.rs index e8c3ee46..d7770483 100644 --- a/native/rust/validation/core/src/indirect_signature.rs +++ b/native/rust/validation/core/src/indirect_signature.rs @@ -98,7 +98,7 @@ fn header_text_or_utf8_bytes(map: &CoseHeaderMap, label: i64) -> Option let v = map.get(&key)?; match v { CoseHeaderValue::Text(s) => Some(s.to_string()), - CoseHeaderValue::Bytes(b) => std::str::from_utf8(b.as_ref()).ok().map(|s| s.to_string()), + CoseHeaderValue::Bytes(b) => std::str::from_utf8(b.as_ref()).ok().map(Into::into), _ => None, } } @@ -135,6 +135,13 @@ fn detect_indirect_signature_kind( None } +/// Compute a hash of in-memory bytes. +/// +/// ## Allocation note +/// The `.to_vec()` on each digest is **structural**: `sha2::digest()` returns a +/// fixed-size `GenericArray` whose size varies per algorithm, so a uniform `Vec` +/// return type is the simplest cross-algorithm representation. The allocation is +/// small (32–64 bytes) and happens once per validation. fn compute_hash_bytes(alg: HashAlgorithm, data: &[u8]) -> Vec { use sha2::Digest as _; match alg { @@ -213,7 +220,7 @@ fn compute_hash_from_detached_payload( match payload { cose_sign1_primitives::payload::Payload::Bytes(b) => { if b.is_empty() { - return Err("detached payload was empty".to_string()); + return Err("detached payload was empty".into()); } Ok(compute_hash_bytes(alg, b.as_ref())) } @@ -232,10 +239,10 @@ fn parse_cose_hash_v(payload: &[u8]) -> Result<(HashAlgorithm, Vec), String> let len = d .decode_array_len() .map_err(|e| format!("invalid COSE_Hash_V: {e}"))? - .ok_or_else(|| "invalid COSE_Hash_V: indefinite array not supported".to_string())?; + .ok_or_else(|| String::from("invalid COSE_Hash_V: indefinite array not supported"))?; if len != 2 { - return Err("invalid COSE_Hash_V: expected array of 2 elements".to_string()); + return Err("invalid COSE_Hash_V: expected array of 2 elements".into()); } let alg = d @@ -250,7 +257,7 @@ fn parse_cose_hash_v(payload: &[u8]) -> Result<(HashAlgorithm, Vec), String> .ok_or_else(|| format!("unsupported COSE_Hash_V algorithm {alg}"))?; if hash_bytes.is_empty() { - return Err("invalid COSE_Hash_V: empty hash".to_string()); + return Err("invalid COSE_Hash_V: empty hash".into()); } Ok((alg, hash_bytes)) @@ -341,6 +348,7 @@ impl PostSignatureValidator for IndirectSignaturePostSignatureValidator { ); }; + // Structural copy: payload &[u8] must be owned for the comparison tuple. (alg, payload.to_vec(), "Legacy+hash-*") } IndirectSignatureKind::CoseHashV => match parse_cose_hash_v(payload) { @@ -371,6 +379,7 @@ impl PostSignatureValidator for IndirectSignaturePostSignatureValidator { ); }; + // Structural copy: payload &[u8] must be owned for the comparison tuple. (alg, payload.to_vec(), "CoseHashEnvelope") } }; @@ -389,14 +398,8 @@ impl PostSignatureValidator for IndirectSignaturePostSignatureValidator { if actual_hash == expected_hash { let mut metadata = std::collections::BTreeMap::new(); - metadata.insert( - "IndirectSignature.Format".to_string(), - format_name.to_string(), - ); - metadata.insert( - "IndirectSignature.HashAlgorithm".to_string(), - alg.name().to_string(), - ); + metadata.insert("IndirectSignature.Format".into(), format_name.into()); + metadata.insert("IndirectSignature.HashAlgorithm".into(), alg.name().into()); ValidationResult::success(VALIDATOR_NAME, Some(metadata)) } else { ValidationResult::failure_message( diff --git a/native/rust/validation/core/src/message_fact_producer.rs b/native/rust/validation/core/src/message_fact_producer.rs index 435ccf53..ac3654fb 100644 --- a/native/rust/validation/core/src/message_fact_producer.rs +++ b/native/rust/validation/core/src/message_fact_producer.rs @@ -95,7 +95,7 @@ impl TrustFactProducer for CoseSign1MessageFactProducer { m } else { // Message should always be available from the validator - ctx.mark_error::("no parsed message in context".to_string()); + ctx.mark_error::("no parsed message in context"); for k in self.provides() { ctx.mark_produced(*k); } @@ -188,7 +188,7 @@ fn produce_cwt_claims_facts( produce_cwt_claims_from_map(ctx, pairs) } _ => { - ctx.mark_error::("CwtClaimsValueNotMap".to_string()); + ctx.mark_error::("CwtClaimsValueNotMap"); Ok(()) } } @@ -359,7 +359,7 @@ fn produce_cwt_claims_from_bytes( let map_len = match d.decode_map_len() { Ok(Some(len)) => len, Ok(None) => { - ctx.mark_error::("cwt_claims indefinite map not supported".to_string()); + ctx.mark_error::("cwt_claims indefinite map not supported"); return Ok(()); } Err(e) => { diff --git a/native/rust/validation/core/src/message_facts.rs b/native/rust/validation/core/src/message_facts.rs index 62419a7b..d92af0d4 100644 --- a/native/rust/validation/core/src/message_facts.rs +++ b/native/rust/validation/core/src/message_facts.rs @@ -361,7 +361,7 @@ pub mod fluent_ext { impl From<&str> for CwtClaimKey { /// Convert a borrowed text claim key into a `CwtClaimKey`. fn from(value: &str) -> Self { - Self::Text(value.to_string()) + Self::Text(value.into()) } } diff --git a/native/rust/validation/core/src/validator.rs b/native/rust/validation/core/src/validator.rs index eea7528d..0bb837d1 100644 --- a/native/rust/validation/core/src/validator.rs +++ b/native/rust/validation/core/src/validator.rs @@ -54,18 +54,33 @@ impl Default for ValidationResultKind { } /// A single validation failure, optionally annotated with an error code and details. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +/// +/// Fields use `Cow<'static, str>` so that static string constants (the common case) +/// avoid heap allocation while dynamic messages still work via `Cow::Owned`. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ValidationFailure { /// Human-readable failure message. - pub message: String, + pub message: Cow<'static, str>, /// Optional stable error code for programmatic handling. - pub error_code: Option, + pub error_code: Option>, /// Optional property/field name associated with the failure. - pub property_name: Option, + pub property_name: Option>, /// Optional attempted value (as string) associated with the failure. - pub attempted_value: Option, + pub attempted_value: Option>, /// Optional exception/debug details. - pub exception: Option, + pub exception: Option>, +} + +impl Default for ValidationFailure { + fn default() -> Self { + Self { + message: Cow::Borrowed(""), + error_code: None, + property_name: None, + attempted_value: None, + exception: None, + } + } } /// Result for a single validation stage. @@ -129,7 +144,7 @@ impl ValidationResult { let mut metadata = BTreeMap::new(); if let Some(r) = reason { if !r.trim().is_empty() { - metadata.insert(Self::METADATA_REASON_KEY.into(), r.to_string()); + metadata.insert(Self::METADATA_REASON_KEY.into(), r.into()); } } Self { @@ -156,14 +171,14 @@ impl ValidationResult { /// Convenience helper for a single failure message. pub fn failure_message( validator_name: impl Into>, - message: impl Into, - error_code: Option<&str>, + message: impl Into>, + error_code: Option<&'static str>, ) -> Self { Self::failure( validator_name, vec![ValidationFailure { message: message.into(), - error_code: error_code.map(|s| s.to_string()), + error_code: error_code.map(Cow::Borrowed), ..ValidationFailure::default() }], ) @@ -376,10 +391,11 @@ pub struct PostSignatureValidationContext<'a> { /// Top-level validation errors (as opposed to per-stage failures). /// /// Stage failures are represented by [`ValidationResult`] within [`CoseSign1ValidationResult`]. +/// Variants use `Cow<'static, str>` so that static messages avoid allocation. #[derive(Debug)] pub enum CoseSign1ValidationError { - CoseDecode(String), - Trust(String), + CoseDecode(Cow<'static, str>), + Trust(Cow<'static, str>), } impl std::fmt::Display for CoseSign1ValidationError { @@ -639,7 +655,7 @@ impl CoseSign1Validator { let parsed_message = CoseSign1Message::parse(&cose_sign1_bytes).map_err(|e| { error!(stage = "parse", error = %e, "Failed to parse COSE_Sign1 message"); - CoseSign1ValidationError::CoseDecode(e.to_string()) + CoseSign1ValidationError::CoseDecode(e.to_string().into()) })?; debug!(stage = "parse", algorithm = ?parsed_message.alg(), is_detached = parsed_message.is_detached(), "Message parsed"); @@ -661,7 +677,7 @@ impl CoseSign1Validator { let parsed_message = CoseSign1Message::parse(&cose_sign1_bytes).map_err(|e| { error!(stage = "parse", error = %e, "Failed to parse COSE_Sign1 message"); - CoseSign1ValidationError::CoseDecode(e.to_string()) + CoseSign1ValidationError::CoseDecode(e.to_string().into()) })?; debug!(stage = "parse", algorithm = ?parsed_message.alg(), is_detached = parsed_message.is_detached(), "Message parsed"); @@ -704,7 +720,7 @@ impl CoseSign1Validator { // (e.g. MST receipts) even when the primary key was resolved. true, ) - .map_err(CoseSign1ValidationError::Trust)?; + .map_err(|e| CoseSign1ValidationError::Trust(e.into()))?; info!( stage = "trust_evaluation", is_trusted = trust_decision.is_trusted, @@ -979,7 +995,7 @@ impl CoseSign1Validator { cose_sign1_parsed.clone(), true, // Always check for counter-sig bypass (OR-composed trust plans) ) - .map_err(CoseSign1ValidationError::Trust)?; + .map_err(|e| CoseSign1ValidationError::Trust(e.into()))?; let counter_sig_bypassed = signature_stage_metadata .get(Self::METADATA_KEY_SIGNATURE_VERIFICATION_MODE) @@ -1271,8 +1287,8 @@ impl CoseSign1Validator { kind: ValidationResultKind::Failure, validator_name: Cow::Borrowed(Self::STAGE_NAME_KEY_MATERIAL_RESOLUTION), failures: vec![ValidationFailure { - message: Self::ERROR_MESSAGE_NO_SIGNING_KEY_RESOLVED.to_string(), - error_code: Some(Self::ERROR_CODE_NO_SIGNING_KEY_RESOLVED.to_string()), + message: Cow::Borrowed(Self::ERROR_MESSAGE_NO_SIGNING_KEY_RESOLVED), + error_code: Some(Cow::Borrowed(Self::ERROR_CODE_NO_SIGNING_KEY_RESOLVED)), ..ValidationFailure::default() }], metadata, @@ -1315,12 +1331,12 @@ impl CoseSign1Validator { } let mut failure = ValidationFailure { - message: Self::ERROR_MESSAGE_NO_SIGNING_KEY_RESOLVED.to_string(), - error_code: Some(Self::ERROR_CODE_NO_SIGNING_KEY_RESOLVED.to_string()), + message: Cow::Borrowed(Self::ERROR_MESSAGE_NO_SIGNING_KEY_RESOLVED), + error_code: Some(Cow::Borrowed(Self::ERROR_CODE_NO_SIGNING_KEY_RESOLVED)), ..ValidationFailure::default() }; if !diagnostics.is_empty() { - failure.exception = Some(diagnostics.join(";")); + failure.exception = Some(Cow::Owned(diagnostics.join(";"))); } ( @@ -1363,8 +1379,8 @@ impl CoseSign1Validator { if !decision.is_trusted { let failures = if decision.reasons.is_empty() { vec![ValidationFailure { - error_code: Some(Self::ERROR_CODE_TRUST_PLAN_NOT_SATISFIED.to_string()), - message: Self::ERROR_MESSAGE_TRUST_PLAN_NOT_SATISFIED.to_string(), + error_code: Some(Cow::Borrowed(Self::ERROR_CODE_TRUST_PLAN_NOT_SATISFIED)), + message: Cow::Borrowed(Self::ERROR_MESSAGE_TRUST_PLAN_NOT_SATISFIED), ..ValidationFailure::default() }] } else { @@ -1372,8 +1388,8 @@ impl CoseSign1Validator { .reasons .iter() .map(|r| ValidationFailure { - error_code: Some(Self::ERROR_CODE_TRUST_PLAN_NOT_SATISFIED.to_string()), - message: r.to_string(), + error_code: Some(Cow::Borrowed(Self::ERROR_CODE_TRUST_PLAN_NOT_SATISFIED)), + message: Cow::Owned(r.to_string()), ..ValidationFailure::default() }) .collect() @@ -1455,7 +1471,7 @@ impl CoseSign1Validator { if let Some(details) = integrity_facts .iter() .find_map(|f| f.details.as_deref()) - .map(str::to_string) + .map(str::to_owned) { metadata.insert(Self::METADATA_KEY_SIGNATURE_BYPASS_DETAILS.into(), details); } @@ -1769,7 +1785,7 @@ impl CoseSign1Validator { match payload { Payload::Bytes(b) => { if b.is_empty() { - return Err(Self::ERROR_MESSAGE_SIGNATURE_MISSING_PAYLOAD.to_string()); + return Err(Self::ERROR_MESSAGE_SIGNATURE_MISSING_PAYLOAD.into()); } Ok(Arc::from(b.as_slice())) } @@ -1782,7 +1798,7 @@ impl CoseSign1Validator { .read_to_end(&mut buf) .map_err(|e| format!("detached_payload_read_failed: {e}"))?; if buf.is_empty() { - return Err(Self::ERROR_MESSAGE_SIGNATURE_MISSING_PAYLOAD.to_string()); + return Err(Self::ERROR_MESSAGE_SIGNATURE_MISSING_PAYLOAD.into()); } Ok(Arc::from(buf.into_boxed_slice())) } diff --git a/native/rust/validation/core/tests/additional_validator_coverage.rs b/native/rust/validation/core/tests/additional_validator_coverage.rs index 5a9cec32..c841ce1a 100644 --- a/native/rust/validation/core/tests/additional_validator_coverage.rs +++ b/native/rust/validation/core/tests/additional_validator_coverage.rs @@ -5,15 +5,11 @@ use cbor_primitives::{CborEncoder, CborProvider}; use cbor_primitives_everparse::EverParseCborProvider; -use cose_sign1_primitives::payload::{MemoryPayload, Payload}; -use cose_sign1_primitives::CoseSign1Message; use cose_sign1_validation::fluent::*; use cose_sign1_validation_primitives::{ error::TrustError, fact_properties::{FactProperties, FactValue}, facts::{FactKey, TrustFactContext, TrustFactProducer}, - rules::allow_all, - subject::TrustSubject, }; use cose_sign1_validation_test_utils::SimpleTrustPack; use std::borrow::Cow; @@ -177,10 +173,6 @@ fn validator_with_options_closure_variations() { #[test] fn validation_result_field_access() { - use cose_sign1_validation::fluent::{CoseSign1ValidationError, CoseSign1ValidationResult}; - use cose_sign1_validation_primitives::audit::{AuditEvent, TrustDecisionAudit}; - use cose_sign1_validation_primitives::decision::TrustDecision; - let pack: Arc = Arc::new(SimpleTrustPack::no_facts("test")); let validator = CoseSign1Validator::new(vec![pack]); diff --git a/native/rust/validation/core/tests/async_and_streaming_coverage.rs b/native/rust/validation/core/tests/async_and_streaming_coverage.rs index 78511d60..bd744d4b 100644 --- a/native/rust/validation/core/tests/async_and_streaming_coverage.rs +++ b/native/rust/validation/core/tests/async_and_streaming_coverage.rs @@ -462,8 +462,8 @@ fn test_cose_key_resolution_result_failure_helper() { assert_eq!(ValidationResultKind::Failure, result.resolution.kind); assert!(!result.resolution.failures.is_empty()); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_SIGNING_KEY_RESOLVED.to_string()), - result.resolution.failures[0].error_code + result.resolution.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_NO_SIGNING_KEY_RESOLVED) ); } diff --git a/native/rust/validation/core/tests/final_targeted_coverage.rs b/native/rust/validation/core/tests/final_targeted_coverage.rs index 3070352b..0d1c96c9 100644 --- a/native/rust/validation/core/tests/final_targeted_coverage.rs +++ b/native/rust/validation/core/tests/final_targeted_coverage.rs @@ -1008,10 +1008,10 @@ fn message_fact_producer_primary_signing_key_subject() { #[test] fn validation_error_display() { - let e = CoseSign1ValidationError::CoseDecode("bad cbor".to_string()); + let e = CoseSign1ValidationError::CoseDecode("bad cbor".into()); assert!(format!("{}", e).contains("COSE decode failed")); - let e = CoseSign1ValidationError::Trust("bad trust".to_string()); + let e = CoseSign1ValidationError::Trust("bad trust".into()); assert!(format!("{}", e).contains("trust evaluation failed")); } diff --git a/native/rust/validation/core/tests/final_validator_coverage.rs b/native/rust/validation/core/tests/final_validator_coverage.rs index 17d8c60a..c92d2dea 100644 --- a/native/rust/validation/core/tests/final_validator_coverage.rs +++ b/native/rust/validation/core/tests/final_validator_coverage.rs @@ -61,11 +61,11 @@ fn test_validation_failure_default() { #[test] fn test_validation_failure_with_all_fields() { let failure = ValidationFailure { - message: "test message".to_string(), - error_code: Some("ERR001".to_string()), - property_name: Some("field_name".to_string()), - attempted_value: Some("bad_value".to_string()), - exception: Some("stack trace here".to_string()), + message: "test message".into(), + error_code: Some("ERR001".into()), + property_name: Some("field_name".into()), + attempted_value: Some("bad_value".into()), + exception: Some("stack trace here".into()), }; assert_eq!(failure.message, "test message"); @@ -78,8 +78,8 @@ fn test_validation_failure_with_all_fields() { #[test] fn test_validation_failure_clone() { let failure = ValidationFailure { - message: "test".to_string(), - error_code: Some("E1".to_string()), + message: "test".into(), + error_code: Some("E1".into()), property_name: None, attempted_value: None, exception: None, @@ -92,7 +92,7 @@ fn test_validation_failure_clone() { #[test] fn test_validation_failure_debug() { let failure = ValidationFailure { - message: "test".to_string(), + message: "test".into(), error_code: None, property_name: None, attempted_value: None, @@ -179,11 +179,11 @@ fn test_validation_result_not_applicable_with_empty_reason() { fn test_validation_result_failure() { let failures = vec![ ValidationFailure { - message: "error 1".to_string(), + message: "error 1".into(), ..ValidationFailure::default() }, ValidationFailure { - message: "error 2".to_string(), + message: "error 2".into(), ..ValidationFailure::default() }, ]; @@ -339,7 +339,7 @@ fn test_validation_options_debug() { #[test] fn test_validation_error_cose_decode_display() { - let error = CoseSign1ValidationError::CoseDecode("invalid CBOR".to_string()); + let error = CoseSign1ValidationError::CoseDecode("invalid CBOR".into()); let display = format!("{}", error); assert!(display.contains("COSE decode failed")); @@ -348,7 +348,7 @@ fn test_validation_error_cose_decode_display() { #[test] fn test_validation_error_trust_display() { - let error = CoseSign1ValidationError::Trust("trust plan failed".to_string()); + let error = CoseSign1ValidationError::Trust("trust plan failed".into()); let display = format!("{}", error); assert!(display.contains("trust evaluation failed")); @@ -357,14 +357,14 @@ fn test_validation_error_trust_display() { #[test] fn test_validation_error_debug() { - let error = CoseSign1ValidationError::CoseDecode("test".to_string()); + let error = CoseSign1ValidationError::CoseDecode("test".into()); let debug_str = format!("{:?}", error); assert!(debug_str.contains("CoseDecode")); } #[test] fn test_validation_error_is_error_trait() { - let error = CoseSign1ValidationError::Trust("test".to_string()); + let error = CoseSign1ValidationError::Trust("test".into()); // Should implement std::error::Error fn assert_error() {} @@ -483,8 +483,8 @@ fn test_counter_signature_resolution_result_clone() { #[test] fn test_validation_failure_equality() { let f1 = ValidationFailure { - message: "test".to_string(), - error_code: Some("E1".to_string()), + message: "test".into(), + error_code: Some("E1".into()), property_name: None, attempted_value: None, exception: None, diff --git a/native/rust/validation/core/tests/targeted_coverage_gaps.rs b/native/rust/validation/core/tests/targeted_coverage_gaps.rs index 9d9c1508..a333b384 100644 --- a/native/rust/validation/core/tests/targeted_coverage_gaps.rs +++ b/native/rust/validation/core/tests/targeted_coverage_gaps.rs @@ -395,12 +395,12 @@ fn validator_skip_post_signature_validation() { #[test] fn cose_sign1_validation_error_display() { - let err = CoseSign1ValidationError::CoseDecode("bad cbor".to_string()); + let err = CoseSign1ValidationError::CoseDecode("bad cbor".into()); let display = format!("{err}"); assert!(display.contains("COSE decode failed")); assert!(display.contains("bad cbor")); - let err2 = CoseSign1ValidationError::Trust("plan eval failed".to_string()); + let err2 = CoseSign1ValidationError::Trust("plan eval failed".into()); let display2 = format!("{err2}"); assert!(display2.contains("trust evaluation failed")); } diff --git a/native/rust/validation/core/tests/v2_validator_parity.rs b/native/rust/validation/core/tests/v2_validator_parity.rs index 7facf983..64c0d3f4 100644 --- a/native/rust/validation/core/tests/v2_validator_parity.rs +++ b/native/rust/validation/core/tests/v2_validator_parity.rs @@ -187,8 +187,8 @@ fn v2_validate_when_trust_is_denied_without_reasons_skips_signature_and_post() { assert!(result.trust.is_failure()); assert_eq!( - Some("TRUST_PLAN_NOT_SATISFIED".to_string()), - result.trust.failures[0].error_code.clone() + result.trust.failures[0].error_code.as_deref(), + Some("TRUST_PLAN_NOT_SATISFIED") ); assert_eq!(ValidationResultKind::NotApplicable, result.signature.kind); assert_eq!( @@ -216,8 +216,8 @@ fn v2_validate_when_no_resolvers_returns_resolution_failure() { assert!(result.resolution.is_failure()); assert_eq!( - Some("NO_SIGNING_KEY_RESOLVED".to_string()), - result.resolution.failures[0].error_code.clone() + result.resolution.failures[0].error_code.as_deref(), + Some("NO_SIGNING_KEY_RESOLVED") ); assert_eq!(ValidationResultKind::NotApplicable, result.trust.kind); assert_eq!(ValidationResultKind::NotApplicable, result.signature.kind); @@ -272,8 +272,8 @@ fn v2_validate_when_signing_key_resolved_but_wrong_key_returns_signature_failure assert!(result.signature.is_failure()); assert_eq!( - Some("SIGNATURE_VERIFICATION_FAILED".to_string()), - result.signature.failures[0].error_code.clone() + result.signature.failures[0].error_code.as_deref(), + Some("SIGNATURE_VERIFICATION_FAILED") ); assert_eq!( ValidationResultKind::NotApplicable, @@ -307,8 +307,8 @@ fn v2_validate_when_detached_signature_and_no_payload_provided_returns_signature assert!(result.signature.is_failure()); assert_eq!( - Some("SIGNATURE_MISSING_PAYLOAD".to_string()), - result.signature.failures[0].error_code.clone() + result.signature.failures[0].error_code.as_deref(), + Some("SIGNATURE_MISSING_PAYLOAD") ); } diff --git a/native/rust/validation/core/tests/validation_result_helper_coverage.rs b/native/rust/validation/core/tests/validation_result_helper_coverage.rs index 675cc8fd..cdefb8a4 100644 --- a/native/rust/validation/core/tests/validation_result_helper_coverage.rs +++ b/native/rust/validation/core/tests/validation_result_helper_coverage.rs @@ -79,14 +79,14 @@ fn test_validation_result_not_applicable_no_reason() { fn test_validation_result_failure_multiple() { let failures = vec![ ValidationFailure { - message: "error 1".to_string(), - error_code: Some("E001".to_string()), - property_name: Some("prop1".to_string()), - attempted_value: Some("val1".to_string()), - exception: Some("ex1".to_string()), + message: "error 1".into(), + error_code: Some("E001".into()), + property_name: Some("prop1".into()), + attempted_value: Some("val1".into()), + exception: Some("ex1".into()), }, ValidationFailure { - message: "error 2".to_string(), + message: "error 2".into(), error_code: None, property_name: None, attempted_value: None, @@ -111,7 +111,7 @@ fn test_validation_result_failure_message_with_code() { let failure = &result.failures[0]; assert_eq!(failure.message, "test error"); - assert_eq!(failure.error_code, Some("ERR123".to_string())); + assert_eq!(failure.error_code.as_deref(), Some("ERR123")); assert!(failure.property_name.is_none()); assert!(failure.attempted_value.is_none()); assert!(failure.exception.is_none()); @@ -151,11 +151,11 @@ fn test_validation_result_kind_debug() { #[test] fn test_validation_failure_debug() { let failure = ValidationFailure { - message: "test message".to_string(), - error_code: Some("TEST".to_string()), - property_name: Some("field".to_string()), - attempted_value: Some("value".to_string()), - exception: Some("Exception info".to_string()), + message: "test message".into(), + error_code: Some("TEST".into()), + property_name: Some("field".into()), + attempted_value: Some("value".into()), + exception: Some("Exception info".into()), }; let debug_str = format!("{:?}", failure); @@ -190,11 +190,11 @@ fn test_validation_result_is_valid() { #[test] fn test_validation_failure_clone() { let failure = ValidationFailure { - message: "test message".to_string(), - error_code: Some("TEST".to_string()), - property_name: Some("field".to_string()), - attempted_value: Some("value".to_string()), - exception: Some("Exception info".to_string()), + message: "test message".into(), + error_code: Some("TEST".into()), + property_name: Some("field".into()), + attempted_value: Some("value".into()), + exception: Some("Exception info".into()), }; let cloned = failure.clone(); diff --git a/native/rust/validation/core/tests/validator_additional_coverage.rs b/native/rust/validation/core/tests/validator_additional_coverage.rs index 998c790f..1cdc9df4 100644 --- a/native/rust/validation/core/tests/validator_additional_coverage.rs +++ b/native/rust/validation/core/tests/validator_additional_coverage.rs @@ -47,7 +47,7 @@ fn test_validation_result_is_failure() { let failure = ValidationResult::failure( "test", vec![ValidationFailure { - message: "error".to_string(), + message: "error".into(), ..Default::default() }], ); @@ -117,10 +117,7 @@ fn test_validation_result_failure_with_message() { assert_eq!(result.kind, ValidationResultKind::Failure); assert_eq!(result.failures.len(), 1); assert_eq!(result.failures[0].message, "Something failed"); - assert_eq!( - result.failures[0].error_code, - Some("ERROR_CODE".to_string()) - ); + assert_eq!(result.failures[0].error_code.as_deref(), Some("ERROR_CODE")); } #[test] @@ -136,15 +133,15 @@ fn test_validation_result_failure_with_message_no_code() { fn test_validation_result_failure_multiple() { let failures = vec![ ValidationFailure { - message: "Failure 1".to_string(), - error_code: Some("CODE1".to_string()), - property_name: Some("prop1".to_string()), - attempted_value: Some("val1".to_string()), - exception: Some("exc1".to_string()), + message: "Failure 1".into(), + error_code: Some("CODE1".into()), + property_name: Some("prop1".into()), + attempted_value: Some("val1".into()), + exception: Some("exc1".into()), }, ValidationFailure { - message: "Failure 2".to_string(), - error_code: Some("CODE2".to_string()), + message: "Failure 2".into(), + error_code: Some("CODE2".into()), ..Default::default() }, ]; @@ -171,17 +168,17 @@ fn test_validation_failure_default() { #[test] fn test_validation_failure_full_fields() { let failure = ValidationFailure { - message: "Test message".to_string(), - error_code: Some("TEST_CODE".to_string()), - property_name: Some("property".to_string()), - attempted_value: Some("value".to_string()), - exception: Some("exception details".to_string()), + message: "Test message".into(), + error_code: Some("TEST_CODE".into()), + property_name: Some("property".into()), + attempted_value: Some("value".into()), + exception: Some("exception details".into()), }; assert_eq!(failure.message, "Test message"); - assert_eq!(failure.error_code, Some("TEST_CODE".to_string())); - assert_eq!(failure.property_name, Some("property".to_string())); - assert_eq!(failure.attempted_value, Some("value".to_string())); - assert_eq!(failure.exception, Some("exception details".to_string())); + assert_eq!(failure.error_code.as_deref(), Some("TEST_CODE")); + assert_eq!(failure.property_name.as_deref(), Some("property")); + assert_eq!(failure.attempted_value.as_deref(), Some("value")); + assert_eq!(failure.exception.as_deref(), Some("exception details")); } // ===================================================================== @@ -293,7 +290,7 @@ fn test_validation_options_with_associated_data() { #[test] fn test_cose_decode_error_display() { - let error = CoseSign1ValidationError::CoseDecode("test error".to_string()); + let error = CoseSign1ValidationError::CoseDecode("test error".into()); let display = format!("{}", error); assert!(display.contains("COSE decode failed")); assert!(display.contains("test error")); @@ -301,7 +298,7 @@ fn test_cose_decode_error_display() { #[test] fn test_trust_error_display() { - let error = CoseSign1ValidationError::Trust("trust failed".to_string()); + let error = CoseSign1ValidationError::Trust("trust failed".into()); let display = format!("{}", error); assert!(display.contains("trust evaluation failed")); assert!(display.contains("trust failed")); @@ -310,7 +307,7 @@ fn test_trust_error_display() { #[test] fn test_cose_sign1_validation_error_is_error() { use std::error::Error; - let error = CoseSign1ValidationError::CoseDecode("test".to_string()); + let error = CoseSign1ValidationError::CoseDecode("test".into()); let _e: &dyn Error = &error; } @@ -744,24 +741,24 @@ fn test_validation_options_combinations() { fn test_validation_failure_partial_fields() { // Test ValidationFailure with only some fields set let failure1 = ValidationFailure { - message: "message".to_string(), + message: "message".into(), error_code: None, - property_name: Some("prop".to_string()), + property_name: Some("prop".into()), attempted_value: None, exception: None, }; assert_eq!(failure1.message, "message"); - assert_eq!(failure1.property_name, Some("prop".to_string())); + assert_eq!(failure1.property_name.as_deref(), Some("prop")); let failure2 = ValidationFailure { - message: "".to_string(), - error_code: Some("CODE".to_string()), + message: "".into(), + error_code: Some("CODE".into()), property_name: None, - attempted_value: Some("attempted".to_string()), + attempted_value: Some("attempted".into()), exception: None, }; - assert_eq!(failure2.error_code, Some("CODE".to_string())); - assert_eq!(failure2.attempted_value, Some("attempted".to_string())); + assert_eq!(failure2.error_code.as_deref(), Some("CODE")); + assert_eq!(failure2.attempted_value.as_deref(), Some("attempted")); } #[test] @@ -771,8 +768,8 @@ fn test_validation_result_clone_equality() { assert_eq!(result1, result2); let failure = ValidationFailure { - message: "test".to_string(), - error_code: Some("CODE".to_string()), + message: "test".into(), + error_code: Some("CODE".into()), property_name: None, attempted_value: None, exception: None, diff --git a/native/rust/validation/core/tests/validator_async_tests.rs b/native/rust/validation/core/tests/validator_async_tests.rs index 1d13872e..4bdbaa21 100644 --- a/native/rust/validation/core/tests/validator_async_tests.rs +++ b/native/rust/validation/core/tests/validator_async_tests.rs @@ -382,8 +382,8 @@ fn signature_stage_no_payload_and_no_detached_returns_missing_payload() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD) ); assert_eq!( CoseSign1Validator::ERROR_MESSAGE_SIGNATURE_MISSING_PAYLOAD, @@ -415,8 +415,8 @@ fn validate_async_no_payload_and_no_detached_returns_missing_payload() { let result = block_on(v.validate_async(&parsed, Arc::from(cose.into_boxed_slice()))).unwrap(); assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD) ); } @@ -449,8 +449,8 @@ fn signature_stage_no_alg_in_protected_header_returns_no_applicable_validator() assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR) ); } @@ -481,8 +481,8 @@ fn signature_stage_verify_sig_structure_error_returns_failure() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED) ); assert!(result.signature.failures[0].message.contains("verify_boom")); } @@ -565,8 +565,8 @@ fn validate_async_no_resolvers_resolution_fails() { let result = block_on(v.validate_async(&parsed, Arc::from(cose.into_boxed_slice()))).unwrap(); assert_eq!(ValidationResultKind::Failure, result.resolution.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_SIGNING_KEY_RESOLVED.to_string()), - result.resolution.failures[0].error_code + result.resolution.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_NO_SIGNING_KEY_RESOLVED) ); } @@ -622,8 +622,8 @@ impl PostSignatureValidator for FailingPostValidator { ValidationResult::failure( "post", vec![ValidationFailure { - message: "post_failed".to_string(), - error_code: Some("POST_ERR".to_string()), + message: "post_failed".into(), + error_code: Some("POST_ERR".into()), ..Default::default() }], ) diff --git a/native/rust/validation/core/tests/validator_comprehensive_coverage.rs b/native/rust/validation/core/tests/validator_comprehensive_coverage.rs index b10d29bd..753014cd 100644 --- a/native/rust/validation/core/tests/validator_comprehensive_coverage.rs +++ b/native/rust/validation/core/tests/validator_comprehensive_coverage.rs @@ -26,6 +26,7 @@ use cose_sign1_validation_primitives::evaluation_options::CoseHeaderLocation; use cose_sign1_validation_primitives::facts::{FactKey, TrustFactContext, TrustFactProducer}; use cose_sign1_validation_primitives::plan::CompiledTrustPlan; use cose_sign1_validation_primitives::TrustEvaluationOptions; +use std::borrow::Cow; use std::future::Future; use std::io::Read; use std::pin::Pin; @@ -795,7 +796,7 @@ fn test_validation_result_helpers_comprehensive() { assert_eq!(failure.failures.len(), 1); assert_eq!( failure.failures[0].error_code, - Some("TEST_ERROR".to_string()) + Some(Cow::Borrowed("TEST_ERROR")) ); } diff --git a/native/rust/validation/core/tests/validator_deep_coverage.rs b/native/rust/validation/core/tests/validator_deep_coverage.rs index 0e71bcbe..4362167f 100644 --- a/native/rust/validation/core/tests/validator_deep_coverage.rs +++ b/native/rust/validation/core/tests/validator_deep_coverage.rs @@ -971,12 +971,12 @@ fn streaming_no_alg_returns_no_applicable_validator() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR), ); } @@ -1090,12 +1090,12 @@ fn streaming_finalize_false_returns_failure() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED), ); } @@ -1877,7 +1877,7 @@ fn validator_advanced_constructor() { #[test] fn validation_error_display_trust() { - let err = CoseSign1ValidationError::Trust("bad trust".to_string()); + let err = CoseSign1ValidationError::Trust("bad trust".into()); assert!(err.to_string().contains("trust evaluation failed")); assert!(err.to_string().contains("bad trust")); @@ -1887,7 +1887,7 @@ fn validation_error_display_trust() { #[test] fn validation_error_display_cose_decode() { - let err = CoseSign1ValidationError::CoseDecode("invalid cbor".to_string()); + let err = CoseSign1ValidationError::CoseDecode("invalid cbor".into()); assert!(err.to_string().contains("COSE decode failed")); } diff --git a/native/rust/validation/core/tests/validator_error_paths.rs b/native/rust/validation/core/tests/validator_error_paths.rs index 1174db45..90c521a2 100644 --- a/native/rust/validation/core/tests/validator_error_paths.rs +++ b/native/rust/validation/core/tests/validator_error_paths.rs @@ -227,30 +227,30 @@ fn validation_result_failure_message_with_none_error_code() { fn validation_result_failure_message_with_some_error_code() { let r = ValidationResult::failure_message("sig", "bad", Some("E001")); assert!(r.is_failure()); - assert_eq!(Some("E001".to_string()), r.failures[0].error_code); + assert_eq!(r.failures[0].error_code.as_deref(), Some("E001")); } #[test] fn validation_result_failure_with_multiple_failures() { let failures = vec![ ValidationFailure { - message: "a".to_string(), - error_code: Some("X".to_string()), - property_name: Some("prop".to_string()), - attempted_value: Some("val".to_string()), - exception: Some("ex".to_string()), + message: "a".into(), + error_code: Some("X".into()), + property_name: Some("prop".into()), + attempted_value: Some("val".into()), + exception: Some("ex".into()), }, ValidationFailure { - message: "b".to_string(), + message: "b".into(), ..ValidationFailure::default() }, ]; let r = ValidationResult::failure("multi", failures); assert!(r.is_failure()); assert_eq!(2, r.failures.len()); - assert_eq!(Some("prop".to_string()), r.failures[0].property_name); - assert_eq!(Some("val".to_string()), r.failures[0].attempted_value); - assert_eq!(Some("ex".to_string()), r.failures[0].exception); + assert_eq!(r.failures[0].property_name.as_deref(), Some("prop")); + assert_eq!(r.failures[0].attempted_value.as_deref(), Some("val")); + assert_eq!(r.failures[0].exception.as_deref(), Some("ex")); } // --------------------------------------------------------------------------- @@ -347,8 +347,8 @@ fn signature_stage_algorithm_mismatch_returns_failure() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_ALGORITHM_MISMATCH.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_ALGORITHM_MISMATCH) ); assert!(result.signature.failures[0].message.contains("-35")); assert!(result.signature.failures[0].message.contains("-7")); @@ -578,8 +578,8 @@ fn buffered_signature_fails_when_alg_missing() { .unwrap(); assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR.to_string()), - result.signature.failures[0].error_code + result.signature.failures[0].error_code.as_deref(), + Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR) ); } @@ -589,11 +589,11 @@ fn buffered_signature_fails_when_alg_missing() { #[test] fn validation_error_display_formats() { - let e1 = CoseSign1ValidationError::CoseDecode("bad cbor".to_string()); + let e1 = CoseSign1ValidationError::CoseDecode("bad cbor".into()); let s1 = format!("{e1}"); assert!(s1.contains("bad cbor")); - let e2 = CoseSign1ValidationError::Trust("plan failed".to_string()); + let e2 = CoseSign1ValidationError::Trust("plan failed".into()); let s2 = format!("{e2}"); assert!(s2.contains("plan failed")); } diff --git a/native/rust/validation/core/tests/validator_final_coverage_gaps.rs b/native/rust/validation/core/tests/validator_final_coverage_gaps.rs index 9d75a91d..feeee1b9 100644 --- a/native/rust/validation/core/tests/validator_final_coverage_gaps.rs +++ b/native/rust/validation/core/tests/validator_final_coverage_gaps.rs @@ -65,11 +65,11 @@ fn test_validation_failure_fields() { assert!(failure.exception.is_none()); // Test field assignment - failure.message = "Test message".to_string(); - failure.error_code = Some("TEST_CODE".to_string()); - failure.property_name = Some("test_prop".to_string()); - failure.attempted_value = Some("test_val".to_string()); - failure.exception = Some("test_exception".to_string()); + failure.message = "Test message".into(); + failure.error_code = Some("TEST_CODE".into()); + failure.property_name = Some("test_prop".into()); + failure.attempted_value = Some("test_val".into()); + failure.exception = Some("test_exception".into()); assert_eq!(failure.message, "Test message"); assert_eq!(failure.error_code.as_deref(), Some("TEST_CODE")); @@ -81,7 +81,7 @@ fn test_validation_failure_fields() { #[test] fn test_validation_result_methods() { // Test success result - let success_result = ValidationResult::success("TestValidator".to_string(), None); + let success_result = ValidationResult::success("TestValidator", None); assert!(success_result.is_valid()); assert!(!success_result.is_failure()); assert_eq!(success_result.kind, ValidationResultKind::Success); @@ -119,7 +119,7 @@ fn test_validation_result_with_metadata() { metadata.insert("test_key".to_string(), "test_value".to_string()); metadata.insert("another_key".to_string(), "another_value".to_string()); - let result = ValidationResult::success("TestValidator".to_string(), Some(metadata.clone())); + let result = ValidationResult::success("TestValidator", Some(metadata.clone())); assert!(result.is_valid()); assert_eq!(result.metadata, metadata); assert_eq!(result.metadata.get("test_key").unwrap(), "test_value"); @@ -222,13 +222,13 @@ fn test_validation_options_with_detached_payload() { #[test] fn test_validation_error_types() { - let decode_error = CoseSign1ValidationError::CoseDecode("Invalid CBOR".to_string()); + let decode_error = CoseSign1ValidationError::CoseDecode("Invalid CBOR".into()); match decode_error { CoseSign1ValidationError::CoseDecode(msg) => assert_eq!(msg, "Invalid CBOR"), _ => panic!("Unexpected error type"), } - let trust_error = CoseSign1ValidationError::Trust("Trust evaluation failed".to_string()); + let trust_error = CoseSign1ValidationError::Trust("Trust evaluation failed".into()); match trust_error { CoseSign1ValidationError::Trust(msg) => assert_eq!(msg, "Trust evaluation failed"), _ => panic!("Unexpected error type"), @@ -244,8 +244,8 @@ fn test_validation_result_metadata_reason_key() { #[test] fn test_cloneable_types() { let failure = ValidationFailure { - message: "test".to_string(), - error_code: Some("CODE".to_string()), + message: "test".into(), + error_code: Some("CODE".into()), property_name: None, attempted_value: None, exception: None, @@ -276,8 +276,8 @@ fn test_partial_eq_implementations() { let failure2 = ValidationFailure::default(); assert_eq!(failure1, failure2); - let result1 = ValidationResult::success("test".to_string(), None); - let result2 = ValidationResult::success("test".to_string(), None); + let result1 = ValidationResult::success("test", None); + let result2 = ValidationResult::success("test", None); assert_eq!(result1, result2); let result3 = ValidationResult::failure_message("test", "error", None); @@ -291,7 +291,7 @@ fn test_debug_implementations() { let debug_str = format!("{:?}", failure); assert!(debug_str.contains("ValidationFailure")); - let result = ValidationResult::success("test".to_string(), None); + let result = ValidationResult::success("test", None); let debug_str = format!("{:?}", result); assert!(debug_str.contains("ValidationResult")); @@ -450,7 +450,7 @@ struct MockPostSignatureValidator; impl PostSignatureValidator for MockPostSignatureValidator { fn validate(&self, _context: &PostSignatureValidationContext) -> ValidationResult { - ValidationResult::success("MockPostSigValidator".to_string(), None) + ValidationResult::success("MockPostSigValidator", None) } fn validate_async<'a>( @@ -702,7 +702,7 @@ fn test_validation_result_helper_methods() { let mut metadata = BTreeMap::new(); metadata.insert("key".to_string(), "value".to_string()); - let result = ValidationResult::success("TestValidator".to_string(), Some(metadata)); + let result = ValidationResult::success("TestValidator", Some(metadata)); assert!(result.is_valid()); assert!(!result.is_failure()); @@ -755,11 +755,11 @@ fn test_validation_failure_comprehensive() { assert!(failure.message.is_empty()); assert!(failure.error_code.is_none()); - failure.message = "Test failure message".to_string(); - failure.error_code = Some("TEST_CODE".to_string()); - failure.property_name = Some("test_property".to_string()); - failure.attempted_value = Some("test_value".to_string()); - failure.exception = Some("test_exception".to_string()); + failure.message = "Test failure message".into(); + failure.error_code = Some("TEST_CODE".into()); + failure.property_name = Some("test_property".into()); + failure.attempted_value = Some("test_value".into()); + failure.exception = Some("test_exception".into()); // Test that all fields are properly set assert_eq!(failure.message, "Test failure message"); diff --git a/native/rust/validation/core/tests/validator_pipeline_tests.rs b/native/rust/validation/core/tests/validator_pipeline_tests.rs index abe9fc19..e1fc8589 100644 --- a/native/rust/validation/core/tests/validator_pipeline_tests.rs +++ b/native/rust/validation/core/tests/validator_pipeline_tests.rs @@ -242,12 +242,12 @@ fn validate_bytes_signature_missing_payload_when_detached_and_no_payload_provide assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_MISSING_PAYLOAD) ); } @@ -268,12 +268,12 @@ fn validate_bytes_signature_errors_when_alg_missing() { assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_NO_APPLICABLE_SIGNATURE_VALIDATOR) ); } @@ -315,12 +315,12 @@ fn validate_bytes_embedded_payload_signature_success_and_failure_paths() { .unwrap(); assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED) ); } @@ -340,12 +340,12 @@ fn validate_bytes_embedded_payload_signature_success_and_failure_paths() { .unwrap(); assert_eq!(ValidationResultKind::Failure, result.signature.kind); assert_eq!( - Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED.to_string()), result .signature .failures .first() - .and_then(|f| f.error_code.clone()) + .and_then(|f| f.error_code.as_deref()), + Some(CoseSign1Validator::ERROR_CODE_SIGNATURE_VERIFICATION_FAILED) ); } } diff --git a/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs b/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs index 72cf2835..01b0011e 100644 --- a/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs +++ b/native/rust/validation/core/tests/validator_simple_coverage_gaps.rs @@ -38,11 +38,11 @@ fn test_validation_failure_fields() { assert!(failure.exception.is_none()); // Test field assignment - failure.message = "Test message".to_string(); - failure.error_code = Some("TEST_CODE".to_string()); - failure.property_name = Some("test_prop".to_string()); - failure.attempted_value = Some("test_val".to_string()); - failure.exception = Some("test_exception".to_string()); + failure.message = "Test message".into(); + failure.error_code = Some("TEST_CODE".into()); + failure.property_name = Some("test_prop".into()); + failure.attempted_value = Some("test_val".into()); + failure.exception = Some("test_exception".into()); assert_eq!(failure.message, "Test message"); assert_eq!(failure.error_code.as_deref(), Some("TEST_CODE")); @@ -189,13 +189,13 @@ fn test_validation_options_with_detached_payload() { #[test] fn test_validation_error_types() { - let decode_error = CoseSign1ValidationError::CoseDecode("Invalid CBOR".to_string()); + let decode_error = CoseSign1ValidationError::CoseDecode("Invalid CBOR".into()); match decode_error { CoseSign1ValidationError::CoseDecode(msg) => assert_eq!(msg, "Invalid CBOR"), _ => panic!("Unexpected error type"), } - let trust_error = CoseSign1ValidationError::Trust("Trust evaluation failed".to_string()); + let trust_error = CoseSign1ValidationError::Trust("Trust evaluation failed".into()); match trust_error { CoseSign1ValidationError::Trust(msg) => assert_eq!(msg, "Trust evaluation failed"), _ => panic!("Unexpected error type"), @@ -211,8 +211,8 @@ fn test_validation_result_metadata_reason_key() { #[test] fn test_cloneable_types() { let failure = ValidationFailure { - message: "test".to_string(), - error_code: Some("CODE".to_string()), + message: "test".into(), + error_code: Some("CODE".into()), property_name: None, attempted_value: None, exception: None, From 7a59744a8bb34647511c3692f6fc820054ac54f2 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Tue, 7 Apr 2026 19:18:13 -0700 Subject: [PATCH 8/8] Add comprehensive README.md for did_x509 crate Documents the DID:x509 method implementation including parsing, building, validation, and resolution capabilities. Covers architecture, all modules, key types with usage examples, supported policies, FFI surface, SCITT compliance patterns, and memory design notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- native/rust/did/x509/README.md | 319 +++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 native/rust/did/x509/README.md diff --git a/native/rust/did/x509/README.md b/native/rust/did/x509/README.md new file mode 100644 index 00000000..89ec3773 --- /dev/null +++ b/native/rust/did/x509/README.md @@ -0,0 +1,319 @@ + + +# did_x509 + +DID:x509 identifier parsing, building, validation, and resolution. + +## Overview + +This crate implements the [DID:x509 method specification](https://github.com/nicosResworworking-group/did-x509), +which creates Decentralized Identifiers (DIDs) from X.509 certificate chains. +A DID:x509 identifier binds a trust anchor (CA certificate fingerprint) to one +or more policy constraints (EKU, subject, SAN, Fulcio issuer) that must be +satisfied by the leaf certificate in a presented chain. + +Key capabilities: + +- **Parsing** — zero-copy-friendly DID:x509 string parsing with full validation +- **Building** — fluent construction of DID:x509 identifiers from certificate chains +- **Validation** — validate DID:x509 identifiers against certificate chains +- **Resolution** — resolve DID:x509 identifiers to W3C DID Documents with JWK public keys +- **Policy validators** — EKU, Subject DN, SAN (email/dns/uri/dn), and Fulcio issuer +- **FFI** — complete C/C++ projection via the companion `did_x509_ffi` crate + +## DID:x509 Format + +``` +did:x509:0:sha256:::eku::::subject:CN: +│ │ │ │ │ │ +│ │ │ │ │ └─ Subject policy +│ │ │ │ └─ EKU policy +│ │ │ └─ Base64url-encoded CA certificate fingerprint +│ │ └─ Hash algorithm (sha256, sha384, sha512) +│ └─ Version (always 0) +└─ DID method prefix +``` + +Multiple policies are separated by `::` (double colon). Within a policy, values +are separated by `:` (single colon). Special characters are percent-encoded. + +## Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ did_x509 │ +├─────────────┬───────────────┬───────────────────┤ +│ parsing/ │ builder │ validator │ +│ ├ Parser │ ├ build() │ ├ validate() │ +│ ├ Percent │ ├ build_ │ └ policy match │ +│ │ encode │ │ sha256() │ │ +│ └ Percent │ ├ build_ ├───────────────────┤ +│ decode │ │ from_ │ resolver │ +│ │ │ chain() │ ├ resolve() │ +│ │ └ build_ │ ├ RSA→JWK │ +│ │ from_ │ └ EC→JWK │ +│ │ chain_ │ │ +│ │ with_eku()│ │ +├─────────────┴───────────────┴───────────────────┤ +│ models/ │ +│ ├ DidX509ParsedIdentifier │ +│ ├ DidX509Policy (Eku, Subject, San, Fulcio) │ +│ ├ DidX509ValidationResult │ +│ ├ SanType (Email, Dns, Uri, Dn) │ +│ ├ CertificateInfo, X509Name │ +│ └ SubjectAlternativeName │ +├─────────────────────────────────────────────────┤ +│ policy_validators │ x509_extensions │ +│ ├ validate_eku() │ ├ extract_eku_oids() │ +│ ├ validate_subject()│ ├ extract_extended_ │ +│ ├ validate_san() │ │ key_usage() │ +│ └ validate_fulcio() │ ├ extract_fulcio_issuer()│ +│ │ └ extract_san() │ +├──────────────────────┴──────────────────────────┤ +│ did_document │ constants │ +│ ├ DidDocument │ ├ OID constants │ +│ ├ Verification │ ├ Attribute labels │ +│ │ Method │ └ oid_to_attribute_ │ +│ └ to_json() │ label() │ +└─────────────────────────────────────────────────┘ + │ + ▼ + x509-parser (DER parsing) + sha2 (fingerprint hashing) + serde/serde_json (DID Document serialization) +``` + +## Modules + +| Module | Description | +|--------|-------------| +| `parsing` | `DidX509Parser::parse()` — parses DID:x509 strings into structured identifiers | +| `builder` | `DidX509Builder` — constructs DID:x509 strings from certificates and policies | +| `validator` | `DidX509Validator::validate()` — validates DIDs against certificate chains | +| `resolver` | `DidX509Resolver::resolve()` — resolves DIDs to W3C DID Documents | +| `models` | Core types: `DidX509ParsedIdentifier`, `DidX509Policy`, `DidX509ValidationResult` | +| `policy_validators` | Per-policy validation: EKU, Subject DN, SAN, Fulcio issuer | +| `x509_extensions` | X.509 extension extraction utilities (EKU, SAN, Fulcio) | +| `san_parser` | Subject Alternative Name parsing from certificates | +| `did_document` | W3C DID Document model with JWK-based verification methods | +| `constants` | DID:x509 format constants, well-known OIDs, attribute labels | +| `error` | `DidX509Error` with 24 descriptive variants | + +## Key Types + +### `DidX509Parser` + +Parses a DID:x509 string into its structured components with full validation +of version, hash algorithm, fingerprint length, and policy syntax. + +```rust +use did_x509::DidX509Parser; + +let did = "did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkExample::eku:1.3.6.1.5.5.7.3.3"; +let parsed = DidX509Parser::parse(did).unwrap(); + +assert_eq!(parsed.hash_algorithm, "sha256"); +assert!(parsed.has_eku_policy()); +assert_eq!(parsed.policies.len(), 1); +``` + +### `DidX509Builder` + +Constructs DID:x509 identifier strings from CA certificates and policy constraints. + +```rust +use did_x509::{DidX509Builder, DidX509Policy}; + +// Build from a CA certificate with EKU policy +let did = DidX509Builder::build_sha256( + ca_cert_der, + &[DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()])], +).unwrap(); + +// Build from a certificate chain (automatically uses root as CA) +let did = DidX509Builder::build_from_chain( + &[leaf_der, intermediate_der, root_der], + &[DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()])], +).unwrap(); + +// Build with EKU extracted from the leaf certificate +let did = DidX509Builder::build_from_chain_with_eku( + &[leaf_der, intermediate_der, root_der], +).unwrap(); +``` + +### `DidX509Validator` + +Validates a DID:x509 identifier against a certificate chain by verifying the +CA fingerprint matches a certificate in the chain and all policy constraints +are satisfied by the leaf certificate. + +```rust +use did_x509::DidX509Validator; + +let result = DidX509Validator::validate(did_string, &[leaf_der, root_der]).unwrap(); + +if result.is_valid { + println!("CA matched at chain index: {}", result.matched_ca_index.unwrap()); +} else { + for error in &result.errors { + eprintln!("Validation error: {}", error); + } +} +``` + +### `DidX509Resolver` + +Resolves a DID:x509 identifier to a W3C DID Document containing the leaf +certificate's public key in JWK format. Performs full validation first. + +```rust +use did_x509::DidX509Resolver; + +let doc = DidX509Resolver::resolve(did_string, &[leaf_der, root_der]).unwrap(); + +// DID Document contains the public key as a JsonWebKey2020 verification method +assert_eq!(doc.id, did_string); +assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); + +// Serialize to JSON +let json = doc.to_json(true).unwrap(); +``` + +### `DidX509Policy` + +Policy constraints that can be included in a DID:x509 identifier: + +```rust +use did_x509::{DidX509Policy, SanType}; + +// Extended Key Usage — OID list +let eku = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()]); + +// Subject Distinguished Name — key-value pairs +let subject = DidX509Policy::Subject(vec![ + ("CN".to_string(), "example.com".to_string()), + ("O".to_string(), "Example Corp".to_string()), +]); + +// Subject Alternative Name — typed value +let san = DidX509Policy::San(SanType::Email, "user@example.com".to_string()); + +// Fulcio issuer — OIDC issuer URL +let fulcio = DidX509Policy::FulcioIssuer("https://accounts.google.com".to_string()); +``` + +### `DidX509Error` + +Comprehensive error type with 24 variants covering every failure mode: + +| Category | Variants | +|----------|----------| +| Format | `EmptyDid`, `InvalidPrefix`, `InvalidFormat`, `MissingPolicies` | +| Version | `UnsupportedVersion` | +| Hash | `UnsupportedHashAlgorithm`, `EmptyFingerprint`, `FingerprintLengthMismatch`, `InvalidFingerprintChars` | +| Policy syntax | `EmptyPolicy`, `InvalidPolicyFormat`, `EmptyPolicyName`, `EmptyPolicyValue` | +| EKU | `InvalidEkuOid` | +| Subject | `InvalidSubjectPolicyComponents`, `EmptySubjectPolicyKey`, `DuplicateSubjectPolicyKey` | +| SAN | `InvalidSanPolicyFormat`, `InvalidSanType` | +| Fulcio | `EmptyFulcioIssuer` | +| Chain | `InvalidChain`, `CertificateParseError`, `NoCaMatch` | +| Validation | `PolicyValidationFailed`, `ValidationFailed` | +| Encoding | `PercentDecodingError`, `InvalidHexCharacter` | + +## Supported Hash Algorithms + +| Algorithm | Fingerprint Length | Constant | +|-----------|--------------------|----------| +| SHA-256 | 32 bytes (43 base64url chars) | `HASH_ALGORITHM_SHA256` | +| SHA-384 | 48 bytes (64 base64url chars) | `HASH_ALGORITHM_SHA384` | +| SHA-512 | 64 bytes (86 base64url chars) | `HASH_ALGORITHM_SHA512` | + +## Supported Policies + +| Policy | DID Syntax | Description | +|--------|-----------|-------------| +| EKU | `eku::` | Extended Key Usage OIDs must all be present on the leaf cert | +| Subject | `subject::` | Subject DN attributes must match (CN, O, OU, L, ST, C, STREET) | +| SAN | `san::` | Subject Alternative Name must match (email, dns, uri, dn) | +| Fulcio Issuer | `fulcio-issuer:` | Fulcio OIDC issuer extension must match | + +## FFI Support + +The companion `did_x509_ffi` crate exposes the full API through C-compatible functions: + +| FFI Function | Purpose | +|-------------|---------| +| `did_x509_parse` | Parse a DID:x509 string into a handle | +| `did_x509_parsed_get_fingerprint` | Get the CA fingerprint bytes | +| `did_x509_parsed_get_hash_algorithm` | Get the hash algorithm string | +| `did_x509_parsed_get_policy_count` | Get the number of policies | +| `did_x509_parsed_free` | Free a parsed handle | +| `did_x509_build_with_eku` | Build a DID:x509 string with EKU policy | +| `did_x509_build_from_chain` | Build from a certificate chain | +| `did_x509_validate` | Validate a DID against a certificate chain | +| `did_x509_resolve` | Resolve a DID to a JSON DID Document | +| `did_x509_error_message` | Get last error message | +| `did_x509_error_code` | Get last error code | +| `did_x509_error_free` | Free an error handle | +| `did_x509_string_free` | Free a Rust-allocated string | + +C and C++ headers are available at: +- **C**: `native/c/include/cose/did/x509.h` +- **C++**: `native/c_pp/include/cose/did/x509.hpp` + +## Usage Example: SCITT Compliance + +A common pattern for SCITT (Supply Chain Integrity, Transparency, and Trust) +compliance is to build a DID:x509 identifier from a signing certificate chain +and embed it as the `iss` (issuer) claim in CWT protected headers: + +```rust +use did_x509::{DidX509Builder, DidX509Policy, DidX509Validator}; + +// 1. Build the DID from the signing chain (leaf-first order) +let did = DidX509Builder::build_from_chain_with_eku( + &[leaf_der, intermediate_der, root_der], +).expect("Failed to build DID:x509"); + +// 2. The DID string can be used as the CWT `iss` claim +// e.g., "did:x509:0:sha256:::eku:1.3.6.1.5.5.7.3.3" + +// 3. During validation, verify the DID against the presented chain +let result = DidX509Validator::validate(&did, &[leaf_der, intermediate_der, root_der]) + .expect("Validation error"); +assert!(result.is_valid); +``` + +## Dependencies + +| Crate | Purpose | +|-------|---------| +| `x509-parser` | DER certificate parsing, extension extraction | +| `sha2` | SHA-256/384/512 fingerprint computation | +| `serde` / `serde_json` | DID Document JSON serialization | + +## Memory Design + +- **Parsing**: `DidX509Parser::parse()` returns owned `DidX509ParsedIdentifier` (allocation required for fingerprint bytes and policy data extracted from the DID string) +- **Policies**: `DidX509Policy::Eku` uses `Vec>` — static OID strings use `Cow::Borrowed` (zero allocation), dynamic OIDs use `Cow::Owned` +- **DID Documents**: `VerificationMethod` JWK maps use `HashMap, String>` — all JWK field names (`kty`, `crv`, `x`, `y`, `n`, `e`) are `Cow::Borrowed` +- **Validation**: `DidX509ValidationResult` collects errors as `Vec` — only allocated on validation failure +- **Fingerprinting**: SHA digests use `to_vec()` for cross-algorithm uniform handling (structurally required) +- **Policy validators**: Borrow certificate data (zero-copy) — only allocate on error paths + +## Test Coverage + +The crate has 23 test files covering: + +- Parser tests: format validation, edge cases, percent encoding/decoding +- Builder tests: SHA-256/384/512, chain construction, EKU extraction +- Validator tests: fingerprint matching, policy validation, error cases +- Resolver tests: RSA and EC key conversion, DID Document generation +- Policy validator tests: EKU, Subject DN, SAN, Fulcio issuer +- X.509 extension tests: extraction utilities +- Comprehensive edge case and coverage-targeted tests + +## License + +Licensed under the MIT License. See [LICENSE](../../../../LICENSE) for details. \ No newline at end of file