xml-sec 0.1.21

Pure Rust XML Security: XMLDSig, XMLEnc, C14N. Drop-in replacement for libxmlsec1.
Documentation
//! HMAC policy and complete XMLDSig pipeline regression coverage.
#![cfg(feature = "xmldsig")]

#[path = "common/xmlsec1.rs"]
mod xmlsec1;

use xml_sec::policy::{PolicyViolation, SigningPolicy, VerificationPolicy};
use xml_sec::xmldsig::{
    DsigError, DsigStatus, HmacSigningKey, HmacVerificationKey, ParseError, SignContext,
    SignatureAlgorithm, SigningError, SigningKey, SigningKeyError, SigningPublicKeyInfo,
    VerifyContext,
};

fn template(algorithm: SignatureAlgorithm, output: Option<usize>) -> String {
    let parameter = output
        .map(|bits| format!("<HMACOutputLength>{bits}</HMACOutputLength>"))
        .unwrap_or_default();
    format!(
        r##"<root><payload xml:id="payload">authenticated</payload><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="{}">{parameter}</SignatureMethod><Reference URI="#payload"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue/></Reference></SignedInfo><SignatureValue/></Signature></root>"##,
        algorithm.uri()
    )
}

#[test]
fn every_hmac_algorithm_and_legal_octet_length_round_trips() {
    // Every legal truncation boundary must work through reference digesting,
    // canonicalization, signing and verification, not just direct MAC calls.
    let signing_key = HmacSigningKey::new(vec![0x42; 32]).unwrap();
    let verifying_key = HmacVerificationKey::new(vec![0x42; 32]).unwrap();
    let wrong_key = HmacVerificationKey::new(vec![0x43; 32]).unwrap();
    for (algorithm, width) in [
        #[cfg(feature = "legacy-algorithms")]
        (SignatureAlgorithm::HmacMd5, 128),
        #[cfg(feature = "legacy-algorithms")]
        (SignatureAlgorithm::HmacRipemd160, 160),
        (SignatureAlgorithm::HmacSha1, 160),
        (SignatureAlgorithm::HmacSha224, 224),
        (SignatureAlgorithm::HmacSha256, 256),
        (SignatureAlgorithm::HmacSha384, 384),
        (SignatureAlgorithm::HmacSha512, 512),
    ] {
        let mut policy = VerificationPolicy {
            signature_algorithms: Some(std::collections::HashSet::from([algorithm])),
            ..VerificationPolicy::default()
        };
        policy
            .key_trust
            .allowed_legacy_signature_algorithms
            .insert(algorithm);
        let signing_policy = SigningPolicy {
            signature_algorithms: Some(std::collections::HashSet::from([algorithm])),
            ..SigningPolicy::default()
        };
        for output in (128usize.max(width / 2)..=width)
            .step_by(8)
            .map(Some)
            .chain([None])
        {
            let signed = SignContext::new(&signing_key)
                .policy(signing_policy.clone())
                .sign_template(&template(algorithm, output))
                .unwrap();
            let result = VerifyContext::new()
                .policy(policy.clone())
                .key(&verifying_key)
                .verify(&signed)
                .unwrap();
            assert_eq!(result.status, DsigStatus::Valid, "{algorithm:?}/{output:?}");
            let wrong = VerifyContext::new()
                .policy(policy.clone())
                .key(&wrong_key)
                .verify(&signed)
                .unwrap();
            assert_ne!(wrong.status, DsigStatus::Valid, "{algorithm:?}/{output:?}");
            let tampered = signed.replace("authenticated", "unauthenticated");
            let result = VerifyContext::new()
                .policy(policy.clone())
                .key(&verifying_key)
                .verify(&tampered)
                .unwrap();
            assert_ne!(result.status, DsigStatus::Valid, "{algorithm:?}/{output:?}");
        }
    }
}

