diff --git a/Cargo.toml b/Cargo.toml index 951a188..40269f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,18 @@ readme = "README.md" roxmltree = { version = "0.21", features = ["positions"] } # Crypto -ring = "0.17" +rsa = { version = "0.9", optional = true } +sha1 = { version = "0.10", features = ["oid"], optional = true } +sha2 = { version = "0.10", features = ["oid"], optional = true } +p256 = { version = "0.13", features = ["ecdsa"], optional = true } +p384 = { version = "0.13", features = ["ecdsa"], optional = true } +p521 = { version = "0.13", features = ["ecdsa"], optional = true } +signature = { version = "2", optional = true } +subtle = { version = "2", optional = true } # X.509 certificates -x509-parser = "0.18" -der = "0.8" +x509-parser = { version = "0.18", optional = true } +der = { version = "0.8", optional = true } # Base64 encoding/decoding base64 = "0.22" @@ -31,6 +38,17 @@ thiserror = "2" [features] default = ["xmldsig", "c14n"] -xmldsig = [] # XML Digital Signatures (sign + verify) +xmldsig = [ # XML Digital Signatures (sign + verify) + "dep:der", + "dep:p256", + "dep:p384", + "dep:p521", + "dep:rsa", + "dep:sha1", + "dep:sha2", + "dep:signature", + "dep:subtle", + "dep:x509-parser", +] xmlenc = [] # XML Encryption (encrypt + decrypt) c14n = [] # XML Canonicalization (inclusive + exclusive) diff --git a/src/xmldsig/digest.rs b/src/xmldsig/digest.rs index 9b2d505..a602142 100644 --- a/src/xmldsig/digest.rs +++ b/src/xmldsig/digest.rs @@ -3,9 +3,11 @@ //! Implements [XMLDSig §6.1](https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod): //! compute message digests over transform output bytes using SHA-family algorithms. //! -//! All digest computation uses `ring::digest` (no custom implementations). +//! All digest computation uses RustCrypto hash implementations. -use ring::digest; +use sha1::Sha1; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use subtle::ConstantTimeEq; /// Digest algorithms supported by XMLDSig. /// @@ -73,24 +75,18 @@ impl DigestAlgorithm { Self::Sha512 => 64, } } - - /// Map to the corresponding `ring::digest` algorithm. - fn ring_algorithm(self) -> &'static digest::Algorithm { - match self { - Self::Sha1 => &digest::SHA1_FOR_LEGACY_USE_ONLY, - Self::Sha256 => &digest::SHA256, - Self::Sha384 => &digest::SHA384, - Self::Sha512 => &digest::SHA512, - } - } } /// Compute the digest of `data` using the specified algorithm. /// /// Returns the raw digest bytes (not base64-encoded). pub fn compute_digest(algorithm: DigestAlgorithm, data: &[u8]) -> Vec { - let result = digest::digest(algorithm.ring_algorithm(), data); - result.as_ref().to_vec() + match algorithm { + DigestAlgorithm::Sha1 => Sha1::digest(data).to_vec(), + DigestAlgorithm::Sha256 => Sha256::digest(data).to_vec(), + DigestAlgorithm::Sha384 => Sha384::digest(data).to_vec(), + DigestAlgorithm::Sha512 => Sha512::digest(data).to_vec(), + } } /// Constant-time comparison of two byte slices. @@ -100,13 +96,9 @@ pub fn compute_digest(algorithm: DigestAlgorithm, data: &[u8]) -> Vec { /// where they differ — preventing timing side-channel attacks on digest /// comparison. /// -/// Uses `ring::constant_time::verify_slices_are_equal` internally. +/// Uses `subtle` constant-time equality. pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { - #[expect( - deprecated, - reason = "legacy ring constant-time helper is still used here" - )] - ring::constant_time::verify_slices_are_equal(a, b).is_ok() + a.ct_eq(b).into() } #[cfg(test)] diff --git a/src/xmldsig/parse.rs b/src/xmldsig/parse.rs index 4d5266a..1d6c19e 100644 --- a/src/xmldsig/parse.rs +++ b/src/xmldsig/parse.rs @@ -38,7 +38,12 @@ pub enum SignatureAlgorithm { RsaSha512, /// ECDSA P-256 with SHA-256. EcdsaP256Sha256, - /// ECDSA P-384 with SHA-384. + /// XMLDSig `ecdsa-sha384` URI. + /// + /// The variant name is historical. + /// + /// Verification currently accepts this XMLDSig URI for P-384 and for the + /// donor P-521 interop case. EcdsaP384Sha384, } diff --git a/src/xmldsig/signature.rs b/src/xmldsig/signature.rs index 3e6d13e..af95091 100644 --- a/src/xmldsig/signature.rs +++ b/src/xmldsig/signature.rs @@ -1,15 +1,25 @@ //! Signature verification helpers for XMLDSig. //! //! This module currently covers roadmap task P1-019 (RSA PKCS#1 v1.5) and -//! P1-020 (ECDSA P-256/P-384) verification. +//! P1-020 (ECDSA P-256/P-384) verification, plus donor P-521 interop under +//! the XMLDSig `ecdsa-sha384` URI. //! //! Input public keys are accepted in SubjectPublicKeyInfo (SPKI) form because -//! that is how the vendored PEM fixtures are stored. `ring` expects the inner -//! SPKI payload for both algorithm families: -//! - RSA: ASN.1 `RSAPublicKey` -//! - ECDSA: uncompressed SEC1 EC point bytes from the SPKI bit string - -use ring::signature; +//! that is how the vendored PEM fixtures are stored. +//! - RSA keys are parsed from full SPKI DER (`PUBLIC KEY`) and verified via +//! RustCrypto `rsa::pkcs1v15`. +//! - ECDSA keys are validated as uncompressed SEC1 points from the SPKI bit +//! string and verified with RustCrypto curve crates (`p256`/`p384`/`p521`). + +use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey}; +use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey}; +use p521::ecdsa::{Signature as P521Signature, VerifyingKey as P521VerifyingKey}; +use rsa::pkcs1v15::{Signature as RsaPkcs1v15Signature, VerifyingKey as RsaVerifyingKey}; +use rsa::pkcs8::DecodePublicKey; +use sha1::Sha1; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use signature::hazmat::PrehashVerifier; +use signature::{DigestVerifier, Verifier}; use x509_parser::prelude::FromDer; use x509_parser::public_key::{ECPoint, PublicKey}; use x509_parser::x509::SubjectPublicKeyInfo; @@ -125,7 +135,6 @@ pub fn verify_rsa_signature_spki( signed_data: &[u8], signature_value: &[u8], ) -> Result { - let verification_algorithm = verification_algorithm(algorithm)?; let (rest, spki) = SubjectPublicKeyInfo::from_der(public_key_spki_der) .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; if !rest.is_empty() { @@ -138,13 +147,41 @@ pub fn verify_rsa_signature_spki( match public_key { PublicKey::RSA(rsa) => { validate_rsa_public_key(&rsa, algorithm)?; - let key = signature::UnparsedPublicKey::new( - verification_algorithm, - spki.subject_public_key.data, - ); - Ok(key.verify(signed_data, signature_value).is_ok()) + let key = rsa::RsaPublicKey::from_public_key_der(public_key_spki_der) + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + let Ok(signature) = RsaPkcs1v15Signature::try_from(signature_value) else { + return Ok(false); + }; + + let verified = match algorithm { + SignatureAlgorithm::RsaSha1 => { + let key = RsaVerifyingKey::::new(key); + key.verify(signed_data, &signature).is_ok() + } + SignatureAlgorithm::RsaSha256 => { + let key = RsaVerifyingKey::::new(key); + key.verify(signed_data, &signature).is_ok() + } + SignatureAlgorithm::RsaSha384 => { + let key = RsaVerifyingKey::::new(key); + key.verify(signed_data, &signature).is_ok() + } + SignatureAlgorithm::RsaSha512 => { + let key = RsaVerifyingKey::::new(key); + key.verify(signed_data, &signature).is_ok() + } + _ => { + return Err(SignatureVerificationError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }); + } + }; + + Ok(verified) } - _ => Err(SignatureVerificationError::InvalidKeyDer), + _ => Err(SignatureVerificationError::KeyAlgorithmMismatch { + uri: algorithm.uri().to_string(), + }), } } @@ -183,26 +220,28 @@ pub fn verify_ecdsa_signature_spki( match public_key { PublicKey::EC(ec) => { validate_ec_public_key_encoding(&ec, &spki.subject_public_key.data)?; - let (fixed_algorithm, asn1_algorithm, signature_encoding) = - ecdsa_verification_algorithms(&spki, &ec, algorithm, signature_value)?; - let public_key = &spki.subject_public_key.data; - let fixed_key = signature::UnparsedPublicKey::new(fixed_algorithm, public_key); - let asn1_key = signature::UnparsedPublicKey::new(asn1_algorithm, public_key); - - match signature_encoding { - EcdsaSignatureEncoding::XmlDsigFixed => { - Ok(fixed_key.verify(signed_data, signature_value).is_ok()) - } - EcdsaSignatureEncoding::Asn1Der => { - Ok(asn1_key.verify(signed_data, signature_value).is_ok()) - } - EcdsaSignatureEncoding::Ambiguous => { - if asn1_key.verify(signed_data, signature_value).is_ok() { - return Ok(true); - } - - Ok(fixed_key.verify(signed_data, signature_value).is_ok()) - } + let (curve, component_len) = ecdsa_curve_and_component_len(&spki, &ec, algorithm)?; + let signature_encoding = + classify_ecdsa_signature_encoding(signature_value, component_len)?; + match curve { + EcCurve::P256 => verify_ecdsa_p256_sha256( + &spki.subject_public_key.data, + signed_data, + signature_value, + signature_encoding, + ), + EcCurve::P384 => verify_ecdsa_p384_sha384( + &spki.subject_public_key.data, + signed_data, + signature_value, + signature_encoding, + ), + EcCurve::P521 => verify_ecdsa_p521_sha384( + &spki.subject_public_key.data, + signed_data, + signature_value, + signature_encoding, + ), } } _ => Err(SignatureVerificationError::KeyAlgorithmMismatch { @@ -260,33 +299,18 @@ fn minimum_rsa_modulus_bits( } } -fn verification_algorithm( - algorithm: SignatureAlgorithm, -) -> Result<&'static dyn signature::VerificationAlgorithm, SignatureVerificationError> { - match algorithm { - SignatureAlgorithm::RsaSha1 => Ok(&signature::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY), - SignatureAlgorithm::RsaSha256 => Ok(&signature::RSA_PKCS1_2048_8192_SHA256), - SignatureAlgorithm::RsaSha384 => Ok(&signature::RSA_PKCS1_2048_8192_SHA384), - SignatureAlgorithm::RsaSha512 => Ok(&signature::RSA_PKCS1_2048_8192_SHA512), - _ => Err(SignatureVerificationError::UnsupportedAlgorithm { - uri: algorithm.uri().to_string(), - }), - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EcCurve { + P256, + P384, + P521, } -fn ecdsa_verification_algorithms( +fn ecdsa_curve_and_component_len( spki: &SubjectPublicKeyInfo<'_>, ec: &ECPoint<'_>, algorithm: SignatureAlgorithm, - signature_value: &[u8], -) -> Result< - ( - &'static dyn signature::VerificationAlgorithm, - &'static dyn signature::VerificationAlgorithm, - EcdsaSignatureEncoding, - ), - SignatureVerificationError, -> { +) -> Result<(EcCurve, usize), SignatureVerificationError> { let curve_oid = spki .algorithm .parameters @@ -296,45 +320,166 @@ fn ecdsa_verification_algorithms( let point_len = ec.key_size(); let curve_oid = curve_oid.to_id_string(); - let (fixed_algorithm, asn1_algorithm, component_len) = match algorithm { + match algorithm { SignatureAlgorithm::EcdsaP256Sha256 => { if curve_oid == "1.2.840.10045.3.1.7" && point_len == 256 { - ( - &signature::ECDSA_P256_SHA256_FIXED, - &signature::ECDSA_P256_SHA256_ASN1, - 32, - ) + Ok((EcCurve::P256, 32)) } else { - return Err(SignatureVerificationError::KeyAlgorithmMismatch { + Err(SignatureVerificationError::KeyAlgorithmMismatch { uri: algorithm.uri().to_string(), - }); + }) } } SignatureAlgorithm::EcdsaP384Sha384 => { if curve_oid == "1.3.132.0.34" && point_len == 384 { - ( - &signature::ECDSA_P384_SHA384_FIXED, - &signature::ECDSA_P384_SHA384_ASN1, - 48, - ) + Ok((EcCurve::P384, 48)) + // XMLDSig `ecdsa-sha384` identifies the digest/signature method URI, + // not a single curve. For interop we accept secp521r1 donor vectors; + // x509-parser reports ECPoint::key_size() as byte-aligned bits (528) + // for P-521 uncompressed points, so allow both exact and aligned size. + } else if curve_oid == "1.3.132.0.35" && matches!(point_len, 521 | 528) { + Ok((EcCurve::P521, 66)) } else { - return Err(SignatureVerificationError::KeyAlgorithmMismatch { + Err(SignatureVerificationError::KeyAlgorithmMismatch { uri: algorithm.uri().to_string(), - }); + }) + } + } + _ => Err(SignatureVerificationError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }), + } +} + +fn verify_ecdsa_p256_sha256( + public_key: &[u8], + signed_data: &[u8], + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, +) -> Result { + let key = P256VerifyingKey::from_sec1_bytes(public_key) + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + let mut digest = Sha256::new(); + digest.update(signed_data); + verify_p256_signature(&key, signature_value, signature_encoding, digest) +} + +fn verify_ecdsa_p384_sha384( + public_key: &[u8], + signed_data: &[u8], + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, +) -> Result { + let key = P384VerifyingKey::from_sec1_bytes(public_key) + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + let mut digest = Sha384::new(); + digest.update(signed_data); + verify_p384_signature(&key, signature_value, signature_encoding, digest) +} + +fn verify_ecdsa_p521_sha384( + public_key: &[u8], + signed_data: &[u8], + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, +) -> Result { + let key = P521VerifyingKey::from_sec1_bytes(public_key) + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + let prehash = Sha384::digest(signed_data); + verify_p521_signature(&key, signature_value, signature_encoding, &prehash) +} + +fn verify_p256_signature( + key: &P256VerifyingKey, + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, + digest: Sha256, +) -> Result { + match signature_encoding { + EcdsaSignatureEncoding::XmlDsigFixed => { + let signature = P256Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) + } + EcdsaSignatureEncoding::Asn1Der => { + let signature = P256Signature::from_der(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) + } + EcdsaSignatureEncoding::Ambiguous => { + if let Ok(signature) = P256Signature::from_der(signature_value) + && key.verify_digest(digest.clone(), &signature).is_ok() + { + return Ok(true); } + + let signature = P256Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) } - _ => { - return Err(SignatureVerificationError::UnsupportedAlgorithm { - uri: algorithm.uri().to_string(), - }); + } +} + +fn verify_p384_signature( + key: &P384VerifyingKey, + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, + digest: Sha384, +) -> Result { + match signature_encoding { + EcdsaSignatureEncoding::XmlDsigFixed => { + let signature = P384Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) } - }; + EcdsaSignatureEncoding::Asn1Der => { + let signature = P384Signature::from_der(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) + } + EcdsaSignatureEncoding::Ambiguous => { + if let Ok(signature) = P384Signature::from_der(signature_value) + && key.verify_digest(digest.clone(), &signature).is_ok() + { + return Ok(true); + } + + let signature = P384Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_digest(digest, &signature).is_ok()) + } + } +} - Ok(( - fixed_algorithm, - asn1_algorithm, - classify_ecdsa_signature_encoding(signature_value, component_len)?, - )) +fn verify_p521_signature( + key: &P521VerifyingKey, + signature_value: &[u8], + signature_encoding: EcdsaSignatureEncoding, + prehash: &[u8], +) -> Result { + match signature_encoding { + EcdsaSignatureEncoding::XmlDsigFixed => { + let signature = P521Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_prehash(prehash, &signature).is_ok()) + } + EcdsaSignatureEncoding::Asn1Der => { + let signature = P521Signature::from_der(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_prehash(prehash, &signature).is_ok()) + } + EcdsaSignatureEncoding::Ambiguous => { + if let Ok(signature) = P521Signature::from_der(signature_value) + && key.verify_prehash(prehash, &signature).is_ok() + { + return Ok(true); + } + + let signature = P521Signature::from_slice(signature_value) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + Ok(key.verify_prehash(prehash, &signature).is_ok()) + } + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -505,7 +650,7 @@ mod tests { SignatureAlgorithm::EcdsaP256Sha256, SignatureAlgorithm::EcdsaP384Sha384, ] { - let err = verification_algorithm(algorithm).unwrap_err(); + let err = minimum_rsa_modulus_bits(algorithm).unwrap_err(); assert!(matches!( err, SignatureVerificationError::UnsupportedAlgorithm { .. } diff --git a/tests/c14n_golden.rs b/tests/c14n_golden.rs index f8b0d77..aac56a4 100644 --- a/tests/c14n_golden.rs +++ b/tests/c14n_golden.rs @@ -20,7 +20,7 @@ use std::collections::HashSet; use std::fs; -use ring::digest; +use sha1::{Digest, Sha1}; use xml_sec::c14n::{C14nAlgorithm, C14nMode, canonicalize, canonicalize_xml}; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -36,8 +36,7 @@ fn fixture_bytes(path: &str) -> Vec { } fn sha1_base64(data: &[u8]) -> String { - let hash = digest::digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, data); - base64_encode(hash.as_ref()) + base64_encode(&Sha1::digest(data)) } fn base64_encode(data: &[u8]) -> String { diff --git a/tests/donor_full_verification_suite.rs b/tests/donor_full_verification_suite.rs new file mode 100644 index 0000000..a16bc8e --- /dev/null +++ b/tests/donor_full_verification_suite.rs @@ -0,0 +1,248 @@ +//! Donor full verification suite for ROADMAP task P1-025. +//! +//! This suite tracks pass/fail/skip accounting across donor vectors and +//! enforces that all supported donor vectors verify end-to-end. + +use std::path::{Path, PathBuf}; + +use xml_sec::xmldsig::{ + DsigError, DsigStatus, FailureReason, ParseError, VerifyContext, verify_signature_with_pem_key, +}; + +#[derive(Clone, Copy)] +enum SkipProbe { + KeyNotFound, + UnsupportedSignatureAlgorithm, +} + +#[derive(Clone, Copy)] +enum Expectation { + ValidWithKey { + key_path: &'static str, + }, + Skip { + reason: &'static str, + probe: SkipProbe, + }, +} + +struct VectorCase { + name: &'static str, + xml_path: &'static str, + expectation: Expectation, +} + +fn read_fixture(path: &Path) -> String { + std::fs::read_to_string(path) + .unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display())) +} + +fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn cases() -> Vec { + vec![ + // Aleksey donor vectors: supported algorithms must pass end-to-end. + VectorCase { + name: "aleksey-rsa-sha1", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha1-rsa-sha1.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-rsa-sha256", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/rsa/rsa-2048-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-rsa-sha384", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha384-rsa-sha384.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-rsa-sha512", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha512-rsa-sha512.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-ecdsa-p256-sha256", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-ecdsa-p521-sha384", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha384-ecdsa-sha384.xml", + expectation: Expectation::ValidWithKey { + key_path: "tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem", + }, + }, + VectorCase { + name: "aleksey-rsa-sha512-x509-digest", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-x509-digest-sha512.xml", + expectation: Expectation::Skip { + reason: "X509Digest key resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::KeyNotFound, + }, + }, + // Merlin "basic signatures" required by P1-025. + // These are tracked explicitly as skips until P2/P4 capabilities exist. + VectorCase { + name: "merlin-enveloped-dsa", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloped-dsa.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + VectorCase { + name: "merlin-enveloping-rsa-keyvalue", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloping-rsa.xml", + expectation: Expectation::Skip { + reason: "KeyValue auto-resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::KeyNotFound, + }, + }, + VectorCase { + name: "merlin-x509-crt", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009); X509 KeyInfo resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + VectorCase { + name: "merlin-x509-crt-crl", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009); X509/CRL KeyInfo resolution is not implemented yet (planned P2-009/P2-005)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + VectorCase { + name: "merlin-x509-is", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-is.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009); X509IssuerSerial resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + VectorCase { + name: "merlin-x509-ski", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-ski.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009); X509SKI resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + VectorCase { + name: "merlin-x509-sn", + xml_path: "tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-sn.xml", + expectation: Expectation::Skip { + reason: "DSA signature method is not implemented yet (planned P4-009); X509SubjectName resolution is not implemented yet (planned P2-009)", + probe: SkipProbe::UnsupportedSignatureAlgorithm, + }, + }, + ] +} + +#[test] +fn donor_full_verification_suite_tracks_pass_fail_skip_counts() { + let root = project_root(); + let mut passed = 0usize; + let mut failed = Vec::::new(); + let mut skipped = Vec::::new(); + + for case in cases() { + match case.expectation { + Expectation::ValidWithKey { key_path } => { + let xml = read_fixture(&root.join(case.xml_path)); + let key = read_fixture(&root.join(key_path)); + match verify_signature_with_pem_key(&xml, &key, false) { + Ok(result) if matches!(result.status, DsigStatus::Valid) => { + passed += 1; + } + Ok(result) => { + failed.push(format!( + "{}: expected Valid, got {:?}", + case.name, result.status + )); + } + Err(err) => { + failed.push(format!("{}: verification error {err}", case.name)); + } + } + } + Expectation::Skip { reason, probe } => { + let xml = read_fixture(&root.join(case.xml_path)); + roxmltree::Document::parse(&xml) + .unwrap_or_else(|err| panic!("{}: fixture XML must parse: {err}", case.name)); + match probe { + SkipProbe::KeyNotFound => match VerifyContext::new().verify(&xml) { + Ok(result) + if matches!( + result.status, + DsigStatus::Invalid(FailureReason::KeyNotFound) + ) => {} + Ok(result) => failed.push(format!( + "{}: expected Invalid(KeyNotFound) for skipped vector, got {:?}", + case.name, result.status + )), + Err(err) => failed.push(format!( + "{}: expected Invalid(KeyNotFound) for skipped vector, got error {err}", + case.name + )), + }, + SkipProbe::UnsupportedSignatureAlgorithm => match VerifyContext::new().verify(&xml) + { + Err(DsigError::ParseSignedInfo(ParseError::UnsupportedAlgorithm { + .. + })) => {} + Ok(result) => failed.push(format!( + "{}: expected unsupported signature algorithm error for skipped vector, got {:?}", + case.name, result.status + )), + Err(err) => failed.push(format!( + "{}: expected unsupported signature algorithm error for skipped vector, got {err}", + case.name + )), + }, + } + skipped.push(format!("{}: {}", case.name, reason)); + } + } + } + + assert_eq!( + failed.len(), + 0, + "donor full verification suite had failures:\n{}", + failed.join("\n") + ); + + let expected_skipped = vec![ + "aleksey-rsa-sha512-x509-digest: X509Digest key resolution is not implemented yet (planned P2-009)", + "merlin-enveloped-dsa: DSA signature method is not implemented yet (planned P4-009)", + "merlin-enveloping-rsa-keyvalue: KeyValue auto-resolution is not implemented yet (planned P2-009)", + "merlin-x509-crt: DSA signature method is not implemented yet (planned P4-009); X509 KeyInfo resolution is not implemented yet (planned P2-009)", + "merlin-x509-crt-crl: DSA signature method is not implemented yet (planned P4-009); X509/CRL KeyInfo resolution is not implemented yet (planned P2-009/P2-005)", + "merlin-x509-is: DSA signature method is not implemented yet (planned P4-009); X509IssuerSerial resolution is not implemented yet (planned P2-009)", + "merlin-x509-ski: DSA signature method is not implemented yet (planned P4-009); X509SKI resolution is not implemented yet (planned P2-009)", + "merlin-x509-sn: DSA signature method is not implemented yet (planned P4-009); X509SubjectName resolution is not implemented yet (planned P2-009)", + ]; + + // P1-025 minimum expected accounting: + // - all supported aleksey RSA/ECDSA vectors pass + // - unsupported/deferred merlin vectors are tracked as skips with explicit reasons + assert_eq!(passed, 6, "unexpected pass count"); + assert_eq!(skipped, expected_skipped, "unexpected skip inventory"); +} diff --git a/tests/ecdsa_signature_integration.rs b/tests/ecdsa_signature_integration.rs index 58aec8b..b39fb5a 100644 --- a/tests/ecdsa_signature_integration.rs +++ b/tests/ecdsa_signature_integration.rs @@ -7,10 +7,9 @@ use std::path::Path; use base64::Engine; -use ring::rand::SystemRandom; -use ring::signature::{ - ECDSA_P384_SHA384_ASN1_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, EcdsaKeyPair, -}; +use p384::ecdsa::signature::Signer; +use p384::ecdsa::{Signature as P384Signature, SigningKey as P384SigningKey}; +use p384::pkcs8::DecodePrivateKey; use xml_sec::c14n::canonicalize; use xml_sec::xmldsig::parse::{SignatureAlgorithm, find_signature_node, parse_signed_info}; use xml_sec::xmldsig::{ @@ -110,18 +109,16 @@ fn local_p384_signature_matches() { .expect("fixture PEM should parse") .1 .contents; - let rng = SystemRandom::new(); - let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &pkcs8_der, &rng) - .expect("fixture PKCS#8 should parse"); - let signature = key_pair - .sign(&rng, &canonical_signed_info) - .expect("fixture P-384 key should sign"); + let signing_key = + P384SigningKey::from_pkcs8_der(&pkcs8_der).expect("fixture PKCS#8 should parse"); + let signature: P384Signature = signing_key.sign(&canonical_signed_info); + let signature_bytes = signature.to_bytes(); let valid = verify_ecdsa_signature_pem( SignatureAlgorithm::EcdsaP384Sha384, &public_key_pem, &canonical_signed_info, - signature.as_ref(), + signature_bytes.as_ref(), ) .expect("P-384 verification should not error on valid fixtures"); @@ -147,18 +144,16 @@ fn local_p384_der_signature_matches() { .expect("fixture PEM should parse") .1 .contents; - let rng = SystemRandom::new(); - let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_ASN1_SIGNING, &pkcs8_der, &rng) - .expect("fixture PKCS#8 should parse"); - let signature = key_pair - .sign(&rng, &canonical_signed_info) - .expect("fixture P-384 key should sign"); + let signing_key = + P384SigningKey::from_pkcs8_der(&pkcs8_der).expect("fixture PKCS#8 should parse"); + let signature: P384Signature = signing_key.sign(&canonical_signed_info); + let signature_der = signature.to_der(); let valid = verify_ecdsa_signature_pem( SignatureAlgorithm::EcdsaP384Sha384, &public_key_pem, &canonical_signed_info, - signature.as_ref(), + signature_der.as_bytes(), ) .expect("P-384 DER verification should not error on valid fixtures"); diff --git a/tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem b/tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem new file mode 100644 index 0000000..0168b66 --- /dev/null +++ b/tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBr/AHhJsVuacixRwxwUVB7/ERZtvS +Jx8Xqg8T9lwp2qag97WQVSbYEc6pft5WO46vP0xHfDlu8Dy9SMCRNvJP6kIBRmNK +hgj0Hgsv9Ac/rYVlm/63cad5L0sslWiv3m6wT3crJQePmZRBGZ7d8hUB73vm/BbS +ATM3JZVDq0LDcQGtpJs= +-----END PUBLIC KEY----- diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloped-dsa.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloped-dsa.xml new file mode 100644 index 0000000..f5ff1f5 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloped-dsa.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + fdy6S2NLpnT4fMdokUHSHsmpcvo= + + + + Z4pBb+o+XOKWME7CpLyXuNqyIYdXOcGvthfUf+ZDLL5immPx+3tK8Q== + + + + +

