diff --git a/README.md b/README.md index ee92fdb..231890d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Currently implemented (core paths): - C14N 1.0, C14N 1.1, and Exclusive C14N - XMLDSig parsing, same-document URI dereference, transform chains, and digest verification - XMLDSig full verify pipeline (`SignedInfo` canonicalization + `SignatureValue` verification) -- Built-in verification-key resolution from X.509, DER, `KeyName`, and RSA `KeyValue` sources +- Built-in verification-key resolution from X.509, DER, `KeyName`, RSA `KeyValue`, and EC `KeyValue` sources - RSA PKCS#1 v1.5 verification helpers for SHA-1 / SHA-256 / SHA-384 / SHA-512 - ECDSA verification helpers for P-256/SHA-256 and P-384/SHA-384 - Opt-in X.509 certificate-chain validation with explicit trust anchors, validity checks, CA constraints, and CRLs diff --git a/src/xmldsig/keys.rs b/src/xmldsig/keys.rs index 2faf45f..3a0f4d0 100644 --- a/src/xmldsig/keys.rs +++ b/src/xmldsig/keys.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, time::SystemTime}; -use rsa::pkcs8::EncodePublicKey; +use p256::pkcs8::EncodePublicKey; use x509_parser::{ prelude::{FromDer, X509Certificate}, public_key::PublicKey, @@ -11,8 +11,9 @@ use x509_parser::{ use super::{ DsigError, KeyInfo, KeyInfoSource, KeyResolver, KeyValueInfo, SignatureAlgorithm, VerifyingKey, - X509ChainOptions, X509DataInfo, verify_ecdsa_signature_spki, verify_rsa_signature_spki, - verify_x509_certificate_chain, + X509ChainOptions, X509DataInfo, + parse::{EC_P256_OID, EC_P384_OID}, + verify_ecdsa_signature_spki, verify_rsa_signature_spki, verify_x509_certificate_chain, }; /// A public verification key available to key resolvers. @@ -177,29 +178,34 @@ impl DefaultKeyResolver { key_value: &KeyValueInfo, algorithm: SignatureAlgorithm, ) -> Result, KeyResolutionError> { - let KeyValueInfo::Rsa { modulus, exponent } = key_value else { - return Ok(None); + let public_key_bytes = match key_value { + KeyValueInfo::Rsa { modulus, exponent } => { + if !matches!( + algorithm, + SignatureAlgorithm::RsaSha1 + | SignatureAlgorithm::RsaSha256 + | SignatureAlgorithm::RsaSha384 + | SignatureAlgorithm::RsaSha512 + ) { + return Err(KeyResolutionError::AlgorithmMismatch); + } + rsa_key_value_to_spki_der(modulus, exponent)? + } + KeyValueInfo::Ec { + curve_oid, + public_key, + } => { + if !matches!( + algorithm, + SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 + ) { + return Ok(None); + } + ec_key_value_to_spki_der(curve_oid, public_key)? + } + KeyValueInfo::InvalidEcKeyValue => return Err(KeyResolutionError::InvalidPublicKey), + KeyValueInfo::Unsupported { .. } => return Ok(None), }; - if !matches!( - algorithm, - SignatureAlgorithm::RsaSha1 - | SignatureAlgorithm::RsaSha256 - | SignatureAlgorithm::RsaSha384 - | SignatureAlgorithm::RsaSha512 - ) { - return Err(KeyResolutionError::AlgorithmMismatch); - } - - let key = rsa::RsaPublicKey::new( - rsa::BigUint::from_bytes_be(modulus), - rsa::BigUint::from_bytes_be(exponent), - ) - .map_err(|_| KeyResolutionError::InvalidPublicKey)?; - let public_key_bytes = key - .to_public_key_der() - .map_err(|_| KeyResolutionError::InvalidPublicKey)? - .as_bytes() - .to_vec(); validate_spki_algorithm(&public_key_bytes, algorithm)?; Ok(Some(VerificationKey { @@ -220,6 +226,7 @@ impl KeyResolver for DefaultKeyResolver { let Some(key_info) = key_info else { return Ok(None); }; + let mut deferred_key_value_error = None; for source in &key_info.sources { let resolved = match source { KeyInfoSource::X509Data(info) => self.resolve_x509(info, algorithm)?, @@ -245,13 +252,23 @@ impl KeyResolver for DefaultKeyResolver { }) .transpose()?, KeyInfoSource::KeyValue(key_value) => { - Self::resolve_key_value(key_value, algorithm)? + match Self::resolve_key_value(key_value, algorithm) { + Ok(resolved) => resolved, + Err(error) if ec_key_value_error_allows_fallback(key_value, &error) => { + deferred_key_value_error.get_or_insert(error); + None + } + Err(error) => return Err(error.into()), + } } }; if let Some(key) = resolved { return Ok(Some(Box::new(key))); } } + if let Some(error) = deferred_key_value_error { + return Err(error.into()); + } Ok(None) } @@ -260,6 +277,52 @@ impl KeyResolver for DefaultKeyResolver { } } +fn rsa_key_value_to_spki_der( + modulus: &[u8], + exponent: &[u8], +) -> Result, KeyResolutionError> { + let key = rsa::RsaPublicKey::new( + rsa::BigUint::from_bytes_be(modulus), + rsa::BigUint::from_bytes_be(exponent), + ) + .map_err(|_| KeyResolutionError::InvalidPublicKey)?; + key.to_public_key_der() + .map_err(|_| KeyResolutionError::InvalidPublicKey) + .map(|der| der.as_bytes().to_vec()) +} + +fn ec_key_value_to_spki_der( + curve_oid: &str, + public_key: &[u8], +) -> Result, KeyResolutionError> { + match curve_oid { + EC_P256_OID => p256::PublicKey::from_sec1_bytes(public_key) + .map_err(|_| KeyResolutionError::InvalidPublicKey)? + .to_public_key_der() + .map_err(|_| KeyResolutionError::InvalidPublicKey) + .map(|der| der.as_bytes().to_vec()), + EC_P384_OID => p384::PublicKey::from_sec1_bytes(public_key) + .map_err(|_| KeyResolutionError::InvalidPublicKey)? + .to_public_key_der() + .map_err(|_| KeyResolutionError::InvalidPublicKey) + .map(|der| der.as_bytes().to_vec()), + _ => Err(KeyResolutionError::InvalidPublicKey), + } +} + +fn ec_key_value_error_allows_fallback( + key_value: &KeyValueInfo, + error: &KeyResolutionError, +) -> bool { + matches!( + key_value, + KeyValueInfo::Ec { .. } | KeyValueInfo::InvalidEcKeyValue + ) && matches!( + error, + KeyResolutionError::InvalidPublicKey | KeyResolutionError::AlgorithmMismatch + ) +} + fn validate_spki_algorithm( public_key_bytes: &[u8], algorithm: SignatureAlgorithm, @@ -321,6 +384,12 @@ mod tests { const LEGACY_RSA_KEY_VALUE_SIGNATURE: &str = include_str!( "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-enveloping-rsa.xml" ); + const EC_P256_KEY_VALUE_SIGNATURE: &str = include_str!( + "../../tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p256_sha256.xml" + ); + const EC_P384_KEY_VALUE_SIGNATURE: &str = include_str!( + "../../tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p384_sha384.xml" + ); fn replace_key_info(xml: &str, replacement: &str) -> String { let start = xml.find("").expect("fixture has KeyInfo"); @@ -487,6 +556,278 @@ mod tests { )); } + #[test] + fn resolves_ec_p256_key_value_end_to_end() { + // XMLDSig 1.1 ECKeyValue must verify without a preset key or certificate. + let resolver = DefaultKeyResolver::default(); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(EC_P256_KEY_VALUE_SIGNATURE) + .expect("P-256 ECKeyValue should resolve"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn resolves_ec_p384_key_value_end_to_end() { + // The donor P-384 vector uses NamedCurve + uncompressed PublicKey. + let resolver = DefaultKeyResolver::default(); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(EC_P384_KEY_VALUE_SIGNATURE) + .expect("P-384 ECKeyValue should resolve"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn ec_key_value_ignored_for_rsa_signature_method() { + // Embedded EC key material must not be relabeled for an RSA SignatureMethod. + let key_info = r#"BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4="#; + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, key_info); + let resolver = DefaultKeyResolver::default(); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("single incompatible ECKeyValue should be ignored"); + + assert_eq!( + result.status, + super::super::DsigStatus::Invalid(super::super::FailureReason::KeyNotFound) + ); + } + + #[test] + fn incompatible_ec_key_value_falls_back_to_later_rsa_key_value() { + // Mixed KeyInfo should keep scanning after an incompatible ECKeyValue source. + let public_key = rsa::RsaPublicKey::from_public_key_pem(RSA_PUBLIC_KEY) + .expect("fixture must contain an RSA public key"); + let key_info = format!( + r#"BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4={}{}"#, + STANDARD.encode(public_key.n().to_bytes_be()), + STANDARD.encode(public_key.e().to_bytes_be()), + ); + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, &key_info); + let resolver = DefaultKeyResolver::default(); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later RSAKeyValue should resolve"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn unsupported_ec_key_value_falls_back_to_later_key_name() { + // Unsupported curves are non-fatal so a later compatible source can verify. + let key_info = r#"BA==idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn invalid_ec_key_value_falls_back_to_later_key_name() { + // Off-curve EC points are typed errors only if no later source can verify. + let key_info = r#"BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after invalid ECKeyValue"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn malformed_ec_key_value_falls_back_to_later_key_name() { + // Parse-level EC point errors remain non-fatal while later sources exist. + let key_info = r#"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after malformed ECKeyValue"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn invalid_base64_ec_key_value_falls_back_to_later_key_name() { + // A bad ECKeyValue payload is an unusable source, not a reason to skip + // later ordered KeyInfo sources that can verify the signature. + let key_info = r#"not base64!idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after bad ECKeyValue base64"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn missing_curve_uri_ec_key_value_falls_back_to_later_key_name() { + // Missing EC curve parameters make only this KeyValue source unusable. + let key_info = r#"BA==idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after missing EC curve URI"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn malformed_ec_key_value_children_fall_back_to_later_key_name() { + // An unusable EC source must not prevent later ordered KeyInfo sources + // from resolving, regardless of which required child-shape check fails. + let malformed_ec_key_values = [ + r#""#, + r#""#, + r#"BA==BA=="#, + ]; + + for malformed_children in malformed_ec_key_values { + let key_info = format!( + r#"{malformed_children}idp-signing"# + ); + let xml = replace_key_info(SIGNED_SAML, &key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after malformed EC child shape"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + } + + #[test] + fn mismatched_ec_curve_falls_back_to_later_key_name() { + // A valid P-384 key is unusable for an ECDSA-SHA256 signature but must not + // prevent a later P-256 KeyName from resolving the same document. + let key_info = r#"BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==idp-signing"#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + "idp-signing".into(), + VerificationKey { + algorithm: SignatureAlgorithm::EcdsaP256Sha256, + public_key_bytes: public_key_der(SAML_PUBLIC_KEY), + certificate_der: None, + name: Some("idp-signing".into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("later KeyName should resolve after mismatched ECKeyValue"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn lone_malformed_ec_key_value_reports_invalid_public_key() { + let key_info = r#"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let error = super::super::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&xml) + .expect_err("lone malformed ECKeyValue should surface typed key error"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::InvalidPublicKey) + )); + } + + #[test] + fn lone_mismatched_ec_curve_reports_algorithm_mismatch() { + let key_info = r#"BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA=="#; + let xml = replace_key_info(SIGNED_SAML, key_info); + let error = super::super::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&xml) + .expect_err("lone mismatched ECKeyValue should surface typed key error"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::AlgorithmMismatch) + )); + } + #[test] fn chain_verification_rejects_untrusted_embedded_certificate() { // Enabling chain policy must fail closed when no trust anchor is configured. diff --git a/src/xmldsig/parse.rs b/src/xmldsig/parse.rs index f433d9c..51f54e8 100644 --- a/src/xmldsig/parse.rs +++ b/src/xmldsig/parse.rs @@ -39,6 +39,9 @@ const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LE const MAX_KEY_NAME_TEXT_LEN: usize = 4096; const MAX_RSA_MODULUS_LEN: usize = 1024; const MAX_RSA_EXPONENT_LEN: usize = 8; +pub(crate) const EC_P256_OID: &str = "1.2.840.10045.3.1.7"; +pub(crate) const EC_P384_OID: &str = "1.3.132.0.34"; +const MAX_EC_PUBLIC_KEY_LEN: usize = 97; const MAX_X509_BASE64_TEXT_LEN: usize = 262_144; const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN; const MAX_X509_DECODED_BINARY_LEN: usize = MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3; @@ -167,8 +170,15 @@ pub enum KeyValueInfo { /// RSA public exponent. exponent: Vec, }, - /// `dsig11:ECKeyValue` (the XMLDSig 1.1 namespace form). - EcKeyValue, + /// `dsig11:ECKeyValue` with a supported named curve and SEC1 public point. + Ec { + /// Bare named-curve OID, without the XMLDSig `urn:oid:` prefix. + curve_oid: String, + /// Uncompressed SEC1 point (`0x04 || x || y`). + public_key: Vec, + }, + /// `dsig11:ECKeyValue` with unusable curve or point data. + InvalidEcKeyValue, /// Any other `` child not yet supported by this phase. Unsupported { /// Namespace URI of the unsupported child, when present. @@ -420,7 +430,7 @@ pub(crate) fn parse_reference(reference_node: Node) -> Result` -/// - `` (dispatch by child QName; only `dsig11:ECKeyValue` is treated as supported EC) +/// - `` (dispatch by child QName; RSA and `dsig11:ECKeyValue` are parsed) /// - `` /// - `` /// @@ -488,6 +498,26 @@ fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), Pars Ok(()) } +/// Verify that a node is a `` element. +fn verify_dsig11_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> { + if !node.is_element() { + return Err(ParseError::InvalidStructure(format!( + "expected element <{expected_name}>, got non-element node" + ))); + } + let tag = node.tag_name(); + if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG11_NS) { + return Err(ParseError::InvalidStructure(format!( + "expected , got <{}{}>", + tag.namespace() + .map(|ns| format!("{{{ns}}}")) + .unwrap_or_default(), + tag.name() + ))); + } + Ok(()) +} + /// Get the required `Algorithm` attribute from an element. fn required_algorithm_attr<'a>( node: Node<'a, 'a>, @@ -548,7 +578,7 @@ fn parse_key_value_dispatch(node: Node) -> Result { first_child.tag_name().name(), ) { (Some(XMLDSIG_NS), "RSAKeyValue") => parse_rsa_key_value(first_child), - (Some(XMLDSIG11_NS), "ECKeyValue") => Ok(KeyValueInfo::EcKeyValue), + (Some(XMLDSIG11_NS), "ECKeyValue") => parse_ec_key_value(first_child), (namespace, child_name) => Ok(KeyValueInfo::Unsupported { namespace: namespace.map(str::to_string), local_name: child_name.to_string(), @@ -556,6 +586,107 @@ fn parse_key_value_dispatch(node: Node) -> Result { } } +fn parse_ec_key_value(node: Node<'_, '_>) -> Result { + verify_dsig11_element(node, "ECKeyValue")?; + ensure_no_non_whitespace_text(node, "ECKeyValue")?; + + let mut children = element_children(node); + let Some(named_curve_node) = children.next() else { + return Ok(KeyValueInfo::InvalidEcKeyValue); + }; + if named_curve_node.tag_name().namespace() == Some(XMLDSIG11_NS) + && named_curve_node.tag_name().name() == "ECParameters" + { + return Ok(KeyValueInfo::Unsupported { + namespace: Some(XMLDSIG11_NS.to_string()), + local_name: "ECKeyValue".into(), + }); + } + if named_curve_node.tag_name().namespace() != Some(XMLDSIG11_NS) + || named_curve_node.tag_name().name() != "NamedCurve" + { + return Ok(KeyValueInfo::InvalidEcKeyValue); + } + ensure_no_element_children(named_curve_node, "NamedCurve")?; + ensure_no_non_whitespace_text(named_curve_node, "NamedCurve")?; + let Some((curve_oid, expected_public_key_len)) = + (match parse_ec_named_curve_oid(named_curve_node) { + Ok(curve) => curve, + Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue), + }) + else { + return Ok(KeyValueInfo::Unsupported { + namespace: Some(XMLDSIG11_NS.to_string()), + local_name: "ECKeyValue".into(), + }); + }; + + let Some(public_key_node) = children.next() else { + return Ok(KeyValueInfo::InvalidEcKeyValue); + }; + if public_key_node.tag_name().namespace() != Some(XMLDSIG11_NS) + || public_key_node.tag_name().name() != "PublicKey" + { + return Ok(KeyValueInfo::InvalidEcKeyValue); + } + ensure_no_element_children(public_key_node, "PublicKey")?; + if children.next().is_some() { + return Ok(KeyValueInfo::InvalidEcKeyValue); + } + + let public_key = match decode_crypto_binary(public_key_node, "PublicKey", MAX_EC_PUBLIC_KEY_LEN) + { + Ok(public_key) => public_key, + Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue), + }; + if validate_ec_public_key_point(&public_key, expected_public_key_len).is_err() { + return Ok(KeyValueInfo::InvalidEcKeyValue); + } + + Ok(KeyValueInfo::Ec { + curve_oid, + public_key, + }) +} + +fn parse_ec_named_curve_oid(node: Node<'_, '_>) -> Result, ParseError> { + let uri = node.attribute("URI").ok_or_else(|| { + ParseError::InvalidStructure("ECKeyValue NamedCurve must include URI attribute".into()) + })?; + let curve_oid = uri.strip_prefix("urn:oid:").unwrap_or(uri); + if curve_oid.is_empty() { + return Err(ParseError::InvalidStructure( + "ECKeyValue NamedCurve URI must not be empty".into(), + )); + } + let Some(public_key_len) = ec_public_key_len(curve_oid) else { + return Ok(None); + }; + Ok(Some((curve_oid.to_string(), public_key_len))) +} + +fn ec_public_key_len(curve_oid: &str) -> Option { + match curve_oid { + EC_P256_OID => Some(65), + EC_P384_OID => Some(97), + _ => None, + } +} + +fn validate_ec_public_key_point(public_key: &[u8], expected_len: usize) -> Result<(), ParseError> { + if public_key.len() != expected_len { + return Err(ParseError::InvalidStructure( + "ECKeyValue PublicKey length does not match NamedCurve".into(), + )); + } + if public_key.first().copied() != Some(0x04) { + return Err(ParseError::InvalidStructure( + "ECKeyValue PublicKey must be an uncompressed SEC1 point".into(), + )); + } + Ok(()) +} + fn parse_rsa_key_value(node: Node<'_, '_>) -> Result { verify_ds_element(node, "RSAKeyValue")?; ensure_no_non_whitespace_text(node, "RSAKeyValue")?; @@ -2214,10 +2345,154 @@ BA== #[test] fn parse_key_info_dispatches_dsig11_ec_keyvalue() { + let public_key = "BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4="; + let xml = r#" + + + + BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4= + + + "#; + let doc = Document::parse(xml).unwrap(); + let expected_public_key = base64::engine::general_purpose::STANDARD + .decode(public_key) + .expect("fixture EC point must be valid base64"); + + let key_info = parse_key_info(doc.root_element()).unwrap(); + assert_eq!( + key_info.sources, + vec![KeyInfoSource::KeyValue(KeyValueInfo::Ec { + curve_oid: "1.2.840.10045.3.1.7".into(), + public_key: expected_public_key, + })] + ); + } + + #[test] + fn parse_ec_key_value_accepts_bare_curve_oid() { + use base64::Engine; + + let encoded_public_key = "BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA=="; + let xml = r#" + + + + BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA== + + + "#; + let doc = Document::parse(xml).unwrap(); + let expected_public_key = base64::engine::general_purpose::STANDARD + .decode(encoded_public_key) + .unwrap(); + + let sources = parse_key_info(doc.root_element()).unwrap().sources; + + assert!(matches!( + &sources[0], + KeyInfoSource::KeyValue(KeyValueInfo::Ec { curve_oid, public_key }) + if curve_oid == EC_P384_OID && public_key == &expected_public_key + )); + } + + #[test] + fn parse_ec_key_value_marks_ec_parameters_as_unsupported() { + let xml = r#" + + + + BA== + + + "#; + let doc = Document::parse(xml).unwrap(); + + let key_info = parse_key_info(doc.root_element()).unwrap(); + assert_eq!( + key_info.sources, + vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported { + namespace: Some(XMLDSIG11_NS.to_string()), + local_name: "ECKeyValue".into(), + })] + ); + } + + #[test] + fn parse_ec_key_value_marks_unsupported_curve_as_unsupported() { + let xml = r#" + + + + BA== + + + "#; + let doc = Document::parse(xml).unwrap(); + + let key_info = parse_key_info(doc.root_element()).unwrap(); + assert_eq!( + key_info.sources, + vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported { + namespace: Some(XMLDSIG11_NS.to_string()), + local_name: "ECKeyValue".into(), + })] + ); + } + + #[test] + fn parse_ec_key_value_marks_missing_named_curve_uri_invalid() { + let xml = r#" + + + + BA== + + + "#; + let doc = Document::parse(xml).unwrap(); + + let key_info = parse_key_info(doc.root_element()).unwrap(); + assert_eq!( + key_info.sources, + vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)] + ); + } + + #[test] + fn parse_ec_key_value_marks_reordered_children_invalid() { + let xml = r#" + + + BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4= + + + + "#; + let doc = Document::parse(xml).unwrap(); + + let key_info = parse_key_info(doc.root_element()).unwrap(); + assert_eq!( + key_info.sources, + vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)] + ); + } + + #[test] + fn parse_ec_key_value_marks_non_uncompressed_point_invalid() { let xml = r#" - + + + Ap/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4= + "#; let doc = Document::parse(xml).unwrap(); @@ -2225,7 +2500,7 @@ BA== let key_info = parse_key_info(doc.root_element()).unwrap(); assert_eq!( key_info.sources, - vec![KeyInfoSource::KeyValue(KeyValueInfo::EcKeyValue)] + vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)] ); } diff --git a/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p256_sha256.xml b/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p256_sha256.xml new file mode 100644 index 0000000..0c59f95 --- /dev/null +++ b/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p256_sha256.xml @@ -0,0 +1 @@ +vIgv7JtPOh3hpedKK0rm8XHtYCSoBX4eEF0YwnB26Es=eYx4ImirtPG/eJLWgJHoMS30voH+tozerMftKbYz27vtYNgsHfAvV4M+oEkNgoibq5qnwsO2Z8nn+ndKxhVqFg==BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=up up and away diff --git a/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p384_sha384.xml b/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p384_sha384.xml new file mode 100644 index 0000000..c143ba6 --- /dev/null +++ b/tests/fixtures/xmldsig/xmldsig11-interop-2012/signature-enveloping-p384_sha384.xml @@ -0,0 +1 @@ +QQzWA5o7Aj0x+LglAnSMqaZlUTGiAiWd+wFQwZQixBTly7WkzpFrU3pyPLLOIlB762PoqaZujP1OeuUBaJ4XvBBmMcA1OdZAW+yYP0tllL6oIMcB8Hu11wZtONDDI1XQO0OAFqfVAQ8OEbzUAH15/zydSItqs14IzW9id1dMruVwHzZCSZ0X94e09vLyDrndBO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==up up and away diff --git a/tests/fixtures_smoke.rs b/tests/fixtures_smoke.rs index 0819261..40abc54 100644 --- a/tests/fixtures_smoke.rs +++ b/tests/fixtures_smoke.rs @@ -176,8 +176,8 @@ fn fixture_file_count_matches_expected() { let mut count = 0; count_files_recursive(fixtures_dir(), &mut count); assert_eq!( - count, 79, - "expected 79 fixture files total (23 keys + 41 c14n + 14 donor xmldsig + 1 saml); \ + count, 81, + "expected 81 fixture files total (23 keys + 41 c14n + 16 donor xmldsig + 1 saml); \ if you added/removed files, update this count" ); }