#[test]
fn hmac_parameter_and_signature_boundaries_are_strict() {
    // Neither malformed integer syntax nor an implicit full-width declaration
    // can authorize a differently sized MAC, even when its prefix is genuine.
    let key = HmacSigningKey::new(vec![0x42; 32]).unwrap();
    let verifier = HmacVerificationKey::new(vec![0x42; 32]).unwrap();
    let algorithm = SignatureAlgorithm::HmacSha256;
    for invalid in [
        "0",
        "-128",
        "129",
        "264",
        "1 28",
        "128.0",
        "128suffix",
        "184467440737095516160",
    ] {
        let xml = template(algorithm, Some(128)).replace(
            "<HMACOutputLength>128</HMACOutputLength>",
            &format!("<HMACOutputLength>{invalid}</HMACOutputLength>"),
        );
        assert!(
            SignContext::new(&key).sign_template(&xml).is_err(),
            "{invalid}"
        );
        assert!(
            VerifyContext::new().key(&verifier).verify(&xml).is_err(),
            "{invalid}"
        );
    }
    let signed = SignContext::new(&key)
        .sign_template(&template(algorithm, Some(128)))
        .unwrap();
    let implicit_full = signed.replace("<HMACOutputLength>128</HMACOutputLength>", "");
    assert!(matches!(
        VerifyContext::new().key(&verifier).verify(&implicit_full),
        Err(DsigError::InvalidStructure {
            reason: "SignatureValue length does not match HMACOutputLength"
        })
    ));
    let stricter = VerificationPolicy {
        hmac: xml_sec::policy::HmacPolicy {
            minimum_output_bits: 192,
            ..Default::default()
        },
        ..Default::default()
    };
    assert!(matches!(
        VerifyContext::new()
            .policy(stricter)
            .key(&verifier)
            .verify(&signed),
        Err(DsigError::Policy(PolicyViolation::HmacOutputLength {
            minimum: 192,
            actual: 128,
            ..
        }))
    ));
}

#[test]
fn all_hmac_methods_interoperate_bidirectionally_with_xmlsec1() {
    // An independent implementation guards against shared sign/verify bugs.
    if !xmlsec1::is_available() {
        eprintln!("{}", xmlsec1::skip_reason());
        return;
    }
    let directory = tempfile::tempdir().unwrap();
    let secret = vec![0x42; 32];
    let key_path = directory.path().join("key.bin");
    let template_path = directory.path().join("template.xml");
    let signed_path = directory.path().join("signed.xml");
    let oracle_path = directory.path().join("oracle.xml");
    std::fs::write(&key_path, &secret).unwrap();
    let signing_key = HmacSigningKey::new(secret.clone()).unwrap();
    let verifying_key = HmacVerificationKey::new(secret).unwrap();
    for (algorithm, minimum, full) in [
        (SignatureAlgorithm::HmacSha1, 128, 160),
        (SignatureAlgorithm::HmacSha224, 128, 224),
        (SignatureAlgorithm::HmacSha256, 128, 256),
        (SignatureAlgorithm::HmacSha384, 192, 384),
        (SignatureAlgorithm::HmacSha512, 256, 512),
        #[cfg(feature = "legacy-algorithms")]
        (SignatureAlgorithm::HmacMd5, 128, 128),
        #[cfg(feature = "legacy-algorithms")]
        (SignatureAlgorithm::HmacRipemd160, 128, 160),
    ] {
        let signing_policy = SigningPolicy {
            signature_algorithms: Some(std::collections::HashSet::from([algorithm])),
            ..SigningPolicy::default()
        };
        let mut verification_policy = VerificationPolicy {
            signature_algorithms: Some([algorithm].into()),
            ..VerificationPolicy::default()
        };
        verification_policy
            .key_trust
            .allowed_legacy_signature_algorithms
            .insert(algorithm);
        for output in [Some(minimum), Some(full), None] {
            let template = template(algorithm, output);
            let signed = SignContext::new(&signing_key)
                .policy(signing_policy.clone())
                .sign_template(&template)
                .unwrap();
            std::fs::write(&signed_path, signed).unwrap();
            let verified = xmlsec1::command()
                .arg("verify")
                .arg("--lax-key-search")
                .arg("--hmac-key")
                .arg(&key_path)
                .arg(&signed_path)
                .output()
                .unwrap();
            assert!(
                verified.status.success(),
                "{algorithm:?}/{output:?}: {}",
                String::from_utf8_lossy(&verified.stderr)
            );
            std::fs::write(&template_path, template).unwrap();
            let generated = xmlsec1::command()
                .arg("sign")
                .arg("--lax-key-search")
                .arg("--hmac-key")
                .arg(&key_path)
                .arg("--output")
                .arg(&oracle_path)
                .arg(&template_path)
                .output()
                .unwrap();
            assert!(
                generated.status.success(),
                "{algorithm:?}/{output:?}: {}",
                String::from_utf8_lossy(&generated.stderr)
            );
            let signed = std::fs::read_to_string(&oracle_path).unwrap();
            let result = VerifyContext::new()
                .policy(verification_policy.clone())
                .key(&verifying_key)
                .verify(&signed)
                .unwrap();
            assert_eq!(result.status, DsigStatus::Valid, "{algorithm:?}/{output:?}");
        }
    }
}

