diff --git a/src/xmldsig/mod.rs b/src/xmldsig/mod.rs index 1524fe4..024f6b8 100644 --- a/src/xmldsig/mod.rs +++ b/src/xmldsig/mod.rs @@ -21,7 +21,8 @@ pub use parse::{ ParseError, Reference, SignatureAlgorithm, SignedInfo, find_signature_node, parse_signed_info, }; pub use signature::{ - SignatureVerificationError, verify_rsa_signature_pem, verify_rsa_signature_spki, + SignatureVerificationError, verify_ecdsa_signature_pem, verify_ecdsa_signature_spki, + verify_rsa_signature_pem, verify_rsa_signature_spki, }; pub use transforms::{Transform, execute_transforms, parse_transforms}; pub use types::{NodeSet, TransformData, TransformError}; diff --git a/src/xmldsig/signature.rs b/src/xmldsig/signature.rs index 80240e1..3e6d13e 100644 --- a/src/xmldsig/signature.rs +++ b/src/xmldsig/signature.rs @@ -1,20 +1,22 @@ -//! RSA signature verification helpers for XMLDSig. +//! Signature verification helpers for XMLDSig. //! -//! This module covers roadmap task P1-019: RSA PKCS#1 v1.5 verification for -//! `rsa-sha1`, `rsa-sha256`, `rsa-sha384`, and `rsa-sha512`. +//! This module currently covers roadmap task P1-019 (RSA PKCS#1 v1.5) and +//! P1-020 (ECDSA P-256/P-384) verification. //! //! Input public keys are accepted in SubjectPublicKeyInfo (SPKI) form because //! that is how the vendored PEM fixtures are stored. `ring` expects the inner -//! ASN.1 `RSAPublicKey` bytes, so we validate and unwrap SPKI first. +//! SPKI payload for both algorithm families: +//! - RSA: ASN.1 `RSAPublicKey` +//! - ECDSA: uncompressed SEC1 EC point bytes from the SPKI bit string use ring::signature; use x509_parser::prelude::FromDer; -use x509_parser::public_key::PublicKey; +use x509_parser::public_key::{ECPoint, PublicKey}; use x509_parser::x509::SubjectPublicKeyInfo; use super::parse::SignatureAlgorithm; -/// Errors while preparing or running RSA signature verification. +/// Errors while preparing or running XMLDSig signature verification. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SignatureVerificationError { @@ -36,9 +38,21 @@ pub enum SignatureVerificationError { label: String, }, - /// The provided DER bytes were not a valid SPKI-encoded RSA public key. - #[error("invalid RSA SubjectPublicKeyInfo DER")] + /// The provided DER bytes were not a valid SPKI-encoded public key. + #[error("invalid SubjectPublicKeyInfo DER")] InvalidKeyDer, + + /// The provided public key does not match the signature algorithm. + #[error("public key does not match signature algorithm: {uri}")] + KeyAlgorithmMismatch { + /// XMLDSig algorithm URI used for diagnostics. + uri: String, + }, + + /// The provided ECDSA signature bytes were neither XMLDSig fixed-width + /// nor ASN.1 DER encoded. + #[error("invalid ECDSA signature encoding")] + InvalidSignatureFormat, } /// Verify an RSA XMLDSig signature using a PEM-encoded SPKI public key. @@ -52,6 +66,41 @@ pub fn verify_rsa_signature_pem( signed_data: &[u8], signature_value: &[u8], ) -> Result { + let public_key_spki_der = parse_public_key_pem(public_key_pem)?; + verify_rsa_signature_spki( + algorithm, + &public_key_spki_der, + signed_data, + signature_value, + ) +} + +/// Verify an ECDSA XMLDSig signature using a PEM-encoded SPKI public key. +/// +/// The PEM must contain a `PUBLIC KEY` block. The signature value is expected +/// to use the XMLDSig fixed-width `r || s` format required by RFC 6931 / +/// XMLDSig 1.1, but ASN.1 DER-encoded ECDSA signatures are also accepted as an +/// interop fallback. Returns `Ok(false)` for signature mismatch and `Err` for +/// algorithm/key/signature-format preparation errors (including +/// `InvalidSignatureFormat` when the bytes are neither valid fixed-width +/// `r || s` nor valid ASN.1 DER ECDSA). +#[must_use = "discarding the verification result skips signature validation"] +pub fn verify_ecdsa_signature_pem( + algorithm: SignatureAlgorithm, + public_key_pem: &str, + signed_data: &[u8], + signature_value: &[u8], +) -> Result { + let public_key_spki_der = parse_public_key_pem(public_key_pem)?; + verify_ecdsa_signature_spki( + algorithm, + &public_key_spki_der, + signed_data, + signature_value, + ) +} + +fn parse_public_key_pem(public_key_pem: &str) -> Result, SignatureVerificationError> { let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes()) .map_err(|_| SignatureVerificationError::InvalidKeyPem)?; if !rest.iter().all(|byte| byte.is_ascii_whitespace()) { @@ -61,7 +110,7 @@ pub fn verify_rsa_signature_pem( return Err(SignatureVerificationError::InvalidKeyFormat { label: pem.label }); } - verify_rsa_signature_spki(algorithm, &pem.contents, signed_data, signature_value) + Ok(pem.contents) } /// Verify an RSA XMLDSig signature using DER-encoded SPKI public key bytes. @@ -99,6 +148,69 @@ pub fn verify_rsa_signature_spki( } } +/// Verify an ECDSA XMLDSig signature using DER-encoded SPKI public key bytes. +/// +/// The input must be an X.509 `SubjectPublicKeyInfo` wrapping an EC key. The +/// signature value may be either XMLDSig fixed-width `r || s` bytes or ASN.1 +/// DER-encoded ECDSA for interop compatibility. Returns `Ok(false)` for +/// signature mismatch and `Err` for algorithm/key/signature-format preparation +/// errors. +#[must_use = "discarding the verification result skips signature validation"] +pub fn verify_ecdsa_signature_spki( + algorithm: SignatureAlgorithm, + public_key_spki_der: &[u8], + signed_data: &[u8], + signature_value: &[u8], +) -> Result { + if !matches!( + algorithm, + SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 + ) { + return Err(SignatureVerificationError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }); + } + + let (rest, spki) = SubjectPublicKeyInfo::from_der(public_key_spki_der) + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + if !rest.is_empty() { + return Err(SignatureVerificationError::InvalidKeyDer); + } + let public_key = spki + .parsed() + .map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + + 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()) + } + } + } + _ => Err(SignatureVerificationError::KeyAlgorithmMismatch { + uri: algorithm.uri().to_string(), + }), + } +} + fn validate_rsa_public_key( rsa: &x509_parser::public_key::RSAPublicKey<'_>, algorithm: SignatureAlgorithm, @@ -162,6 +274,226 @@ fn verification_algorithm( } } +fn ecdsa_verification_algorithms( + spki: &SubjectPublicKeyInfo<'_>, + ec: &ECPoint<'_>, + algorithm: SignatureAlgorithm, + signature_value: &[u8], +) -> Result< + ( + &'static dyn signature::VerificationAlgorithm, + &'static dyn signature::VerificationAlgorithm, + EcdsaSignatureEncoding, + ), + SignatureVerificationError, +> { + let curve_oid = spki + .algorithm + .parameters + .as_ref() + .and_then(|params| params.as_oid().ok()) + .ok_or(SignatureVerificationError::InvalidKeyDer)?; + let point_len = ec.key_size(); + + let curve_oid = curve_oid.to_id_string(); + let (fixed_algorithm, asn1_algorithm, component_len) = 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, + ) + } else { + return 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, + ) + } else { + return Err(SignatureVerificationError::KeyAlgorithmMismatch { + uri: algorithm.uri().to_string(), + }); + } + } + _ => { + return Err(SignatureVerificationError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }); + } + }; + + Ok(( + fixed_algorithm, + asn1_algorithm, + classify_ecdsa_signature_encoding(signature_value, component_len)?, + )) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EcdsaSignatureEncoding { + XmlDsigFixed, + Asn1Der, + Ambiguous, +} + +fn classify_ecdsa_signature_encoding( + signature_value: &[u8], + component_len: usize, +) -> Result { + let expected_len = component_len + .checked_mul(2) + .ok_or(SignatureVerificationError::InvalidSignatureFormat)?; + + match inspect_der_encoded_ecdsa_signature(signature_value, component_len) { + Ok(Some(())) if signature_value.len() == expected_len => { + Ok(EcdsaSignatureEncoding::Ambiguous) + } + Ok(Some(())) => Ok(EcdsaSignatureEncoding::Asn1Der), + Ok(None) | Err(_) if signature_value.len() == expected_len => { + Ok(EcdsaSignatureEncoding::XmlDsigFixed) + } + Ok(None) | Err(_) => Err(SignatureVerificationError::InvalidSignatureFormat), + } +} + +fn inspect_der_encoded_ecdsa_signature( + signature_value: &[u8], + component_len: usize, +) -> Result, SignatureVerificationError> { + let Some((&tag, rest)) = signature_value.split_first() else { + return Ok(None); + }; + if tag != 0x30 { + return Ok(None); + } + + let sequence = parse_der_length(rest) + .ok_or(SignatureVerificationError::InvalidSignatureFormat)? + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + let (sequence_len, sequence_rest) = sequence; + let (sequence_content, trailing) = sequence_rest + .split_at_checked(sequence_len) + .ok_or(SignatureVerificationError::InvalidSignatureFormat)?; + if !trailing.is_empty() { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + + let after_r = parse_der_integer(sequence_content, component_len)?; + let after_s = parse_der_integer(after_r, component_len)?; + if !after_s.is_empty() { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + + Ok(Some(())) +} + +fn parse_der_integer( + input: &[u8], + component_len: usize, +) -> Result<&[u8], SignatureVerificationError> { + let Some((&tag, rest)) = input.split_first() else { + return Err(SignatureVerificationError::InvalidSignatureFormat); + }; + if tag != 0x02 { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + + let (len, rest) = parse_der_length(rest) + .ok_or(SignatureVerificationError::InvalidSignatureFormat)? + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + let (integer_bytes, remainder) = rest + .split_at_checked(len) + .ok_or(SignatureVerificationError::InvalidSignatureFormat)?; + + if integer_bytes.is_empty() { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + if integer_bytes.len() > component_len + 1 { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + if integer_bytes.len() == component_len + 1 && integer_bytes[0] != 0 { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + if integer_bytes[0] & 0x80 != 0 { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + if integer_bytes.len() > 1 && integer_bytes[0] == 0 && integer_bytes[1] & 0x80 == 0 { + return Err(SignatureVerificationError::InvalidSignatureFormat); + } + + Ok(remainder) +} + +fn parse_der_length(input: &[u8]) -> Option> { + let (&len_byte, rest) = input.split_first()?; + + if len_byte & 0x80 == 0 { + return Some(Ok((usize::from(len_byte), rest))); + } + + let len_len = usize::from(len_byte & 0x7f); + if len_len == 0 || len_len > std::mem::size_of::() || rest.len() < len_len { + return Some(Err(())); + } + + let (len_bytes, remainder) = rest.split_at(len_len); + if len_bytes[0] == 0 { + return Some(Err(())); + } + + let mut declared_len = 0_usize; + for &byte in len_bytes { + declared_len = match declared_len.checked_mul(256) { + Some(len) => len, + None => return Some(Err(())), + }; + declared_len = match declared_len.checked_add(usize::from(byte)) { + Some(len) => len, + None => return Some(Err(())), + }; + } + + if declared_len < 128 { + return Some(Err(())); + } + + Some(Ok((declared_len, remainder))) +} + +fn validate_ec_public_key_encoding( + ec: &ECPoint<'_>, + public_key_bytes: &[u8], +) -> Result<(), SignatureVerificationError> { + let coordinate_len = ec_coordinate_len_bytes(ec.key_size())?; + let expected_len = coordinate_len + .checked_mul(2) + .and_then(|len| len.checked_add(1)) + .ok_or(SignatureVerificationError::InvalidKeyDer)?; + + let is_uncompressed_sec1 = + public_key_bytes.len() == expected_len && public_key_bytes.first() == Some(&0x04); + if !is_uncompressed_sec1 { + return Err(SignatureVerificationError::InvalidKeyDer); + } + + Ok(()) +} + +fn ec_coordinate_len_bytes(key_bits: usize) -> Result { + key_bits + .checked_add(7) + .and_then(|bits| bits.checked_div(8)) + .ok_or(SignatureVerificationError::InvalidKeyDer) +} + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "unit tests use fixed fixture data")] mod tests { @@ -180,4 +512,61 @@ mod tests { )); } } + + #[test] + fn der_like_prefix_with_fixed_width_len_is_classified_as_raw() { + let mut signature = vec![0xAA_u8; 96]; + signature[0] = 0x30; + signature[1] = 0x20; + + let encoding = classify_ecdsa_signature_encoding(&signature, 48) + .expect("same-width signature with invalid DER must fall back to raw"); + assert_eq!(encoding, EcdsaSignatureEncoding::XmlDsigFixed); + } + + #[test] + fn overlong_der_length_below_128_is_rejected() { + let bad = [0x81_u8, 0x7f]; + let parsed = parse_der_length(&bad).expect("length bytes should be present"); + assert!( + matches!(parsed, Err(())), + "DER must reject long-form lengths below 128" + ); + } + + #[test] + fn ec_coordinate_length_rounds_up_for_non_byte_aligned_curves() { + assert_eq!( + ec_coordinate_len_bytes(521).expect("521-bit curves require rounded byte length"), + 66 + ); + } + + #[test] + fn same_width_valid_der_is_marked_ambiguous() { + let mut signature = Vec::with_capacity(64); + signature.extend_from_slice(&[0x30, 0x3e, 0x02, 0x1d]); + signature.extend(std::iter::repeat_n(0x11_u8, 29)); + signature.extend_from_slice(&[0x02, 0x1d]); + signature.extend(std::iter::repeat_n(0x22_u8, 29)); + + let encoding = classify_ecdsa_signature_encoding(&signature, 32) + .expect("same-width structurally valid DER should classify as ambiguous"); + assert_eq!(encoding, EcdsaSignatureEncoding::Ambiguous); + } + + #[test] + fn der_integer_longer_than_component_requires_sign_byte() { + let mut signature = Vec::with_capacity(72); + signature.extend_from_slice(&[0x30, 0x46, 0x02, 0x21, 0x01]); + signature.extend(std::iter::repeat_n(0x11_u8, 32)); + signature.extend_from_slice(&[0x02, 0x21, 0x01]); + signature.extend(std::iter::repeat_n(0x22_u8, 32)); + + let encoding = classify_ecdsa_signature_encoding(&signature, 32); + assert!(matches!( + encoding, + Err(SignatureVerificationError::InvalidSignatureFormat) + )); + } } diff --git a/tests/ecdsa_signature_integration.rs b/tests/ecdsa_signature_integration.rs new file mode 100644 index 0000000..58aec8b --- /dev/null +++ b/tests/ecdsa_signature_integration.rs @@ -0,0 +1,454 @@ +//! Integration tests for XMLDSig ECDSA signature verification. +//! +//! These tests validate roadmap task P1-020: canonicalized `` bytes +//! plus real donor EC public keys must verify against XMLDSig raw `r || s` +//! `SignatureValue` bytes for the declared `SignatureMethod`. + +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 xml_sec::c14n::canonicalize; +use xml_sec::xmldsig::parse::{SignatureAlgorithm, find_signature_node, parse_signed_info}; +use xml_sec::xmldsig::{ + SignatureVerificationError, verify_ecdsa_signature_pem, verify_ecdsa_signature_spki, +}; + +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 canonicalized_signed_info_and_signature(xml: &str) -> (SignatureAlgorithm, Vec, Vec) { + let doc = roxmltree::Document::parse(xml).expect("fixture XML should parse"); + let signature_node = find_signature_node(&doc).expect("fixture XML should contain Signature"); + let signed_info_node = signature_node + .children() + .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo") + .expect("fixture XML should contain SignedInfo"); + let signed_info = parse_signed_info(signed_info_node).expect("SignedInfo should parse"); + + let mut canonical = Vec::new(); + canonicalize( + &doc, + Some(&|node| { + node == signed_info_node + || node + .ancestors() + .any(|ancestor| ancestor == signed_info_node) + }), + &signed_info.c14n_method, + &mut canonical, + ) + .expect("SignedInfo canonicalization should succeed"); + + let signature_value_text = signature_node + .children() + .find(|node| node.is_element() && node.tag_name().name() == "SignatureValue") + .and_then(|node| node.text()) + .expect("fixture XML should contain SignatureValue text") + .chars() + .filter(|ch| !ch.is_whitespace()) + .collect::(); + let signature_value = base64::engine::general_purpose::STANDARD + .decode(signature_value_text) + .expect("SignatureValue should be valid base64"); + + (signed_info.signature_method, canonical, signature_value) +} + +fn assert_donor_signature_valid( + xml_path: &Path, + public_key_path: &Path, + expected_algorithm: SignatureAlgorithm, +) { + let xml = read_fixture(xml_path); + let public_key_pem = read_fixture(public_key_path); + let (algorithm, canonical_signed_info, signature_value) = + canonicalized_signed_info_and_signature(&xml); + + assert_eq!(algorithm, expected_algorithm, "unexpected SignatureMethod"); + + let valid = verify_ecdsa_signature_pem( + algorithm, + &public_key_pem, + &canonical_signed_info, + &signature_value, + ) + .expect("ECDSA verification should not error on valid fixtures"); + assert!(valid, "donor ECDSA signature should verify"); +} + +#[test] +fn donor_ecdsa_p256_signature_matches() { + assert_donor_signature_valid( + Path::new("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml"), + Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"), + SignatureAlgorithm::EcdsaP256Sha256, + ); +} + +#[test] +fn local_p384_signature_matches() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha384-ecdsa-sha384.xml", + )); + let private_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-key.pem")); + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem")); + let (signature_algorithm, canonical_signed_info, _) = + canonicalized_signed_info_and_signature(&xml); + assert_eq!( + SignatureAlgorithm::EcdsaP384Sha384, + signature_algorithm, + "fixture SignatureMethod should be EcdsaP384Sha384", + ); + + let pkcs8_der = x509_parser::pem::parse_x509_pem(private_key_pem.as_bytes()) + .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 valid = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP384Sha384, + &public_key_pem, + &canonical_signed_info, + signature.as_ref(), + ) + .expect("P-384 verification should not error on valid fixtures"); + + assert!(valid, "locally signed P-384 signature should verify"); +} + +#[test] +fn local_p384_der_signature_matches() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha384-ecdsa-sha384.xml", + )); + let private_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-key.pem")); + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem")); + let (signature_algorithm, canonical_signed_info, _) = + canonicalized_signed_info_and_signature(&xml); + assert_eq!( + SignatureAlgorithm::EcdsaP384Sha384, + signature_algorithm, + "fixture SignatureMethod should be EcdsaP384Sha384", + ); + + let pkcs8_der = x509_parser::pem::parse_x509_pem(private_key_pem.as_bytes()) + .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 valid = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP384Sha384, + &public_key_pem, + &canonical_signed_info, + signature.as_ref(), + ) + .expect("P-384 DER verification should not error on valid fixtures"); + + assert!(valid, "locally signed DER P-384 signature should verify"); +} + +#[test] +fn tampered_signed_info_fails_verification() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", + )); + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")); + let (algorithm, mut canonical_signed_info, signature_value) = + canonicalized_signed_info_and_signature(&xml); + + let last = canonical_signed_info + .last_mut() + .expect("canonical SignedInfo should not be empty"); + *last ^= 0x01; + + let valid = verify_ecdsa_signature_pem( + algorithm, + &public_key_pem, + &canonical_signed_info, + &signature_value, + ) + .expect("tampered data should still be a valid verification attempt"); + + assert!( + !valid, + "tampering SignedInfo bytes must break ECDSA signature verification" + ); +} + +#[test] +fn curve_mismatched_public_key_returns_typed_key_error() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", + )); + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem")); + let (algorithm, canonical_signed_info, signature_value) = + canonicalized_signed_info_and_signature(&xml); + + let err = verify_ecdsa_signature_pem( + algorithm, + &public_key_pem, + &canonical_signed_info, + &signature_value, + ) + .expect_err("curve-mismatched EC key should be rejected before verification"); + + assert!(matches!( + err, + SignatureVerificationError::KeyAlgorithmMismatch { .. } + )); +} + +#[test] +fn non_public_key_pem_returns_invalid_key_format() { + let err = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP256Sha256, + "-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----\n", + b"payload", + &[0_u8; 64], + ) + .expect_err("non-public-key PEM should be rejected"); + + assert!(matches!( + err, + SignatureVerificationError::InvalidKeyFormat { .. } + )); +} + +#[test] +fn malformed_pem_returns_typed_error() { + let err = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP256Sha256, + "-----BEGIN PUBLIC KEY-----\n%%%%\n-----END PUBLIC KEY-----\n", + b"payload", + &[0_u8; 64], + ) + .expect_err("corrupt PEM should be rejected"); + + assert!(matches!(err, SignatureVerificationError::InvalidKeyPem)); +} + +#[test] +fn pem_with_trailing_garbage_returns_typed_error() { + let public_key_pem = format!( + "{}TRAILING", + read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")) + ); + + let err = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP256Sha256, + &public_key_pem, + b"payload", + &[0_u8; 64], + ) + .expect_err("PEM with trailing garbage should be rejected"); + + assert!(matches!(err, SignatureVerificationError::InvalidKeyPem)); +} + +#[test] +fn malformed_spki_der_returns_typed_error() { + let err = verify_ecdsa_signature_spki( + SignatureAlgorithm::EcdsaP256Sha256, + &[0x01, 0x02, 0x03], + b"payload", + &[0_u8; 64], + ) + .expect_err("malformed SPKI DER should be rejected"); + + assert!( + matches!(err, SignatureVerificationError::InvalidKeyDer), + "expected InvalidKeyDer, got {err:?}" + ); +} + +#[test] +fn non_ec_spki_key_returns_algorithm_mismatch_error() { + let public_key_der = x509_parser::pem::parse_x509_pem( + read_fixture(Path::new("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem")).as_bytes(), + ) + .expect("fixture PEM should parse") + .1 + .contents; + + let err = verify_ecdsa_signature_spki( + SignatureAlgorithm::EcdsaP256Sha256, + &public_key_der, + b"payload", + &[0_u8; 64], + ) + .expect_err("non-EC SPKI key should be rejected by ECDSA verifier"); + + assert!(matches!( + err, + SignatureVerificationError::KeyAlgorithmMismatch { .. } + )); +} + +#[test] +fn non_ecdsa_algorithm_is_rejected_before_key_parsing() { + let rsa_public_key_der = x509_parser::pem::parse_x509_pem( + read_fixture(Path::new("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem")).as_bytes(), + ) + .expect("fixture PEM should parse") + .1 + .contents; + + for public_key_der in [&rsa_public_key_der[..], &[0x01_u8, 0x02, 0x03]] { + let err = verify_ecdsa_signature_spki( + SignatureAlgorithm::RsaSha256, + public_key_der, + b"payload", + &[0_u8; 64], + ) + .expect_err("non-ECDSA algorithm must be rejected before key parsing"); + + assert!(matches!( + err, + SignatureVerificationError::UnsupportedAlgorithm { .. } + )); + } +} + +#[test] +fn spki_der_valid_signature_matches() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", + )); + let (algorithm, canonical_signed_info, signature_value) = + canonicalized_signed_info_and_signature(&xml); + let public_key_der = x509_parser::pem::parse_x509_pem( + read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")).as_bytes(), + ) + .expect("fixture PEM should parse") + .1 + .contents; + + let valid = verify_ecdsa_signature_spki( + algorithm, + &public_key_der, + &canonical_signed_info, + &signature_value, + ) + .expect("SPKI verifier should accept valid fixture key and signature"); + + assert!(valid, "SPKI verifier should validate donor P-256 signature"); +} + +#[test] +fn spki_with_invalid_ec_point_prefix_returns_typed_error() { + let xml = read_fixture(Path::new( + "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", + )); + let (algorithm, canonical_signed_info, signature_value) = + canonicalized_signed_info_and_signature(&xml); + let mut public_key_der = x509_parser::pem::parse_x509_pem( + read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")).as_bytes(), + ) + .expect("fixture PEM should parse") + .1 + .contents; + + // For P-256 fixtures we expect BIT STRING header 03 42 00 followed by + // uncompressed SEC1 prefix 0x04; mutating it to 0x02 keeps SPKI parseable + // but should be rejected as invalid key encoding for ring. + let marker = [0x03_u8, 0x42, 0x00, 0x04]; + let marker_pos = public_key_der + .windows(marker.len()) + .position(|window| window == marker) + .expect("fixture SPKI should contain uncompressed EC point marker"); + public_key_der[marker_pos + 3] = 0x02; + + let err = verify_ecdsa_signature_spki( + algorithm, + &public_key_der, + &canonical_signed_info, + &signature_value, + ) + .expect_err("invalid EC point encoding should be rejected as key error"); + + assert!( + matches!(err, SignatureVerificationError::InvalidKeyDer), + "expected InvalidKeyDer, got {err:?}" + ); +} + +#[test] +fn signature_with_wrong_length_returns_typed_error() { + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")); + + let err = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP256Sha256, + &public_key_pem, + b"payload", + &[0_u8; 63], + ) + .expect_err("odd-sized XMLDSig ECDSA signature should be rejected"); + + assert!(matches!( + err, + SignatureVerificationError::InvalidSignatureFormat + )); +} + +#[test] +fn malformed_der_signature_with_non_raw_length_returns_typed_error() { + let public_key_pem = read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem")); + let malformed_der_signature = { + let mut signature = vec![0_u8; 95]; + signature[0] = 0x30; + signature[1] = 93; + signature + }; + + let err = verify_ecdsa_signature_pem( + SignatureAlgorithm::EcdsaP384Sha384, + &public_key_pem, + b"payload", + &malformed_der_signature, + ) + .expect_err("malformed DER-encoded signature should be rejected"); + + assert!(matches!( + err, + SignatureVerificationError::InvalidSignatureFormat + )); +} + +#[test] +fn spki_der_with_trailing_garbage_returns_typed_error() { + let mut public_key_der = x509_parser::pem::parse_x509_pem( + read_fixture(Path::new("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem")).as_bytes(), + ) + .expect("fixture PEM should parse") + .1 + .contents; + public_key_der.extend_from_slice(b"TRAILING"); + + let err = verify_ecdsa_signature_spki( + SignatureAlgorithm::EcdsaP256Sha256, + &public_key_der, + b"payload", + &[0_u8; 64], + ) + .expect_err("SPKI DER with trailing garbage should be rejected"); + + assert!(matches!(err, SignatureVerificationError::InvalidKeyDer)); +}