+ 3eOeAvqnEyFpW+uTSgrdj7YLjaTkpyHecKFIoLu8QZNkGTQI1ciITBH0lqfIkdCH + Si8fiUC3DTq3J9FsJef4YVtDF7JpUvHTOQqtq7Zgx6KC8Wxkz6rQCxOr7F0ApOYi + 89zLRoe4MkDGe6ux0+WtyOTQoVIGNTDDUFXrUQNbLrE= +

+ + hDLcFK0GO/Hz1arxOOvsgM/VLyU= + + + nnx7hbdWozGbtnFgnbFnopfRl7XRacpkPJRGf5P2IUgVspEUSUoN6i1fDBfBg43z + Kt7dlEaQL7b5+JTZt3MhZNPosxsgxVuT7Ts/g5k7EnpdYv0a5hw5Bw29fjbGHfgM + 8d2rhd2Ui0xHbk0D451nhLxVWulviOSPhzKKvXrbySA= + + + cfYpihpAQeepbNFS4MAbQRhdXpDi5wLrwxE5hIvoYqo1L8BQVu8fY1TFAPtoae1i + Bg/GIJyP3iLfyuBJaDvJJLP30wBH9i/s5J3656PevpOVdTfi777Fi9Gj6y/ib2Vv + +OZfJkkp4L50+p5TUhPmQLJtREsgtl+tnIOyJT++G9U= + +
+
+
+
+
diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloping-rsa.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloping-rsa.xml new file mode 100644 index 0000000..1580d83 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloping-rsa.xml @@ -0,0 +1,31 @@ + + + + + + + + 7/XTsHaBSOnJ/jXD5v0zL6VKYsk= + + + + ov3HOoPN0w71N3DdGNhN+dSzQm6NJFUB5qGKRp9Q986nVzMb8wCIVxCQu+x3vMtq + p4/R3KEcPtEJSaoR+thGq++GPIh2mZXyWJs3xHy9P4xmoTVwli7/l7s8ebDSmnbZ + 7xZU4Iy1BSMZSxGKnRG+Z/0GJIfTz8jhH6wCe3l03L4= + + + + + + q07hpxA5DGFfvJFZueFl/LI85XxQxrvqgVugL25V090A9MrlLBg5PmAsxFTe+G6a + xvWJQwYOVHj/nuiCnNLa9a7uAtPFiTtW+v5H3wlLaY3ws4atRBNOQlYkIBp38sTf + QBkk4i8PEU1GQ2M0CLIJq4/2Akfv1wxzSQ9+8oWkArc= + + + AQAB + + + + + some text + diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml new file mode 100644 index 0000000..fe01797 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml @@ -0,0 +1,47 @@ + + + + + + + + 60NvZvtdTB+7UnlLp/H24p7h4bs= + + + + WF6EaX66f8CdGE6NafmzdLpb/1OVYX4kBNsqgGIqHR5JZAu4HpbVQQ== + + + + + MIIDTjCCAw6gAwIBAgIGAOz5Id5/MAkGByqGSM44BAMwdjELMAkGA1UEBhMCSUUx + DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll + cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEdMBsGA1UEAxMUQW5vdGhlciBUcmFu + c2llbnQgQ0EwHhcNMDIwNDAzMDAwMDI4WhcNMTIwNDAyMjI1OTQ2WjBmMQswCQYD + VQQGEwJJRTEPMA0GA1UECBMGRHVibGluMSQwIgYDVQQKExtCYWx0aW1vcmUgVGVj + aG5vbG9naWVzIEx0ZC4xETAPBgNVBAsTCFgvU2VjdXJlMQ0wCwYDVQQDEwRCcmVz + MIIBtjCCASsGByqGSM44BAEwggEeAoGBAISKsEonjNGgHs/uh+9YKgnwZ8Bt3T7u + yQBJW9dxpMF0cPUXz4dFbSFY4QyW8igCLswpOa+eHHEYsWvE0Nr1lcKHUPXq7u41 + JJwHNq1RAFeZiU6wa+1FL3v1/T1rAgzepV7xS4iafz4vxdHMlfwgKfoyKfq6JU1z + oVM/ahI5xWDDAhUAmEv6eIJrB4KN0fPRABPx3NHYclkCgYAlhuYZ/AzPta7+bE5C + QasmSVzc8uM/e+LN7ABlEXwQRk6QfZBcX8TbePNE8ZFng4Uft/QzAOUxALET7kKA + ek4Jeytpzc0XYCYyuGJATm4F9ZY1pAJ5yQmUmwvDYdlaZJ4ldGzO/R57Evngn/G4 + tqjjoi0sx3jq7czvDwdGHnky0AOBhAACgYBgvDFxw1U6Ou2G6P/+347Jfk2wPB1/ + atr4p3JUVLuT0ExZG6np+rKiXmcBbYKbAhMY37zVkroR9bwo+NgaJGubQ4ex5Y1X + N2Q5gIHNhNfKr8G4LPVqWGxf/lFPDYxX3ezqBJPpJCJTREX7s6Hp/VTV2SpQlySv + +GRcFKJFPlhD9aM6MDgwDgYDVR0PAQH/BAQDAgeAMBEGA1UdDgQKBAiC+5gx0MHL + hTATBgNVHSMEDDAKgAiKHFYwWjISfTAJBgcqhkjOOAQDAy8AMCwCFDTcM5i61uqq + /aveERhOJ6NG/LubAhREVDtAeNbTEywXr4O7KvEEvFLUjg== + + + MIIBJDCB5AIBATAJBgcqhkjOOAQDMHYxCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZE + dWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8G + A1UECxMIWC9TZWN1cmUxHTAbBgNVBAMTFEFub3RoZXIgVHJhbnNpZW50IENBFw0w + MjA0MDQwMjE2NThaFw0xMTA0MDIwMjE2NThaMBkwFwIGAOz5Id5/Fw0wMjA0MDQw + MjE2NThaoCMwITATBgNVHSMEDDAKgAiKHFYwWjISfTAKBgNVHRQEAwIBADAJBgcq + hkjOOAQDAzAAMC0CFCEIm38fvGzSJHms284hUs9dNB8nAhUAjEtZr0TGgc6sVRVk + krEgltdo7Jw= + + + + diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml new file mode 100644 index 0000000..2048fd2 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml @@ -0,0 +1,38 @@ + + + + + + + + 60NvZvtdTB+7UnlLp/H24p7h4bs= + + + + GCQVmBq+1H7e9IjvKfe+egLM1Jlp3L1JCGkl9SlJ0eaDh2MKYUUnHA== + + + + + MIIDUDCCAxCgAwIBAgIGAOz5IVHTMAkGByqGSM44BAMwdjELMAkGA1UEBhMCSUUx + DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll + cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEdMBsGA1UEAxMUQW5vdGhlciBUcmFu + c2llbnQgQ0EwHhcNMDIwNDAyMjM1OTUyWhcNMTIwNDAyMjI1OTQ2WjBoMQswCQYD + VQQGEwJJRTEPMA0GA1UECBMGRHVibGluMSQwIgYDVQQKExtCYWx0aW1vcmUgVGVj + aG5vbG9naWVzIEx0ZC4xETAPBgNVBAsTCFgvU2VjdXJlMQ8wDQYDVQQDEwZNb3Jp + Z3UwggG2MIIBKwYHKoZIzjgEATCCAR4CgYEAhIqwSieM0aAez+6H71gqCfBnwG3d + Pu7JAElb13GkwXRw9RfPh0VtIVjhDJbyKAIuzCk5r54ccRixa8TQ2vWVwodQ9eru + 7jUknAc2rVEAV5mJTrBr7UUve/X9PWsCDN6lXvFLiJp/Pi/F0cyV/CAp+jIp+rol + TXOhUz9qEjnFYMMCFQCYS/p4gmsHgo3R89EAE/Hc0dhyWQKBgCWG5hn8DM+1rv5s + TkJBqyZJXNzy4z974s3sAGURfBBGTpB9kFxfxNt480TxkWeDhR+39DMA5TEAsRPu + QoB6Tgl7K2nNzRdgJjK4YkBObgX1ljWkAnnJCZSbC8Nh2VpkniV0bM79HnsS+eCf + 8bi2qOOiLSzHeOrtzO8PB0YeeTLQA4GEAAKBgH1NBJ9Az5TwY4tDE0dPYVHHABt+ + yLspnT3k9G6YWUMFhZ/+3RuqEPjnKrPfUoXTTJGIACgPU3/PkqwrPVD0JMdpOcnZ + LHiJ/P7QRQeMwDRoBrs7genB1bDd4pSJrEUcjrkA5uRrIj2Z5fL+UuLiLGPO2rM7 + BNQRIq3QFPdX++NuozowODAOBgNVHQ8BAf8EBAMCB4AwEQYDVR0OBAoECIK7Ljjh + +EsfMBMGA1UdIwQMMAqACIocVjBaMhJ9MAkGByqGSM44BAMDLwAwLAIUEJJCOHw8 + ppxoRyz3s+Vmb4NKIfMCFDgJoZn9zh/3WoYNBURODwLvyBOy + + + + diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-is.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-is.xml new file mode 100644 index 0000000..b7a01f8 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-is.xml @@ -0,0 +1,24 @@ + + + + + + + + 60NvZvtdTB+7UnlLp/H24p7h4bs= + + + + bmKMy/w1DO9dHA6E7Dt0B8IFkYAj1/UD3TqcdqIcfkMT7evE8+NBgg== + + + + + + CN=Another Transient CA,OU=X/Secure,O=Baltimore Technologies Ltd.,ST=Dublin,C=IE + + 1017792003066 + + + + diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-ski.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-ski.xml new file mode 100644 index 0000000..c71bfce --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-ski.xml @@ -0,0 +1,21 @@ + + + + + + + + 60NvZvtdTB+7UnlLp/H24p7h4bs= + + + + F9nEU1Us48iKTml8n7E4wt7HtFJ5gaLIgox0J9WbujGndW0oQJbeGg== + + + + + hf10xKfSnIg= + + + + diff --git a/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-sn.xml b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-sn.xml new file mode 100644 index 0000000..d5b0808 --- /dev/null +++ b/tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-sn.xml @@ -0,0 +1,21 @@ + + + + + + + + 60NvZvtdTB+7UnlLp/H24p7h4bs= + + + + MUOjiqG0dbjvR6+qYYPL85nKSt2FeZGQBQkYudv48KyJhJLG1Bp+bA== + + + + + CN=Badb,OU=X/Secure,O=Baltimore Technologies Ltd.,ST=Dublin,C=IE + + + + diff --git a/tests/fixtures_smoke.rs b/tests/fixtures_smoke.rs index d336107..0549b45 100644 --- a/tests/fixtures_smoke.rs +++ b/tests/fixtures_smoke.rs @@ -175,8 +175,8 @@ fn fixture_file_count_matches_expected() { let mut count = 0; count_files_recursive(fixtures_dir(), &mut count); assert_eq!( - count, 69, - "expected 69 fixture files total (21 keys + 41 c14n + 7 donor xmldsig); \ + count, 77, + "expected 77 fixture files total (22 keys + 41 c14n + 14 donor xmldsig); \ if you added/removed files, update this count" ); }