#[test]
fn unknown_signing_method_preserves_typed_parse_error() {
    // Early method validation must retain the public unsupported-algorithm error.
    let key = HmacSigningKey::new(vec![0x42; 32]).unwrap();
    let xml = template(SignatureAlgorithm::HmacSha256, None).replace(
        SignatureAlgorithm::HmacSha256.uri(),
        "urn:unsupported:signature",
    );
    assert!(matches!(
        SignContext::new(&key).sign_template(&xml),
        Err(SigningError::ParseSignedInfo(ParseError::UnsupportedAlgorithm { uri }))
            if uri == "urn:unsupported:signature"
    ));
}

#[test]
fn weak_hmac_output_does_not_query_the_signing_key() {
    // Policy refusal must precede even failed external/HSM key callbacks.
    struct UnavailableKey(std::cell::Cell<usize>);
    impl SigningKey for UnavailableKey {
        fn sign(&self, _: SignatureAlgorithm, _: &[u8]) -> Result<Vec<u8>, SigningKeyError> {
            panic!("policy-rejected input must not be signed")
        }

        fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
            self.0.set(self.0.get() + 1);
            Err(SigningKeyError::InvalidPublicKeyInfo)
        }
    }
    let key = UnavailableKey(std::cell::Cell::new(0));
    let xml = template(SignatureAlgorithm::HmacSha256, Some(120));
    assert!(matches!(
        SignContext::new(&key).sign_template(&xml),
        Err(SigningError::Policy(PolicyViolation::HmacOutputLength {
            actual: 120,
            ..
        }))
    ));
    assert_eq!(key.0.get(), 0);
}

#[test]
fn valid_hmac_template_queries_key_metadata_once() {
    // Parameter validation must not add a redundant external/HSM metadata call.
    struct CountedKey {
        key: HmacSigningKey,
        calls: std::cell::Cell<usize>,
    }
    impl SigningKey for CountedKey {
        fn sign(
            &self,
            algorithm: SignatureAlgorithm,
            bytes: &[u8],
        ) -> Result<Vec<u8>, SigningKeyError> {
            self.key.sign(algorithm, bytes)
        }

        fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
            self.calls.set(self.calls.get() + 1);
            self.key.public_key_info()
        }
    }
    let key = CountedKey {
        key: HmacSigningKey::new(vec![0x42; 32]).unwrap(),
        calls: std::cell::Cell::new(0),
    };
    let verifier = HmacVerificationKey::new(vec![0x42; 32]).unwrap();
    for output in [None, Some(128), Some(256)] {
        key.calls.set(0);
        let signed = SignContext::new(&key)
            .sign_template(&template(SignatureAlgorithm::HmacSha256, output))
            .unwrap();
        assert_eq!(key.calls.get(), 1);
        assert_eq!(
            VerifyContext::new()
                .key(&verifier)
                .verify(&signed)
                .unwrap()
                .status,
            DsigStatus::Valid
        );
    }
}

#[test]
fn weak_hmac_output_is_rejected_before_signing_reference_work() {
    // Signing must refuse the selected output before resolving a missing target.
    let xml = r##"<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"><HMACOutputLength>120</HMACOutputLength></SignatureMethod>
        <Reference URI="#missing"><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue/></Reference>
        </SignedInfo><SignatureValue/></Signature>"##;
    let key = HmacSigningKey::new(vec![0x42; 32]).unwrap();
    assert!(matches!(
        SignContext::new(&key).sign_template(xml),
        Err(SigningError::Policy(PolicyViolation::HmacOutputLength {
            actual: 120,
            ..
        }))
    ));
}

#[test]
fn weak_hmac_output_is_rejected_before_reference_work() {
    // A nonexistent target must not hide the policy refusal or cause reference
    // resolution before rejecting an attacker-selected weak authenticator.
    let xml = r##"<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"><HMACOutputLength>120</HMACOutputLength></SignatureMethod>
        <Reference URI="#missing"><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue></Reference>
        </SignedInfo><SignatureValue>AAAAAAAAAAAAAAAAAAAA</SignatureValue></Signature>"##;
    assert!(matches!(
        VerifyContext::new()
            .policy(VerificationPolicy::default())
            .verify(xml),
        Err(DsigError::Policy(PolicyViolation::HmacOutputLength {
            minimum: 128,
            maximum: 256,
            actual: 120
        }))
    ));
}