diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8859d9f1..f9ba90cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,7 +147,7 @@ jobs: env: PKCS11_MODULE: /usr/lib/softhsm/libsofthsm2.so run: | - for profile in dom-xmloxide dom-roxmltree c14n-xmloxide c14n-roxmltree dsig-xmloxide dsig-roxmltree cms-xmloxide dsig-sign-xmloxide xmlenc-xmloxide xmlenc-roxmltree xslt legacy experimental fips-dsig pkcs11 no-backend; do + for profile in dom-xmloxide dom-roxmltree c14n-xmloxide c14n-roxmltree dsig-xmloxide dsig-roxmltree cms-xmloxide dsig-sign-xmloxide xmlenc-xmloxide xmlenc-roxmltree xslt legacy experimental dstu4145 fips-dsig pkcs11 no-backend; do echo "::group::$profile" cargo run --quiet -p xml-sec-tools -- check-features "$profile" echo '::endgroup::' @@ -287,6 +287,14 @@ jobs: cargo clippy -p xml-sec --lib --no-default-features --features cms,xml-backend-xmloxide --target thumbv7em-none-eabihf -- -D warnings + - run: >- + cargo clippy -p xml-sec --lib --no-default-features + --features dstu4145,cms,xml-backend-xmloxide + --target thumbv7em-none-eabihf -- -D warnings + - run: >- + cargo check -p xml-sec --lib --no-default-features + --features dstu4145,cms,xml-backend-xmloxide + --target wasm32-unknown-unknown - run: >- cargo clippy -p xml-sec-xades --lib --no-default-features --features xml-backend-roxmltree diff --git a/Cargo.toml b/Cargo.toml index df295e75..7be463d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -308,6 +308,9 @@ winnow ={ version = "1.0.4", default-features = false, features = ["parser"], op snafu = { version = "0.9", default-features = false, features = ["rust_1_81"], optional = true } # Crypto +structured-dstu4145 = { version = "0.20.1", default-features = false, features = ["alloc"], optional = true } +# Holds the prepared GOST substitution tables that digest states borrow. +once_cell = { version = "1", default-features = false, features = ["alloc", "race"], optional = true } cryptoki = { version = "0.12.1", optional = true } aws-lc-rs = { version = "1.19", default-features = false, features = ["fips"], optional = true } rsa = { package = "sad-rsa", version = "0.10.2", default-features = false, features = ["sha1", "sha2", "hazmat"], optional = true } @@ -522,6 +525,8 @@ c14n = [] # XML Canonicalization (inclusive + exclusive) xslt = ["xmldsig", "std", "dep:icu_collator", "dep:icu_locale", "dep:roxmltree", "dep:time", "dep:url"] # Experimental libxmlsec1 algorithm identifiers; build capability is not policy permission. experimental-pq = ["xmldsig", "der/derive", "dep:slh-dsa", "dep:ml-kem", "dep:common", "dep:ctutils", "dep:hybrid-array", "dep:module-lattice", "dep:shake", "dep:const-oid", "signature/digest", "signature/alloc", "signature/rand_core"] +# DSTU 4145 verification with GOST 34.311 and DSTU 7564; build capability is not policy permission. +dstu4145 = ["xmldsig", "dep:structured-dstu4145", "dep:once_cell"] no-unsafe = [] embedded = [] diff --git a/README.md b/README.md index d5d903d1..b83b4cfc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Just `cargo add xml-sec`.** - **End-to-end XML security**: sign, verify, encrypt, and decrypt through public Rust APIs. - **Modern signature algorithms**: curve-independent ECDSA with SHA-2/SHA-3, Ed25519/Ed448 - with context/prehash variants, and opt-in experimental ML-DSA/SLH-DSA through `CryptoProvider`. + with context/prehash variants, opt-in experimental ML-DSA/SLH-DSA through `CryptoProvider`, + and opt-in verification of Ukrainian DSTU 4145 signatures. - **Experimental post-quantum key establishment**: ML-KEM-512/768/1024 through RustCrypto, RFC 9935 key encodings, and libxmlsec1-compatible HMAC/AES encapsulation workflows. Recipient encapsulation is not sender authentication; explicit policy permission is required. diff --git a/docs/features.md b/docs/features.md index 9d4904a0..c86d7ecd 100644 --- a/docs/features.md +++ b/docs/features.md @@ -17,6 +17,7 @@ See [provider boundaries](crypto-providers.md) and [policy profiles](policy-prof | `xslt` plus one XML backend | XMLDSig XSLT support; implies `xmldsig` and `std` | | `experimental-pq` plus one XML backend | Experimental signature verification and key establishment; implies `xmldsig`, not `xmlenc`; PQ signing keys also need `xmldsig-sign` | | `xmldsig,legacy-algorithms` plus one XML backend | Optional compatibility algorithms, still denied by secure policy defaults | +| `dstu4145` plus one XML backend | Verifies DSTU 4145 signatures with GOST 34.311 and DSTU 7564-256 digests, in XML and in X.509 chains; implies `xmldsig`; denied unless policy names the methods; no signing | | `aws-lc-fips` plus one XML backend | Adds the official FIPS provider; implies `xmldsig` and `std`, not encryption | | `pkcs11` plus one XML backend | Adds explicit external-key operations; implies `xmlenc` and `std` | | `--all-features` | All available providers and mechanisms coexist; runtime selection is still explicit | diff --git a/docs/xmldsig.md b/docs/xmldsig.md index 4f042e2b..03173f57 100644 --- a/docs/xmldsig.md +++ b/docs/xmldsig.md @@ -772,6 +772,18 @@ operation policy. PKCS#8 and SPKI use RFC 9881/RFC 9909 encodings. ML-DSA import seed-only, expanded-only, and consistent seed-plus-expanded keys; expanded imports are checked before signing. See [the maintained `RustCrypto` patch](ml-dsa-patch.md) for provenance, validation, and reproduction. These key primitives are dispatched through `CryptoProvider`. + +The optional `dstu4145` feature verifies DSTU 4145 signatures with the GOST 34.311 and +DSTU 7564-256 digests, in XML, in X.509 paths and in CMS `SignedData`. No registry assigns XML +identifiers to them, so the library follows the ones in the test signatures published by the +Central Certification Authority of Ukraine; the 384 and 512-bit DSTU 7564 methods have no +published example and stay unknown. These URIs have no parameter for a substitution table, so +GOST 34.311 means table number one and a key stating another is refused. Custom curves need +primality evidence that a certificate does not carry, so only named domains and explicit copies +of them are accepted. The feature grants no permission: digests, signature methods and, for +certificates and CMS, `certificate_signature_algorithms` must each be allowed by policy. These +methods only verify. + `HMACOutputLength` uses XML Schema integer syntax (including XML whitespace and an optional leading plus), not Unicode whitespace. `XMLDSig` 1.1 requires at least 80 bits and half the hash width; the default policy tightens that floor to at least 128 bits. Arbitrary diff --git a/src/policy.rs b/src/policy.rs index b2161e0b..b3648df4 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1286,10 +1286,12 @@ impl KeyTrustPolicy { Some(CertificateSignatureAlgorithms::Allowlist(allowed)) => { allowed.contains(&algorithm) } - None => !matches!( - algorithm, - crate::provider::X509SignatureAlgorithm::PostQuantum(_) - ), + None => match algorithm { + crate::provider::X509SignatureAlgorithm::PostQuantum(_) => false, + #[cfg(feature = "dstu4145")] + crate::provider::X509SignatureAlgorithm::Dstu4145 { .. } => false, + _ => true, + }, }; if !permitted { return Err(PolicyViolation::Algorithm { diff --git a/src/provider.rs b/src/provider.rs index 0e49d853..6583c75e 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -495,6 +495,14 @@ pub enum X509SignatureAlgorithm { Ed448, /// Pure ML-DSA or SLH-DSA with the PKIX-mandated empty context. PostQuantum(crate::xmldsig::PqAlgorithm), + /// DSTU 4145 with the selected digest; SPKI selects the curve. + #[cfg(feature = "dstu4145")] + Dstu4145 { + /// Message digest applied to the signed certificate data. + digest: DigestAlgorithm, + /// Whether the signature octets follow the big-endian profile. + big_endian: bool, + }, } #[cfg(feature = "xmldsig")] @@ -507,6 +515,12 @@ impl X509SignatureAlgorithm { Self::Dsa(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160) | Self::RsaPkcs1v15(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160) | Self::Ecdsa(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160) => return None, + #[cfg(feature = "dstu4145")] + Self::Dsa(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256) + | Self::RsaPkcs1v15(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256) + | Self::Ecdsa(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256) => { + return None; + } Self::Dsa(DigestAlgorithm::Sha1) => "1.2.840.10040.4.3", Self::Dsa(DigestAlgorithm::Sha224) => "2.16.840.1.101.3.4.3.1", Self::Dsa(DigestAlgorithm::Sha256) => "2.16.840.1.101.3.4.3.2", @@ -538,6 +552,9 @@ impl X509SignatureAlgorithm { Self::Ed25519 => "1.3.101.112", Self::Ed448 => "1.3.101.113", Self::PostQuantum(algorithm) => algorithm.oid(), + // The identifier also carries the field basis, which only SPKI fixes. + #[cfg(feature = "dstu4145")] + Self::Dstu4145 { .. } => return None, }) } } @@ -1489,6 +1506,10 @@ macro_rules! rust_crypto_digest { use sha2::Digest as _; match algorithm { $($(#[$attr])* DigestAlgorithm::$variant => Self::$variant(<$hasher>::new()),)+ + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => { + unreachable!("begin_digest sends these to the DSTU 4145 module") + } } } } @@ -2054,6 +2075,10 @@ impl CryptoProvider for RustCryptoProvider { &self, algorithm: DigestAlgorithm, ) -> Result, ProviderError> { + #[cfg(feature = "dstu4145")] + if crate::xmldsig::dstu4145::is_ukrainian_digest(algorithm) { + return crate::xmldsig::dstu4145::begin_digest(algorithm); + } Ok(Box::new(RustCryptoDigest::new(algorithm))) } @@ -2120,6 +2145,21 @@ impl CryptoProvider for RustCryptoProvider { signature: &[u8], issuer_spki_der: &[u8], ) -> Result { + // A refused key or framing is a non-matching signature. + #[cfg(feature = "dstu4145")] + if let X509SignatureAlgorithm::Dstu4145 { digest, big_endian } = algorithm + && crate::xmldsig::dstu4145::is_ukrainian_digest(digest) + { + return Ok(crate::xmldsig::dstu4145::verify_identified( + digest, + big_endian, + issuer_spki_der, + signed_data, + signature, + &crate::xmldsig::pkix::CertificateSession::standalone(), + ) + .unwrap_or(false)); + } rustcrypto_x509::verify_signature(algorithm, signed_data, signature, issuer_spki_der) } @@ -2224,6 +2264,10 @@ fn is_supported_x509_signature(algorithm: X509SignatureAlgorithm) -> bool { rustcrypto_x509::xml_signature_algorithm(algorithm).is_some() } X509SignatureAlgorithm::PostQuantum(_) => cfg!(feature = "experimental-pq"), + #[cfg(feature = "dstu4145")] + X509SignatureAlgorithm::Dstu4145 { digest, .. } => { + crate::xmldsig::dstu4145::is_ukrainian_digest(digest) + } X509SignatureAlgorithm::RsaPss { digest, mgf_digest, .. } => { @@ -2470,6 +2514,8 @@ pub(crate) mod rustcrypto_x509 { X509SignatureAlgorithm::Ecdsa(digest) => match digest { #[cfg(feature = "legacy-algorithms")] DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160 => None, + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => None, DigestAlgorithm::Sha1 => Some(SignatureAlgorithm::EcdsaSha1), DigestAlgorithm::Sha224 => Some(SignatureAlgorithm::EcdsaSha224), DigestAlgorithm::Sha256 => Some(SignatureAlgorithm::EcdsaSha256), diff --git a/src/provider/aws_lc.rs b/src/provider/aws_lc.rs index 6c119977..63fe8c59 100644 --- a/src/provider/aws_lc.rs +++ b/src/provider/aws_lc.rs @@ -52,6 +52,8 @@ fn digest_algorithm(algorithm: DigestAlgorithm) -> Option<&'static digest::Algor DigestAlgorithm::Sha3_384 => &digest::SHA3_384, DigestAlgorithm::Sha3_512 => &digest::SHA3_512, DigestAlgorithm::Sha3_224 => return None, + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => return None, }) } diff --git a/src/provider/key_establishment/bit_hash.rs b/src/provider/key_establishment/bit_hash.rs index aad5a342..6379fba2 100644 --- a/src/provider/key_establishment/bit_hash.rs +++ b/src/provider/key_establishment/bit_hash.rs @@ -96,6 +96,11 @@ pub(super) fn hash( total_bits, algorithm.output_len(), ), + // The KDF names its digest in the document; these have no bit framing. + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => Err( + ProviderError::InvalidInput(ProviderInputError::ConcatKdfParameters), + ), } } diff --git a/src/provider/rsa_pss.rs b/src/provider/rsa_pss.rs index d7085053..30addf8c 100644 --- a/src/provider/rsa_pss.rs +++ b/src/provider/rsa_pss.rs @@ -47,6 +47,12 @@ fn hash(algorithm: DigestAlgorithm, parts: &[&[u8]], output: &mut [u8; 64]) { DigestAlgorithm::Md5 => compute!(md5::Md5), #[cfg(feature = "legacy-algorithms")] DigestAlgorithm::Ripemd160 => compute!(ripemd::Ripemd160), + // Parameterized PSS names its hashes in the document. + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => { + let result = crate::xmldsig::dstu4145::digest_parts(algorithm, parts); + output[..result.len()].copy_from_slice(&result); + } } } diff --git a/src/xmldsig/cms.rs b/src/xmldsig/cms.rs index 1033ef23..5b85b782 100644 --- a/src/xmldsig/cms.rs +++ b/src/xmldsig/cms.rs @@ -257,6 +257,10 @@ impl CmsProvider<'_> { // https://www.rfc-editor.org/rfc/rfc8419#section-3 X509SignatureAlgorithm::Ed25519 => digest == DigestAlgorithm::Sha512, X509SignatureAlgorithm::Ed448 => false, + // The identifier names the hash that the signature covers, so + // digestAlgorithm can only repeat it, as for ECDSA. + #[cfg(feature = "dstu4145")] + X509SignatureAlgorithm::Dstu4145 { digest: hash, .. } => hash == digest, _ => true, }; Ok(if consistent { diff --git a/src/xmldsig/digest.rs b/src/xmldsig/digest.rs index 9c162253..e56809e5 100644 --- a/src/xmldsig/digest.rs +++ b/src/xmldsig/digest.rs @@ -39,15 +39,24 @@ pub enum DigestAlgorithm { Sha3_384, /// SHA3-512 (512-bit). Sha3_512, + /// GOST 34.311-95 with the Ukrainian substitution table number one, the + /// only one its URI can name; operation policy must explicitly permit it. + #[cfg(feature = "dstu4145")] + Gost34311, + /// DSTU 7564 (Kupyna) with 256-bit output; operation policy must explicitly + /// permit it. + #[cfg(feature = "dstu4145")] + Dstu7564_256, } impl DigestAlgorithm { /// Every compiled digest capability; this registry grants no policy permission. - pub const ALL: [Self; 9 + if cfg!(feature = "legacy-algorithms") { - 2 - } else { - 0 - }] = [ + pub const ALL: [Self; + 9 + if cfg!(feature = "legacy-algorithms") { + 2 + } else { + 0 + } + if cfg!(feature = "dstu4145") { 2 } else { 0 }] = [ #[cfg(feature = "legacy-algorithms")] Self::Md5, #[cfg(feature = "legacy-algorithms")] @@ -61,6 +70,10 @@ impl DigestAlgorithm { Self::Sha3_256, Self::Sha3_384, Self::Sha3_512, + #[cfg(feature = "dstu4145")] + Self::Gost34311, + #[cfg(feature = "dstu4145")] + Self::Dstu7564_256, ]; /// Parse a digest algorithm from its XML namespace URI. /// @@ -97,6 +110,13 @@ impl DigestAlgorithm { "http://www.w3.org/2007/05/xmldsig-more#sha3-256" => Some(Self::Sha3_256), "http://www.w3.org/2007/05/xmldsig-more#sha3-384" => Some(Self::Sha3_384), "http://www.w3.org/2007/05/xmldsig-more#sha3-512" => Some(Self::Sha3_512), + // No registry assigns these. They are the identifiers of the test + // signatures the Central Certification Authority of Ukraine publishes: + // https://czo.gov.ua/testexamples + #[cfg(feature = "dstu4145")] + "http://www.w3.org/2001/04/xmlenc#gost34311" => Some(Self::Gost34311), + #[cfg(feature = "dstu4145")] + "http://www.w3.org/2001/04/xmlenc#dstu7564-256" => Some(Self::Dstu7564_256), _ => None, } } @@ -118,6 +138,10 @@ impl DigestAlgorithm { Self::Sha3_256 => "http://www.w3.org/2007/05/xmldsig-more#sha3-256", Self::Sha3_384 => "http://www.w3.org/2007/05/xmldsig-more#sha3-384", Self::Sha3_512 => "http://www.w3.org/2007/05/xmldsig-more#sha3-512", + #[cfg(feature = "dstu4145")] + Self::Gost34311 => "http://www.w3.org/2001/04/xmlenc#gost34311", + #[cfg(feature = "dstu4145")] + Self::Dstu7564_256 => "http://www.w3.org/2001/04/xmlenc#dstu7564-256", } } @@ -136,6 +160,8 @@ impl DigestAlgorithm { match self { #[cfg(feature = "legacy-algorithms")] Self::Md5 | Self::Ripemd160 => true, + #[cfg(feature = "dstu4145")] + Self::Gost34311 | Self::Dstu7564_256 => true, _ => false, } } @@ -153,6 +179,8 @@ impl DigestAlgorithm { Self::Sha256 | Self::Sha3_256 => 32, Self::Sha384 | Self::Sha3_384 => 48, Self::Sha512 | Self::Sha3_512 => 64, + #[cfg(feature = "dstu4145")] + Self::Gost34311 | Self::Dstu7564_256 => 32, } } } diff --git a/src/xmldsig/dstu4145.rs b/src/xmldsig/dstu4145.rs new file mode 100644 index 00000000..6e55b0c4 --- /dev/null +++ b/src/xmldsig/dstu4145.rs @@ -0,0 +1,199 @@ +//! DSTU 4145 signature verification and the Ukrainian digests, over the +//! published `structured-dstu4145` arithmetic. + +use super::{DigestAlgorithm, SignatureAlgorithm, SignatureVerificationError}; +use crate::provider::{DigestState, ProviderError}; +use alloc::{boxed::Box, vec::Vec}; +use once_cell::race::OnceBox; +use structured_dstu4145::{ + accounted::DkeBinding, + domain::{Domain, Explicit, PublicKey}, + gost::{DEFAULT_DKE, Dke}, + hash::{Hash, State}, +}; +use structured_pkix::algorithm::dstu4145::{Curve, ExplicitCurve, Parameters}; + +static TABLE: OnceBox = OnceBox::new(); + +/// Substitution table number one: the digest URI has no parameter to name +/// another, so a key or Reference with a different table is not representable. +fn default_dke() -> &'static Dke { + TABLE.get_or_init(|| Box::new(Dke::new(&DEFAULT_DKE).expect("the published table is valid"))) +} + +fn hash(algorithm: DigestAlgorithm) -> Option> { + match algorithm { + DigestAlgorithm::Gost34311 => Some(Hash::Gost34311(default_dke())), + DigestAlgorithm::Dstu7564_256 => Some(Hash::Kupyna(32)), + _ => None, + } +} + +struct UkrainianDigest(State<'static>); + +impl DigestState for UkrainianDigest { + fn update(&mut self, data: &[u8]) -> Result<(), ProviderError> { + self.0.update(data).map_err(|_| digest_refusal()) + } + + fn finalize(self: Box) -> Result, ProviderError> { + Ok(self.0.finalize().as_ref().to_vec()) + } +} + +fn digest_refusal() -> ProviderError { + ProviderError::Unsupported { + operation: crate::provider::ProviderOperation::Digest, + algorithm: None, + } +} + +/// One-shot digest of `parts`. Fixed outputs and an input bounded by the +/// addressable range leave no failure for the two known identities. +pub(crate) fn digest_parts(algorithm: DigestAlgorithm, parts: &[&[u8]]) -> Vec { + let identity = hash(algorithm).expect("a Ukrainian digest"); + let mut state = State::new(identity).expect("a fixed output width"); + for part in parts { + state.update(part).expect("input within u64"); + } + state.finalize().as_ref().to_vec() +} + +/// Whether `algorithm` is one of the digests this module executes. +pub(crate) const fn is_ukrainian_digest(algorithm: DigestAlgorithm) -> bool { + matches!( + algorithm, + DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 + ) +} + +/// Start the incremental digest of a Ukrainian `algorithm`. +pub(crate) fn begin_digest( + algorithm: DigestAlgorithm, +) -> Result, ProviderError> { + let identity = hash(algorithm).ok_or_else(digest_refusal)?; + let state = State::new(identity).map_err(|_| digest_refusal())?; + Ok(Box::new(UkrainianDigest(state))) +} + +/// The numeric domain written in `curve`, for the crate's own validation. +fn explicit_input<'a>( + curve: &ExplicitCurve<'a>, + byte_order: structured_dstu4145::ByteOrder, +) -> Option> { + let a = match curve.a().value().as_u64() { + Some(0) => false, + Some(1) => true, + _ => return None, + }; + Some(Explicit { + degree: curve.degree_value(), + reduction: curve.reduction(), + a, + b: curve.b().as_bytes(), + order: curve.order().value().as_bytes(), + base_point: curve.base_point().as_bytes(), + byte_order, + }) +} + +/// The domain named or written in `parameters`. A custom domain needs primality +/// evidence that a certificate does not carry, so it stays unsupported. +fn domain( + parameters: &Parameters<'_>, + profile: structured_pkix::algorithm::dstu4145::Profile, +) -> Option { + match parameters.curve() { + Curve::Named(named) => { + let (basis, degree) = named.known()?; + (basis == profile.basis()) + .then(|| Domain::named(basis, degree as usize).ok()) + .flatten() + } + Curve::Explicit(curve) => { + Domain::explicit(explicit_input(&curve, profile.byte_order())?).ok() + } + } +} + +/// A validated public key of `der` and its signature byte order. +fn public_key( + der: &[u8], + session: &super::pkix::CertificateSession, +) -> Result<(PublicKey, structured_dstu4145::ByteOrder), SignatureVerificationError> { + session + .with_public_key_info(der, |info, execution| { + let key = structured_pkix::key::dstu4145::read(info, execution)?; + Ok(prepared(&key, execution)) + }) + .map_err(|error| super::signature::key_read_error(session, &error))? + .ok_or(SignatureVerificationError::InvalidKeyDer) +} + +fn prepared( + key: &structured_pkix::key::dstu4145::PublicKey<'_>, + execution: &structured_asn1::memory::Execution<'_>, +) -> Option<(PublicKey, structured_dstu4145::ByteOrder)> { + let profile = key.profile(); + let parameters = key.parameters()?; + // A key under another table signs digests no supported URI can identify. + DkeBinding::new(parameters, default_dke(), execution).ok()?; + let byte_order = profile.byte_order(); + let public = domain(¶meters, profile)? + .public_key(key.point().as_bytes(), byte_order) + .ok()?; + Some((public, byte_order)) +} + +/// Verify `signature`, the bare signature octets, for an ASN.1 carrier: there +/// the identifier, not the key, states the byte order. `digest` must satisfy +/// [`is_ukrainian_digest`]: the public identifier can pair the method with any +/// digest, so the provider refuses the others before calling this. +pub(crate) fn verify_identified( + digest: DigestAlgorithm, + big_endian: bool, + issuer_spki: &[u8], + signed_data: &[u8], + signature: &[u8], + session: &super::pkix::CertificateSession, +) -> Result { + let (key, _) = public_key(issuer_spki, session)?; + let byte_order = if big_endian { + structured_dstu4145::ByteOrder::BigEndian + } else { + structured_dstu4145::ByteOrder::LittleEndian + }; + key.verify_prehash(&digest_parts(digest, &[signed_data]), signature, byte_order) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat) +} + +/// Check that `der` is a usable DSTU 4145 key. +pub(crate) fn validate_public_key( + der: &[u8], + session: &super::pkix::CertificateSession, +) -> Result<(), SignatureVerificationError> { + public_key(der, session).map(|_| ()) +} + +/// Verify `signature` over `message`: hash it with the method's digest, then +/// hand the digest octets unchanged to the verifier. +pub(crate) fn verify( + algorithm: SignatureAlgorithm, + der: &[u8], + message: &[u8], + signature: &[u8], + session: &super::pkix::CertificateSession, +) -> Result { + let digest = algorithm.dstu4145_digest().and_then(hash).ok_or_else(|| { + SignatureVerificationError::UnsupportedAlgorithm { + uri: alloc::borrow::ToOwned::to_owned(algorithm.uri()), + } + })?; + let (key, byte_order) = public_key(der, session)?; + let mut state = State::new(digest).map_err(|_| SignatureVerificationError::InvalidKeyDer)?; + state + .update(message) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat)?; + key.verify_prehash(state.finalize().as_ref(), signature, byte_order) + .map_err(|_| SignatureVerificationError::InvalidSignatureFormat) +} diff --git a/src/xmldsig/keys.rs b/src/xmldsig/keys.rs index a0ed83d6..60df234d 100644 --- a/src/xmldsig/keys.rs +++ b/src/xmldsig/keys.rs @@ -298,6 +298,10 @@ impl VerificationKey { session, ) } + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Gost34311 | SignatureAlgorithm::Dstu4145Dstu7564_256 => { + super::dstu4145::validate_public_key(&self.public_key_bytes, session) + } SignatureAlgorithm::RsaSha1 | SignatureAlgorithm::RsaSha224 | SignatureAlgorithm::RsaSha256 @@ -373,7 +377,12 @@ impl VerificationKey { return Err(KeyResolutionError::AlgorithmMismatch.into()); } let key = &self.public_key_bytes; - let result = if algorithm.eddsa_signature_len().is_some() + #[cfg(feature = "dstu4145")] + let dstu4145 = algorithm.dstu4145_digest().is_some(); + #[cfg(not(feature = "dstu4145"))] + let dstu4145 = false; + let result = if dstu4145 + || algorithm.eddsa_signature_len().is_some() || matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) { super::modern::verify_with_context( @@ -2242,6 +2251,11 @@ fn validate_spki_algorithm( return super::modern::validate_public_key(algorithm, public_key_bytes, session) .map_err(spki_check_error); } + #[cfg(feature = "dstu4145")] + if algorithm.dstu4145_digest().is_some() { + return super::dstu4145::validate_public_key(public_key_bytes, session) + .map_err(spki_check_error); + } let family = session .spki_algorithm(public_key_bytes) .map_err(|error| structured_key_error(session, error))?; diff --git a/src/xmldsig/mod.rs b/src/xmldsig/mod.rs index 1a0c4e84..81b62ea8 100644 --- a/src/xmldsig/mod.rs +++ b/src/xmldsig/mod.rs @@ -62,6 +62,8 @@ pub mod builder; #[cfg(feature = "cms")] pub mod cms; pub mod digest; +#[cfg(feature = "dstu4145")] +pub(crate) mod dstu4145; pub mod extension; pub mod keys; pub(crate) mod modern; diff --git a/src/xmldsig/modern.rs b/src/xmldsig/modern.rs index 6619a21a..d283ae02 100644 --- a/src/xmldsig/modern.rs +++ b/src/xmldsig/modern.rs @@ -280,6 +280,12 @@ pub(crate) fn verify_with_context( } } match algorithm { + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Gost34311 | SignatureAlgorithm::Dstu4145Dstu7564_256 + if context.as_bytes().is_empty() => + { + super::dstu4145::verify(algorithm, der, message, signature, session) + } SignatureAlgorithm::Ed25519 | SignatureAlgorithm::Ed25519Ctx | SignatureAlgorithm::Ed25519Ph => { diff --git a/src/xmldsig/parse.rs b/src/xmldsig/parse.rs index b7d0a117..077cd99f 100644 --- a/src/xmldsig/parse.rs +++ b/src/xmldsig/parse.rs @@ -236,6 +236,14 @@ pub enum SignatureAlgorithm { Ed448Ph, /// Experimental post-quantum method; compile capability does not grant permission. PostQuantum(super::PqAlgorithm), + /// DSTU 4145 over GOST 34.311; the key selects curve and byte order. + /// Verification only, and operation policy must explicitly permit it. + #[cfg(feature = "dstu4145")] + Dstu4145Gost34311, + /// DSTU 4145 over DSTU 7564-256; the key selects curve and byte order. + /// Verification only, and operation policy must explicitly permit it. + #[cfg(feature = "dstu4145")] + Dstu4145Dstu7564_256, } impl SignatureAlgorithm { @@ -282,7 +290,7 @@ impl SignatureAlgorithm { 5 } else { 0 - }] = [ + } + if cfg!(feature = "dstu4145") { 2 } else { 0 }] = [ #[cfg(feature = "legacy-algorithms")] Self::RsaMd5, #[cfg(feature = "legacy-algorithms")] @@ -338,6 +346,10 @@ impl SignatureAlgorithm { Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_192s), Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_256f), Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_256s), + #[cfg(feature = "dstu4145")] + Self::Dstu4145Gost34311, + #[cfg(feature = "dstu4145")] + Self::Dstu4145Dstu7564_256, ]; /// Digest selected by an ECDSA method, independently of its key's curve. @@ -478,6 +490,17 @@ impl SignatureAlgorithm { "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519ph" => Some(Self::Ed25519Ph), "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448" => Some(Self::Ed448), "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448ph" => Some(Self::Ed448Ph), + // No registry assigns these. They are the identifiers of the test + // signatures the Central Certification Authority of Ukraine publishes: + // https://czo.gov.ua/testexamples + #[cfg(feature = "dstu4145")] + "http://www.w3.org/2001/04/xmldsig-more#dstu4145-gost34311" => { + Some(Self::Dstu4145Gost34311) + } + #[cfg(feature = "dstu4145")] + "http://www.w3.org/2001/04/xmldsig-more#dstu4145-dstu7564-256" => { + Some(Self::Dstu4145Dstu7564_256) + } _ => super::PqAlgorithm::ALL .into_iter() .find(|algorithm| algorithm.uri() == uri) @@ -536,6 +559,12 @@ impl SignatureAlgorithm { Self::Ed448 => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448", Self::Ed448Ph => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448ph", Self::PostQuantum(algorithm) => algorithm.uri(), + #[cfg(feature = "dstu4145")] + Self::Dstu4145Gost34311 => "http://www.w3.org/2001/04/xmldsig-more#dstu4145-gost34311", + #[cfg(feature = "dstu4145")] + Self::Dstu4145Dstu7564_256 => { + "http://www.w3.org/2001/04/xmldsig-more#dstu4145-dstu7564-256" + } } } @@ -561,10 +590,23 @@ impl SignatureAlgorithm { | Self::HmacMd5 | Self::HmacRipemd160 | Self::EcdsaRipemd160 => true, + #[cfg(feature = "dstu4145")] + Self::Dstu4145Gost34311 | Self::Dstu4145Dstu7564_256 => true, _ => false, } } + /// Digest selected by a DSTU 4145 method, independently of its key's curve. + #[cfg(feature = "dstu4145")] + #[must_use] + pub(crate) const fn dstu4145_digest(self) -> Option { + match self { + Self::Dstu4145Gost34311 => Some(DigestAlgorithm::Gost34311), + Self::Dstu4145Dstu7564_256 => Some(DigestAlgorithm::Dstu7564_256), + _ => None, + } + } + /// Whether this method uses an RSA key and PKCS#1 signature encoding. #[must_use] pub const fn is_rsa(self) -> bool { diff --git a/src/xmldsig/pkix.rs b/src/xmldsig/pkix.rs index 1deb8775..37e8580c 100644 --- a/src/xmldsig/pkix.rs +++ b/src/xmldsig/pkix.rs @@ -1149,6 +1149,29 @@ impl ProviderVerifier<'_> { // https://www.rfc-editor.org/rfc/rfc5280.html#section-6.1.4 return Ok(structured_pkix::path::Verified::Unsupported); } + // X.509 keeps the DSTU 4145 value in an OCTET STRING inside the BIT + // STRING; CMS carries the same octets directly in SignerInfo. + #[cfg(feature = "dstu4145")] + let signature = if matches!( + algorithm, + crate::provider::X509SignatureAlgorithm::Dstu4145 { .. } + ) { + use structured_asn1::{Tag, universal}; + let mut reader = original_signature + .reader_in(execution) + .map_err(X509ChainError::Processing)?; + match reader.next_element() { + Ok(Some(element)) + if element.tag() == Tag::primitive(universal::OCTET_STRING) + && reader.is_empty() => + { + element.contents() + } + _ => return Ok(structured_pkix::path::Verified::No), + } + } else { + signature + }; let verify = |spki: &[u8]| { check(spki)?; self.provider @@ -1182,6 +1205,10 @@ pub(crate) fn digest_algorithm( Hash::Sha3_256 => super::DigestAlgorithm::Sha3_256, Hash::Sha3_384 => super::DigestAlgorithm::Sha3_384, Hash::Sha3_512 => super::DigestAlgorithm::Sha3_512, + #[cfg(feature = "dstu4145")] + Hash::Gost34311 => super::DigestAlgorithm::Gost34311, + #[cfg(feature = "dstu4145")] + Hash::Kupyna256 => super::DigestAlgorithm::Dstu7564_256, _ => return None, }) } @@ -1231,6 +1258,19 @@ pub(crate) fn known_signature_algorithm( Some(Algorithm::Ecdsa(value)) => digest_algorithm(value).map(Signature::Ecdsa), Some(Algorithm::Ed25519) => Some(Signature::Ed25519), Some(Algorithm::Ed448) => Some(Signature::Ed448), + #[cfg(feature = "dstu4145")] + Some(Algorithm::Dstu4145(profile)) => { + use structured_pkix::algorithm::{digest::Algorithm as Hash, dstu4145::ByteOrder}; + let digest = match profile.digest() { + Hash::Gost34311 => Some(super::DigestAlgorithm::Gost34311), + Hash::Kupyna256 => Some(super::DigestAlgorithm::Dstu7564_256), + _ => None, + }; + digest.map(|digest| Signature::Dstu4145 { + digest, + big_endian: profile.byte_order() == ByteOrder::BigEndian, + }) + } Some(Algorithm::MlDsa(set)) => Some(Signature::PostQuantum(match set { structured_pkix::algorithm::ml_dsa::ParameterSet::MlDsa44 => { super::PqAlgorithm::MlDsa44 diff --git a/src/xmldsig/signature.rs b/src/xmldsig/signature.rs index 867937d9..259e74bb 100644 --- a/src/xmldsig/signature.rs +++ b/src/xmldsig/signature.rs @@ -82,6 +82,11 @@ pub(crate) fn signature_value_matches_algorithm_with_encoding( | SignatureAlgorithm::Ed25519Ctx | SignatureAlgorithm::Ed25519Ph => signature_value.len() == 64, SignatureAlgorithm::Ed448 | SignatureAlgorithm::Ed448Ph => signature_value.len() == 114, + // Two equal components of at most 64 octets; the key fixes their minimum. + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Gost34311 | SignatureAlgorithm::Dstu4145Dstu7564_256 => { + (2..=128).contains(&signature_value.len()) && signature_value.len().is_multiple_of(2) + } SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => algorithm .dsa_component_len() .is_some_and(|component_len| signature_value.len() == component_len * 2), @@ -196,6 +201,15 @@ pub(crate) fn signature_value_matches_spki_with_encoding( }); } } + #[cfg(feature = "dstu4145")] + if algorithm.dstu4145_digest().is_some() { + super::dstu4145::validate_public_key(public_key_spki_der, session)?; + return Ok(signature_value_matches_algorithm_with_encoding( + algorithm, + signature_value, + encoding, + )); + } if let Some(parameters) = algorithm.rsa_pss_parameters() { let modulus = read_rsa_public_key(session, public_key_spki_der, Some(parameters))?.modulus(); diff --git a/src/xmldsig/verify.rs b/src/xmldsig/verify.rs index 633d8448..113cc7d9 100644 --- a/src/xmldsig/verify.rs +++ b/src/xmldsig/verify.rs @@ -6399,6 +6399,25 @@ fn signature_value_limits( (encoded, resources.max_xml_document_bytes) } +/// Verify a method whose key the PEM block's SPKI carries for `modern::verify`. +fn verify_modern_pem( + algorithm: SignatureAlgorithm, + public_key_pem: &str, + signed_data: &[u8], + signature_value: &[u8], +) -> Result { + let pem = super::signature::PemBlock::decode(public_key_pem) + .filter(|pem| pem.label == "PUBLIC KEY") + .ok_or(SignatureVerificationError::InvalidKeyPem)?; + Ok(super::modern::verify( + algorithm, + &pem.contents, + signed_data, + signature_value, + &super::pkix::CertificateSession::standalone(), + )?) +} + fn verify_with_algorithm( algorithm: SignatureAlgorithm, public_key_pem: &str, @@ -6430,16 +6449,11 @@ fn verify_with_algorithm( | SignatureAlgorithm::Ed448 | SignatureAlgorithm::Ed448Ph | SignatureAlgorithm::PostQuantum(_) => { - let pem = super::signature::PemBlock::decode(public_key_pem) - .filter(|pem| pem.label == "PUBLIC KEY") - .ok_or(SignatureVerificationError::InvalidKeyPem)?; - Ok(super::modern::verify( - algorithm, - &pem.contents, - signed_data, - signature_value, - &super::pkix::CertificateSession::standalone(), - )?) + verify_modern_pem(algorithm, public_key_pem, signed_data, signature_value) + } + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Gost34311 | SignatureAlgorithm::Dstu4145Dstu7564_256 => { + verify_modern_pem(algorithm, public_key_pem, signed_data, signature_value) } SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => { let pem = super::signature::PemBlock::decode(public_key_pem) diff --git a/src/xmldsig/verify/session/hash_uses.rs b/src/xmldsig/verify/session/hash_uses.rs index fe107590..ff6aed1a 100644 --- a/src/xmldsig/verify/session/hash_uses.rs +++ b/src/xmldsig/verify/session/hash_uses.rs @@ -81,6 +81,10 @@ fn message_hash(algorithm: SignatureAlgorithm) -> HashUseAlgorithm<'static> { if let Some(digest) = algorithm.ecdsa_digest() { return HashUseAlgorithm::Digest(digest); } + #[cfg(feature = "dstu4145")] + if let Some(digest) = algorithm.dstu4145_digest() { + return HashUseAlgorithm::Digest(digest); + } let digest = match algorithm { S::RsaSha1 | S::DsaSha1 | S::HmacSha1 => D::Sha1, S::RsaSha224 | S::HmacSha224 => D::Sha224, diff --git a/src/xmldsig/x509.rs b/src/xmldsig/x509.rs index 9a6f2e16..e9851cba 100644 --- a/src/xmldsig/x509.rs +++ b/src/xmldsig/x509.rs @@ -1341,6 +1341,10 @@ fn x509_signature_algorithm( require_null_or_absent_signature_parameters(identifier)?; } X509SignatureAlgorithm::RsaPss { .. } => {} + #[cfg(feature = "dstu4145")] + X509SignatureAlgorithm::Dstu4145 { .. } => { + require_absent_signature_parameters(identifier)?; + } } Ok(algorithm) } diff --git a/tests/dstu4145_cms.rs b/tests/dstu4145_cms.rs new file mode 100644 index 00000000..e5759da8 --- /dev/null +++ b/tests/dstu4145_cms.rs @@ -0,0 +1,233 @@ +//! CMS `SignedData` with DSTU 4145: the RFC 3161 tokens embedded in the XAdES-LTA +//! sample published by the Central Certification Authority of Ukraine. +#![cfg(all(feature = "dstu4145", feature = "cms"))] + +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use std::collections::BTreeSet; +use std::sync::Arc; +use structured_asn1::Parser; +use structured_asn1::memory::{Memory, MemoryAccount, Work}; +use structured_cms::binding::Scope; +use structured_cms::prepare::{self, Preparation}; +use structured_cms::provider::SignatureAnswer; +use structured_cms::signed::SignedData; +use structured_cms::timestamp::{CheckContext, Token, TokenSignature}; +use structured_cms::verify::{self, ContentAgreement}; +use structured_pkix::path::WorkingKey; +use structured_pkix::temporal::{Instant, LeapTable}; +use xml_sec::policy::{CertificateSignatureAlgorithms, VerificationPolicy}; +use xml_sec::provider::{ + CryptoProvider, ProviderCapability, ProviderError, RustCryptoProvider, X509SignatureAlgorithm, +}; +use xml_sec::xmldsig::DigestAlgorithm; +use xml_sec::xmldsig::cms::CmsProvider; + +const LTA: &str = include_str!("fixtures/xmldsig/dstu4145/dstu-detached-lta.xml"); +const GOST_CADES: &[u8] = include_bytes!("fixtures/xmldsig/dstu4145/gost-detached-bes.p7s"); +const KUPYNA_CADES: &[u8] = include_bytes!("fixtures/xmldsig/dstu4145/kupyna-detached-bes.p7s"); +const CONTENT: &[u8] = include_bytes!("fixtures/xmldsig/dstu4145/cades-content.bin"); + +fn tokens() -> Vec> { + LTA.split("').expect("fixture opens its token"); + let (encoded, _) = value + .split_once("") + .expect("fixture closes its token"); + STANDARD.decode(encoded).expect("fixture value is Base64") + }) + .collect() +} + +/// The answer for `token` under the certificate it carries. +fn answer( + token: &[u8], + allowed: Option, +) -> Option { + let memory = Memory::new(Arc::new(MemoryAccount::new(1 << 22, 1 << 26))); + let parser = Parser::ber(memory); + let token = Token::read(&parser, token, 0).expect("the token must parse"); + let parent = parser.read(&[], 0).expect("an empty parent"); + let certificate = token + .message() + .certificates() + .first() + .expect("the token carries its authority"); + let utc = LeapTable::new(0, 2_000_000_000, &[]).expect("UTC facts"); + let mut policy = VerificationPolicy { + digest_algorithms: Some(BTreeSet::from([DigestAlgorithm::Gost34311])), + ..VerificationPolicy::default() + }; + policy.key_trust.certificate_signature_algorithms = allowed; + let provider = RustCryptoProvider; + let cms = CmsProvider::new(&provider, &policy); + let context = CheckContext { + certificate, + certificate_parent: &parent, + key: WorkingKey::standing(certificate.tbs().public_key()), + requested: None, + utc: &utc, + reference: Some(Instant::new(1_717_200_000, 0).expect("instant")), + window: None, + policy_accuracy: None, + }; + let covered: &[u8] = b"content other than the signature"; + let checked = token + .check(&cms, &parent, &context, &mut [covered], |_, _| { + Ok::<_, ()>(false) + }) + .ok()?; + match &checked.signature { + TokenSignature::Checked(checked) => checked.signature(), + TokenSignature::Unsupported(_) | TokenSignature::Malformed(_) => None, + } +} + +/// What a detached `SignedData` check answers. +#[derive(Debug, PartialEq, Eq)] +enum Detached { + /// Whether the content matched, and the answer for the signed attributes. + Checked(bool, Option), + /// The signer's declarations contradict each other. + Malformed, +} + +/// Check the published detached `sample` over `content`, under the certificate +/// it carries and with both Ukrainian digests permitted. +fn detached(sample: &[u8], content: &[u8]) -> Detached { + let mut policy = VerificationPolicy { + digest_algorithms: Some(BTreeSet::from([ + DigestAlgorithm::Gost34311, + DigestAlgorithm::Dstu7564_256, + ])), + ..VerificationPolicy::default() + }; + policy.key_trust.certificate_signature_algorithms = + Some(CertificateSignatureAlgorithms::AllSupported); + let provider = RustCryptoProvider; + let cms = CmsProvider::new(&provider, &policy); + let parser = Parser::ber(Memory::new(Arc::new(MemoryAccount::new(1 << 22, 1 << 26)))); + let message = SignedData::read(&parser, sample, 0).expect("the sample must parse"); + let parent = parser.read(&[], 0).expect("an empty parent"); + let certificate = &message.certificates()[0]; + let preparation = prepare::resolve( + &cms, + &message.signers()[0], + WorkingKey::standing(certificate.tbs().public_key()), + &parent, + Scope::Content(message.content().kind()), + ) + .expect("the provider must answer"); + let request = match preparation { + Preparation::Ready(request) => request, + Preparation::Malformed(_) => return Detached::Malformed, + Preparation::Unsupported(part) => panic!("unsupported {part:?}"), + }; + let work = parent.execution(); + let mut requests = work.vector(); + requests.try_push(request).expect("one request"); + let batch = verify::Batch::prepare(&cms, requests.into_owned(), &work, "content", 0) + .expect("the batch must admit one signer"); + let results = batch + .run(&mut [content], &work) + .expect("the pass must complete"); + let checked = &results[0]; + Detached::Checked( + matches!(checked.content(), ContentAgreement::Matches), + checked.signature(), + ) +} + +#[test] +fn published_detached_signatures_verify_with_both_digests() { + // Unlike the tokens, these carry their signer, so the content digest and the + // Kupyna signature method run against bytes signed by another implementation. + for sample in [GOST_CADES, KUPYNA_CADES] { + assert_eq!( + detached(sample, CONTENT), + Detached::Checked(true, Some(SignatureAnswer::Valid)) + ); + } +} + +#[test] +fn other_detached_content_disagrees_before_any_signature_runs() { + assert_eq!( + detached(KUPYNA_CADES, b"another"), + Detached::Checked(false, None) + ); +} + +#[test] +fn a_signature_method_naming_another_digest_is_malformed() { + // digestAlgorithm stays Kupyna while signatureAlgorithm becomes the shorter + // GOST 34.311 method at the same place: the published offsets of this + // sample, one octet less in each of the five enclosing lengths. + const METHOD: usize = 3440; + let mut sample = KUPYNA_CADES.to_vec(); + assert_eq!(sample[METHOD..METHOD + 4], [0x30, 0x0e, 0x06, 0x0c]); + sample.splice( + METHOD..METHOD + 16, + [ + 0x30, 0x0d, 0x06, 0x0b, 0x2a, 0x86, 0x24, 0x02, 0x01, 0x01, 0x01, 0x01, 0x03, 0x01, + 0x01, + ], + ); + for header in [0, 15, 19, 1581, 1585] { + let length = u16::from_be_bytes([sample[header + 2], sample[header + 3]]) - 1; + sample[header + 2..header + 4].copy_from_slice(&length.to_be_bytes()); + } + assert_eq!(detached(&sample, CONTENT), Detached::Malformed); +} + +#[test] +fn a_foreign_digest_is_unsupported_not_a_panic() { + // The variant is public, so a caller can pair it with any digest. The key + // is valid (the signer's SPKI in the published sample) so that the refusal + // comes from the pairing and nothing earlier hides it. + let algorithm = X509SignatureAlgorithm::Dstu4145 { + digest: DigestAlgorithm::Sha256, + big_endian: false, + }; + let provider = RustCryptoProvider; + assert!(!provider.supports(ProviderCapability::VerifyCertificate(algorithm))); + let error = provider + .verify_x509_signature(algorithm, b"signed", &[0; 64], &KUPYNA_CADES[513..691]) + .expect_err("no such method exists"); + assert!( + matches!(error, ProviderError::Unsupported { .. }), + "{error:?}" + ); +} + +#[test] +fn a_published_timestamp_token_verifies() { + // Each token is a DSTU 4145 SignedData over a TSTInfo. SignerInfo holds the + // signature octets directly, where a certificate nests an OCTET STRING. + let tokens = tokens(); + assert_ne!(tokens.len(), 0); + for token in &tokens { + assert_eq!( + answer(token, Some(CertificateSignatureAlgorithms::AllSupported)), + Some(SignatureAnswer::Valid) + ); + } +} + +#[test] +fn a_forged_timestamp_signature_is_invalid() { + // The last octet of the token belongs to the signature of its only signer. + let mut token = tokens().remove(0); + *token.last_mut().expect("a token") ^= 1; + assert_eq!( + answer(&token, Some(CertificateSignatureAlgorithms::AllSupported)), + Some(SignatureAnswer::Invalid) + ); +} + +#[test] +fn the_cms_path_needs_explicit_permission() { + // Without an allowlist the provider refuses before it reads a signed byte. + assert_eq!(answer(&tokens()[0], None), None); +} diff --git a/tests/dstu4145_interop.rs b/tests/dstu4145_interop.rs new file mode 100644 index 00000000..dcd2219d --- /dev/null +++ b/tests/dstu4145_interop.rs @@ -0,0 +1,326 @@ +//! DSTU 4145 verification against the test signatures published by the Central +//! Certification Authority of Ukraine (). +#![cfg(feature = "dstu4145")] + +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use std::collections::BTreeSet; +use xml_sec::policy::{ + CertificateSignatureAlgorithms, Instant, VerificationPolicy, VerificationTrustMode, +}; +use xml_sec::xmldsig::{ + DefaultKeyResolver, DigestAlgorithm, DsigError, DsigStatus, FailureReason, KeyResolverConfig, + SignatureAlgorithm, VerifyContext, +}; + +const GOST: &str = include_str!("fixtures/xmldsig/dstu4145/dstu-enveloped-bb.xml"); +const KUPYNA: &str = include_str!("fixtures/xmldsig/dstu4145/kupyna-enveloped-bb.xml"); +const LTA: &str = include_str!("fixtures/xmldsig/dstu4145/dstu-detached-lta.xml"); +/// June 2024, inside the validity of every published certificate. +const MOMENT: i64 = 1_717_200_000; + +fn permissive() -> VerificationPolicy { + let mut policy = VerificationPolicy::default(); + policy.key_trust.mode = VerificationTrustMode::CryptographicOnly; + policy.digest_algorithms = Some(BTreeSet::from([ + DigestAlgorithm::Gost34311, + DigestAlgorithm::Dstu7564_256, + ])); + let signatures = BTreeSet::from([ + SignatureAlgorithm::Dstu4145Gost34311, + SignatureAlgorithm::Dstu4145Dstu7564_256, + ]); + policy.signature_algorithms = Some(signatures.clone()); + policy.key_trust.allowed_legacy_signature_algorithms = signatures; + policy +} + +fn status(xml: &str) -> DsigStatus { + let resolver = DefaultKeyResolver::default(); + VerifyContext::new() + .policy(permissive()) + .key_resolver(&resolver) + .verify(xml) + .expect("verification must run") + .status +} + +/// The issuing CA and the self-signed root, in the order the LTA sample keeps them. +fn published_chain() -> (Vec, Vec) { + let mut certificates = LTA + .split("") + .skip(1) + .map(|part| { + let (encoded, _) = part + .split_once("") + .expect("fixture closes its certificate"); + STANDARD.decode(encoded).expect("fixture value is Base64") + }); + let issuing = certificates.next().expect("fixture carries the issuing CA"); + let root = certificates.next().expect("fixture carries the root"); + (issuing, root) +} + +/// Verify the signature in `GOST` under full X.509 path validation at a moment +/// inside the published certificates' validity. +fn chain_result( + lookup: Vec>, + trusted: Vec>, + allowed: Option, +) -> Result { + chain_result_at(MOMENT, lookup, trusted, allowed) +} + +fn chain_result_at( + seconds: i64, + lookup: Vec>, + trusted: Vec>, + allowed: Option, +) -> Result { + let mut policy = permissive(); + policy.key_trust.mode = VerificationTrustMode::RequireTrustedKey; + policy.key_trust.verify_x509_chains = true; + policy.key_trust.verification_time = Some(Instant::new(seconds, 0).expect("an instant")); + policy.key_trust.certificate_signature_algorithms = allowed; + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + lookup_certs: lookup, + trusted_certs: trusted, + ..KeyResolverConfig::default() + }); + VerifyContext::new() + .policy(policy) + .key_resolver(&resolver) + .verify(GOST) + .map(|result| result.status) +} + +/// `xml` with the `SignatureValue` octet at `index` flipped. +fn with_flipped_signature_octet(xml: &str, index: usize) -> String { + let (head, rest) = xml + .split_once("") + .expect("fixture has a SignatureValue"); + let (value, tail) = rest + .split_once("") + .expect("fixture closes its SignatureValue"); + let mut octets = STANDARD.decode(value).expect("fixture value is Base64"); + octets[index] ^= 1; + format!( + "{head}{}{tail}", + STANDARD.encode(octets) + ) +} + +#[test] +fn published_gost34311_signature_verifies() { + assert_eq!(status(GOST), DsigStatus::Valid); +} + +#[test] +fn published_dstu7564_signature_verifies() { + assert_eq!(status(KUPYNA), DsigStatus::Valid); +} + +#[test] +fn every_reference_of_a_published_signature_is_digested_and_valid() { + // A passing status alone could hide a Reference that was never processed. + let resolver = DefaultKeyResolver::default(); + let result = VerifyContext::new() + .policy(permissive()) + .key_resolver(&resolver) + .verify(GOST) + .expect("verification must run"); + assert_eq!(result.signed_info_references.len(), 2); + for reference in &result.signed_info_references { + assert_eq!(reference.digest_algorithm, DigestAlgorithm::Gost34311); + assert_eq!(reference.status, DsigStatus::Valid); + } +} + +#[test] +fn an_altered_signature_of_either_component_is_invalid() { + // Index 0 falls in r and 40 in s, which the verifier reads separately. + for xml in [GOST, KUPYNA] { + for index in [0, 40] { + assert!(matches!( + status(&with_flipped_signature_octet(xml, index)), + DsigStatus::Invalid(_) + )); + } + } +} + +#[test] +fn altered_signed_content_fails_its_reference() { + // The fixture signs its enveloping element, including the Signature's + // absence, so a new attribute reaches the Reference digest. + let tampered = KUPYNA.replacen("", "", 1); + assert_ne!(tampered, KUPYNA); + assert_eq!( + status(&tampered), + DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 }) + ); +} + +#[test] +fn the_digest_of_one_family_does_not_stand_for_the_other() { + // Same width, different function: swapping the method must not verify. + let swapped = KUPYNA.replace( + "http://www.w3.org/2001/04/xmlenc#dstu7564-256", + "http://www.w3.org/2001/04/xmlenc#gost34311", + ); + assert!(matches!(status(&swapped), DsigStatus::Invalid(_))); +} + +#[test] +fn identifiers_outside_the_published_set_are_unsupported() { + // The variants without a published sample stay unknown, never a neighbour. + for uri in [ + "http://www.w3.org/2001/04/xmlenc#dstu7564-384", + "http://www.w3.org/2001/04/xmlenc#dstu7564-512", + "http://www.w3.org/2001/04/xmlenc#GOST34311", + ] { + assert_eq!(DigestAlgorithm::from_uri(uri), None); + } + for uri in [ + "http://www.w3.org/2001/04/xmldsig-more#dstu4145-dstu7564-384", + "http://www.w3.org/2001/04/xmldsig-more#dstu4145", + ] { + assert_eq!(SignatureAlgorithm::from_uri(uri), None); + } +} + +#[test] +fn the_identifiers_round_trip() { + for algorithm in [DigestAlgorithm::Gost34311, DigestAlgorithm::Dstu7564_256] { + assert_eq!(DigestAlgorithm::from_uri(algorithm.uri()), Some(algorithm)); + } + for algorithm in [ + SignatureAlgorithm::Dstu4145Gost34311, + SignatureAlgorithm::Dstu4145Dstu7564_256, + ] { + assert_eq!( + SignatureAlgorithm::from_uri(algorithm.uri()), + Some(algorithm) + ); + } +} + +#[test] +fn the_default_policy_refuses_the_methods_without_explicit_permission() { + // Building the capability must not grant it. + let resolver = DefaultKeyResolver::default(); + let mut policy = VerificationPolicy::default(); + policy.key_trust.mode = VerificationTrustMode::CryptographicOnly; + let error = VerifyContext::new() + .policy(policy) + .key_resolver(&resolver) + .verify(GOST) + .expect_err("capability is not permission"); + assert!(matches!(error, DsigError::Policy(_)), "{error:?}"); +} + +#[test] +fn a_key_under_another_substitution_table_is_refused() { + // The URI cannot name a table, so a key that states one other than number + // one must not verify as if it had. The sample states the table explicitly. + let (head, rest) = GOST + .split_once("") + .expect("fixture has a certificate"); + let (encoded, tail) = rest + .split_once("") + .expect("fixture closes its certificate"); + let mut certificate = STANDARD.decode(encoded).expect("fixture value is Base64"); + let table = certificate + .windows(4) + .position(|window| window == [0xa9, 0xd6, 0xeb, 0x45]) + .expect("the key states the table"); + certificate[table] ^= 1; + let xml = format!( + "{head}{}{tail}", + STANDARD.encode(certificate) + ); + let resolver = DefaultKeyResolver::default(); + let result = VerifyContext::new() + .policy(permissive()) + .key_resolver(&resolver) + .verify(&xml) + .map(|result| result.status); + assert_ne!(result.ok(), Some(DsigStatus::Valid)); +} + +#[test] +fn a_published_signature_chains_to_its_root() { + // The leaf is under a 257-bit CA, which is under a 431-bit root that + // writes its substitution table out: three keys, two curves. + let (issuing, root) = published_chain(); + assert_eq!( + chain_result( + vec![issuing], + vec![root], + Some(CertificateSignatureAlgorithms::AllSupported) + ) + .expect("the chain must validate"), + DsigStatus::Valid + ); +} + +#[test] +fn a_historical_signature_validates_only_at_its_own_time() { + // The leaf expired in 2026: a verifier reasons about the moment it is given, + // so the same chain that holds in 2024 must not hold a decade later. + let (issuing, root) = published_chain(); + let at = |seconds| { + chain_result_at( + seconds, + vec![issuing.clone()], + vec![root.clone()], + Some(CertificateSignatureAlgorithms::AllSupported), + ) + }; + assert_eq!(at(MOMENT).expect("inside validity"), DsigStatus::Valid); + at(2_000_000_000).expect_err("after every certificate expired"); +} + +#[test] +fn a_certificate_chain_needs_explicit_permission() { + // No allowlist means classical algorithms only, as for post-quantum ones. + let (issuing, root) = published_chain(); + let error = chain_result(vec![issuing], vec![root], None) + .expect_err("capability is not permission for certificates either"); + assert!(error.to_string().contains("Dstu4145"), "{error}"); +} + +#[test] +fn a_forged_certificate_breaks_the_chain() { + // The last octet belongs to the CA's signature, so its issuer must refuse it. + let (mut issuing, root) = published_chain(); + *issuing.last_mut().expect("a certificate") ^= 1; + chain_result( + vec![issuing], + vec![root], + Some(CertificateSignatureAlgorithms::AllSupported), + ) + .expect_err("a forged intermediate must not chain"); +} + +#[test] +fn a_chain_without_its_anchor_is_refused() { + let (issuing, _) = published_chain(); + chain_result( + vec![issuing], + Vec::new(), + Some(CertificateSignatureAlgorithms::AllSupported), + ) + .expect_err("no anchor, no trust"); +} + +#[test] +fn the_methods_never_sign() { + for algorithm in [ + SignatureAlgorithm::Dstu4145Gost34311, + SignatureAlgorithm::Dstu4145Dstu7564_256, + ] { + assert!(!algorithm.signing_allowed()); + } + assert!(!DigestAlgorithm::Gost34311.signing_allowed()); + assert!(!DigestAlgorithm::Dstu7564_256.signing_allowed()); +} diff --git a/tests/fixtures/xmldsig/dstu4145/PROVENANCE.txt b/tests/fixtures/xmldsig/dstu4145/PROVENANCE.txt new file mode 100644 index 00000000..cb230f5b --- /dev/null +++ b/tests/fixtures/xmldsig/dstu4145/PROVENANCE.txt @@ -0,0 +1,15 @@ +Test signatures published unchanged by the Central Certification Authority of +Ukraine at https://czo.gov.ua/testexamples, under /download/test_sign/XAdES/: + +dstu-enveloped-bb.xml DSTU/enveloped/XAdES-B-B/test.xml +kupyna-enveloped-bb.xml DSTU-7564/enveloped/XAdES-B-B/test.xml +dstu-detached-lta.xml DSTU/detached/XAdES-B-LTA/sign.xml + +and under /download/test_sign/CAdES/: + +gost-detached-bes.p7s DSTU/detached/CAdES-BES/test.txt.p7s +kupyna-detached-bes.p7s DSTU-7564/detached/CAdES-BES/test.txt.p7s +cades-content.bin DSTU-7564/detached/CAdES-BES/test.txt + +They carry test certificates only. The certificates expired in 2026, so path +validation runs at a fixed moment inside their validity. diff --git a/tests/fixtures/xmldsig/dstu4145/cades-content.bin b/tests/fixtures/xmldsig/dstu4145/cades-content.bin new file mode 100644 index 00000000..464a7cfe --- /dev/null +++ b/tests/fixtures/xmldsig/dstu4145/cades-content.bin @@ -0,0 +1 @@ +Test diff --git a/tests/fixtures/xmldsig/dstu4145/dstu-detached-lta.xml b/tests/fixtures/xmldsig/dstu4145/dstu-detached-lta.xml new file mode 100644 index 00000000..178dc22d --- /dev/null +++ b/tests/fixtures/xmldsig/dstu4145/dstu-detached-lta.xml @@ -0,0 +1 @@ +5Z+/zWngOYERfS+3P3tk2R0ZOR9eZYnaylHp0ipW1j0=r0RbfSG+3yewYK9Cju8yVI+d6ltJtToItSrmwNBLkKM=7Ch5eJof1/TrGEjyAHG8NsuUzyux5iPbCrvfm38rHjBzIF558SgedVeiHay4ouKDMOLRG8m2sdNOvHUiwX4JdQ==MIIF4DCCBYigAwIBAgIUNjBDgD6aNBwEAAAAIgQAAPl4AAAwDQYLKoYkAgEBAQEDAQEwgbQxITAfBgNVBAoMGNCU0J8gItCU0IbQryIgKNCi0JXQodCiKTE7MDkGA1UEAwwy0JDQtNC80ZbQvdGW0YHRgtGA0LDRgtC+0YAg0IbQotChINCm0JfQniAoQ0EgVEVTVCkxGTAXBgNVBAUTEFVBLTQzMzk1MDMzLTIxMDExCzAJBgNVBAYTAlVBMREwDwYDVQQHDAjQmtC40ZfQsjEXMBUGA1UEYQwOTlRSVUEtNDMzOTUwMzMwHhcNMjQwMzAzMjIwMDAwWhcNMjYwMzAzMjE1OTU5WjCBrDE8MDoGA1UEAwwz0KLQldCh0KIg0KLQtdGB0YLQvtCy0LjQuSDQotC10YHRgiDQotC10YHRgtC+0LLQuNGHMRkwFwYDVQQEDBDQotC10YHRgtC+0LLQuNC5MSIwIAYDVQQqDBnQotC10YHRgiDQotC10YHRgtC+0LLQuNGHMQ0wCwYDVQQFEwQxMDU4MQswCQYDVQQGEwJVQTERMA8GA1UEBwwI0JrQuNGX0LIwgfIwgckGCyqGJAIBAQEBAwEBMIG5MHUwBwICAQECAQwCAQAEIRC+49tq6p4fhleMRcEllP+UI5Sn1zj5GH5lFQFylPTOAQIhAIAAAAAAAAAAAAAAAAAAAABnWSE68YLph9PhdxSQfUcNBCG2D9LY3OipNCPGEBvKkcR6AH5sMAsmzVVsmw59IO8pKgAEQKnW60XxPHCCgMSWeyMfXq32WOukwDcpHTjZa/Alyk4X+OlyDcYVtDool18Lwd6jZDi1ZOosF5/QEj5tuPrFeQQDJAAEIVuxDgv8iNgXJkG7xdgjSisLQDyjImTMI5Hvh/LZ3iNuAaOCAt8wggLbMCkGA1UdDgQiBCCKAVrKLAMjVV+HRE4VECuDc4DhmY6gBAq1PFuzHMQA6jArBgNVHSMEJDAigCA2MEOAPpo0HJqXmRJFYfjbc4x+P7e9o/Gf5jeoscPKIDAOBgNVHQ8BAf8EBAMCBsAwRAYDVR0gBD0wOzA5BgkqhiQCAQEBAgIwLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9jYS10ZXN0LmN6by5nb3YudWEvY3BzMAkGA1UdEwQCMAAwdgYIKwYBBQUHAQMEajBoMAgGBgQAjkYBATAXBgYEAI5GAQIwDRMDVUFIAgMPQkACAQAwNgYGBACORgEFMCwwKhYkaHR0cHM6Ly9jYS10ZXN0LmN6by5nb3YudWEvcmVnbGFtZW50EwJlbjALBgkqhiQCAQEBAgEwLAYDVR0RBCUwI4EOVGVzdEBUZXN0LnRlc3SgEQYKKwYBBAGCNxQCA6ADDAExME4GA1UdHwRHMEUwQ6BBoD+GPWh0dHA6Ly9jYS10ZXN0LmN6by5nb3YudWEvZG93bmxvYWQvY3Jscy9UZXN0Q1NLLTIwMjEtRnVsbC5jcmwwTwYDVR0uBEgwRjBEoEKgQIY+aHR0cDovL2NhLXRlc3QuY3pvLmdvdi51YS9kb3dubG9hZC9jcmxzL1Rlc3RDU0stMjAyMS1EZWx0YS5jcmwwgZMGCCsGAQUFBwEBBIGGMIGDMDQGCCsGAQUFBzABhihodHRwOi8vY2EtdGVzdC5jem8uZ292LnVhL3NlcnZpY2VzL29jc3AvMEsGCCsGAQUFBzAChj9odHRwczovL2NhLXRlc3QuY3pvLmdvdi51YS9kb3dubG9hZC9jZXJ0aWZpY2F0ZXMvVGVzdENBMjAyMS5wN2IwQwYIKwYBBQUHAQsENzA1MDMGCCsGAQUFBzADhidodHRwOi8vY2EtdGVzdC5jem8uZ292LnVhL3NlcnZpY2VzL3RzcC8wDQYLKoYkAgEBAQEDAQEDQwAEQFF5fDCuTcBUebwMBw7TvbMba1HTtn+wDWE0iJY6QV1UFthli9JZW1TsqOXOPd9Qt3X2pHg82TBMgCe0g3mLlTU=2024-03-04T08:08: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 \ No newline at end of file diff --git a/tests/fixtures/xmldsig/dstu4145/dstu-enveloped-bb.xml b/tests/fixtures/xmldsig/dstu4145/dstu-enveloped-bb.xml new file mode 100644 index 00000000..11659c78 --- /dev/null +++ b/tests/fixtures/xmldsig/dstu4145/dstu-enveloped-bb.xml @@ -0,0 +1,2 @@ + +5Z+/zWngOYERfS+3P3tk2R0ZOR9eZYnaylHp0ipW1j0=RoLkVrZhSFfHh/cDty4dB1hLoFfc7YNYUn639wFanp8=br9odUP5I+ix4+2Iif2GFc+bq/sruCycFFQCrtTZCUjtizJP8pFLrxfH+Z+Qiw1coRX95use0xnwCRXcuVXrUA==MIIF4DCCBYigAwIBAgIUNjBDgD6aNBwEAAAAIgQAAPl4AAAwDQYLKoYkAgEBAQEDAQEwgbQxITAfBgNVBAoMGNCU0J8gItCU0IbQryIgKNCi0JXQodCiKTE7MDkGA1UEAwwy0JDQtNC80ZbQvdGW0YHRgtGA0LDRgtC+0YAg0IbQotChINCm0JfQniAoQ0EgVEVTVCkxGTAXBgNVBAUTEFVBLTQzMzk1MDMzLTIxMDExCzAJBgNVBAYTAlVBMREwDwYDVQQHDAjQmtC40ZfQsjEXMBUGA1UEYQwOTlRSVUEtNDMzOTUwMzMwHhcNMjQwMzAzMjIwMDAwWhcNMjYwMzAzMjE1OTU5WjCBrDE8MDoGA1UEAwwz0KLQldCh0KIg0KLQtdGB0YLQvtCy0LjQuSDQotC10YHRgiDQotC10YHRgtC+0LLQuNGHMRkwFwYDVQQEDBDQotC10YHRgtC+0LLQuNC5MSIwIAYDVQQqDBnQotC10YHRgiDQotC10YHRgtC+0LLQuNGHMQ0wCwYDVQQFEwQxMDU4MQswCQYDVQQGEwJVQTERMA8GA1UEBwwI0JrQuNGX0LIwgfIwgckGCyqGJAIBAQEBAwEBMIG5MHUwBwICAQECAQwCAQAEIRC+49tq6p4fhleMRcEllP+UI5Sn1zj5GH5lFQFylPTOAQIhAIAAAAAAAAAAAAAAAAAAAABnWSE68YLph9PhdxSQfUcNBCG2D9LY3OipNCPGEBvKkcR6AH5sMAsmzVVsmw59IO8pKgAEQKnW60XxPHCCgMSWeyMfXq32WOukwDcpHTjZa/Alyk4X+OlyDcYVtDool18Lwd6jZDi1ZOosF5/QEj5tuPrFeQQDJAAEIVuxDgv8iNgXJkG7xdgjSisLQDyjImTMI5Hvh/LZ3iNuAaOCAt8wggLbMCkGA1UdDgQiBCCKAVrKLAMjVV+HRE4VECuDc4DhmY6gBAq1PFuzHMQA6jArBgNVHSMEJDAigCA2MEOAPpo0HJqXmRJFYfjbc4x+P7e9o/Gf5jeoscPKIDAOBgNVHQ8BAf8EBAMCBsAwRAYDVR0gBD0wOzA5BgkqhiQCAQEBAgIwLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9jYS10ZXN0LmN6by5nb3YudWEvY3BzMAkGA1UdEwQCMAAwdgYIKwYBBQUHAQMEajBoMAgGBgQAjkYBATAXBgYEAI5GAQIwDRMDVUFIAgMPQkACAQAwNgYGBACORgEFMCwwKhYkaHR0cHM6Ly9jYS10ZXN0LmN6by5nb3YudWEvcmVnbGFtZW50EwJlbjALBgkqhiQCAQEBAgEwLAYDVR0RBCUwI4EOVGVzdEBUZXN0LnRlc3SgEQYKKwYBBAGCNxQCA6ADDAExME4GA1UdHwRHMEUwQ6BBoD+GPWh0dHA6Ly9jYS10ZXN0LmN6by5nb3YudWEvZG93bmxvYWQvY3Jscy9UZXN0Q1NLLTIwMjEtRnVsbC5jcmwwTwYDVR0uBEgwRjBEoEKgQIY+aHR0cDovL2NhLXRlc3QuY3pvLmdvdi51YS9kb3dubG9hZC9jcmxzL1Rlc3RDU0stMjAyMS1EZWx0YS5jcmwwgZMGCCsGAQUFBwEBBIGGMIGDMDQGCCsGAQUFBzABhihodHRwOi8vY2EtdGVzdC5jem8uZ292LnVhL3NlcnZpY2VzL29jc3AvMEsGCCsGAQUFBzAChj9odHRwczovL2NhLXRlc3QuY3pvLmdvdi51YS9kb3dubG9hZC9jZXJ0aWZpY2F0ZXMvVGVzdENBMjAyMS5wN2IwQwYIKwYBBQUHAQsENzA1MDMGCCsGAQUFBzADhidodHRwOi8vY2EtdGVzdC5jem8uZ292LnVhL3NlcnZpY2VzL3RzcC8wDQYLKoYkAgEBAQEDAQEDQwAEQFF5fDCuTcBUebwMBw7TvbMba1HTtn+wDWE0iJY6QV1UFthli9JZW1TsqOXOPd9Qt3X2pHg82TBMgCe0g3mLlTU=2024-03-04T08:16:53ZGkFkYH6Kw3j7GQN/pwMIShHzH54K4k7CJWCyG+l+1ho=text/xml \ No newline at end of file diff --git a/tests/fixtures/xmldsig/dstu4145/gost-detached-bes.p7s b/tests/fixtures/xmldsig/dstu4145/gost-detached-bes.p7s new file mode 100644 index 00000000..896e279c Binary files /dev/null and b/tests/fixtures/xmldsig/dstu4145/gost-detached-bes.p7s differ diff --git a/tests/fixtures/xmldsig/dstu4145/kupyna-detached-bes.p7s b/tests/fixtures/xmldsig/dstu4145/kupyna-detached-bes.p7s new file mode 100644 index 00000000..50da883e Binary files /dev/null and b/tests/fixtures/xmldsig/dstu4145/kupyna-detached-bes.p7s differ diff --git a/tests/fixtures/xmldsig/dstu4145/kupyna-enveloped-bb.xml b/tests/fixtures/xmldsig/dstu4145/kupyna-enveloped-bb.xml new file mode 100644 index 00000000..5f7db165 --- /dev/null +++ b/tests/fixtures/xmldsig/dstu4145/kupyna-enveloped-bb.xml @@ -0,0 +1,2 @@ + +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-03-04T09:04:42ZMAc3YoZ6rxDUZKYco59RLYStGZHdcRAh/8FktnKtRoU=text/xml \ No newline at end of file diff --git a/tests/fixtures_smoke.rs b/tests/fixtures_smoke.rs index 7a7e4651..f49d69bf 100644 --- a/tests/fixtures_smoke.rs +++ b/tests/fixtures_smoke.rs @@ -258,7 +258,9 @@ fn fixture_file_count_matches_expected() { // All 17 upstream PSS cases include both signed XML and its template. // ML-KEM adds six key formats and two DSig inputs for each of three sets. // Two alloc-only inputs cover detached and multi-signature documents. - ("xmldsig", 393), + // Five published DSTU 4145 signatures add one signed original and + // their provenance note. + ("xmldsig", 400), ("saml", 2), // Complete Merlin, Phaos and Aleksey snapshots, plus shared XDH keys. // Agreement recipes additionally need all 66 EC and 12 DHX key files. diff --git a/tools/xml-sec-tools/src/check_features.rs b/tools/xml-sec-tools/src/check_features.rs index ed13c75b..93f242ae 100644 --- a/tools/xml-sec-tools/src/check_features.rs +++ b/tools/xml-sec-tools/src/check_features.rs @@ -74,7 +74,7 @@ struct Profile { facade: bool, } -const PROFILES: [Profile; 16] = [ +const PROFILES: [Profile; 17] = [ profile("default", None, &["rust_api"], false), profile("dom-xmloxide", Some("xml-backend-xmloxide"), &[], false), profile("dom-roxmltree", Some("xml-backend-roxmltree"), &[], false), @@ -139,6 +139,12 @@ const PROFILES: [Profile; 16] = [ &[], true, ), + profile( + "dstu4145", + Some("dstu4145,xml-backend-xmloxide"), + &["dstu4145_interop"], + false, + ), profile( "fips-dsig", Some("aws-lc-fips,xmldsig-sign,xml-backend-xmloxide"), diff --git a/tools/xml-sec-tools/src/check_features/tests.rs b/tools/xml-sec-tools/src/check_features/tests.rs index 810709af..f78d5fd3 100644 --- a/tools/xml-sec-tools/src/check_features/tests.rs +++ b/tools/xml-sec-tools/src/check_features/tests.rs @@ -216,6 +216,7 @@ fn each_minimal_profile_selects_only_its_declared_feature_graph() { "experimental", "experimental-pq,xmldsig-sign,xml-backend-xmloxide", ), + ("dstu4145", "dstu4145,xml-backend-xmloxide"), ("fips-dsig", "aws-lc-fips,xmldsig-sign,xml-backend-xmloxide"), ("pkcs11", "pkcs11,xml-backend-xmloxide"), ] { @@ -234,6 +235,19 @@ fn each_minimal_profile_selects_only_its_declared_feature_graph() { } } +#[test] +fn the_dstu_profile_runs_its_interoperability_tests_and_no_signing_facade() { + // The feature only verifies, so the signing facade has no place here, and + // the library tests alone would leave the published signatures unread. + let features = "dstu4145,xml-backend-xmloxide"; + assert_eq!( + planned("dstu4145", "test"), + [format!( + "nextest run -p xml-sec --no-default-features --features {features} --lib --test dstu4145_interop" + )] + ); +} + #[test] fn token_tests_execute_the_real_disposable_store_harness() { // The runner must not turn a missing native module into an ignored test; diff --git a/tools/xmlsec1/Cargo.toml b/tools/xmlsec1/Cargo.toml index f0d0d8e5..37d289c1 100644 --- a/tools/xmlsec1/Cargo.toml +++ b/tools/xmlsec1/Cargo.toml @@ -70,6 +70,7 @@ xml-backends-all = ["xml-sec/xml-backends-all", "xml-sec-config/xml-backends-all xml-backend-differential = ["xml-sec/xml-backend-differential", "xml-sec-config/xml-backend-differential"] legacy-algorithms = ["xml-sec/legacy-algorithms"] experimental-pq = ["xmldsig", "xml-sec/experimental-pq"] +dstu4145 = ["xmldsig", "xml-sec/dstu4145"] xslt = ["xmldsig", "xml-sec/xslt"] aws-lc-fips = ["std", "xmldsig", "xml-sec/aws-lc-fips"] pkcs11 = ["std", "xmlenc", "xml-sec/pkcs11"] diff --git a/tools/xmlsec1/src/commands/tests.rs b/tools/xmlsec1/src/commands/tests.rs index 7105379c..cf651389 100644 --- a/tools/xmlsec1/src/commands/tests.rs +++ b/tools/xmlsec1/src/commands/tests.rs @@ -592,6 +592,10 @@ fn compatibility_signing_policy_includes_every_implemented_algorithm() { assert_eq!( policy.signature_algorithms, Some(BTreeSet::from([ + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Gost34311, + #[cfg(feature = "dstu4145")] + SignatureAlgorithm::Dstu4145Dstu7564_256, #[cfg(feature = "legacy-algorithms")] SignatureAlgorithm::RsaMd5, #[cfg(feature = "legacy-algorithms")] @@ -652,6 +656,10 @@ fn compatibility_signing_policy_includes_every_implemented_algorithm() { assert_eq!( policy.digest_algorithms, Some(BTreeSet::from([ + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Gost34311, + #[cfg(feature = "dstu4145")] + DigestAlgorithm::Dstu7564_256, #[cfg(feature = "legacy-algorithms")] DigestAlgorithm::Md5, #[cfg(feature = "legacy-algorithms")]