Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
echo "OPENSSL_BIN=$XMLSEC1_PREFIX/bin/openssl" >> "$GITHUB_ENV"
- name: Execute every workspace target and enforce canonical coverage
run: cargo run --quiet -p xml-sec-tools -- hardening coverage "$RUNNER_TEMP/coverage"
- name: Build the packaged crate and run every test from its archive
run: cargo run --quiet -p xml-sec-tools -- hardening package "$RUNNER_TEMP/package"
- uses: actions/upload-artifact@v7
if: always()
with:
Expand Down
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ include = [
"/benchmarks/dashboard/**",
"/benchmarks/Dockerfile",
"/tests/**",
"/tools/xmlsec1/**",
# These read sibling packages, which Cargo never packages, or the donor
# checkout; they run in the workspace.
"!/tests/capability_ledger.rs",
"!/tests/donor_negative_vectors.rs",
"!/tests/opc_relationship_transform.rs",
"!/tests/xmlsec1_donor_interop.rs",
"/compatibility/**",
"/scripts/**",
]
Expand Down Expand Up @@ -222,6 +227,11 @@ name = "xmlsec1_interop"
path = "tests/xmlsec1_interop.rs"
required-features = ["xmldsig-sign", "c14n"]

[[test]]
name = "xmlsec1_donor_interop"
path = "tests/xmlsec1_donor_interop.rs"
required-features = ["xmldsig-sign", "c14n"]

[[test]]
name = "signing_digest"
required-features = ["xmldsig-sign", "c14n"]
Expand Down
4 changes: 2 additions & 2 deletions compatibility/libxmlsec1-1.3.13-behavior-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"schema_version": 1,
"evidence": {
"asn1-signatures": {
"positive": [{"file":"tests/xmlsec1_interop.rs","test":"asn1_ecdsa_compatibility_matches_pinned_xmlsec1","description":"Pinned donor and native verification accept the upstream DER fixture only in explicit compatibility mode."},{"file":"tests/signing_digest.rs","test":"ecdsa_asn1_compatibility_is_explicit_across_the_signing_pipeline","description":"Native signing emits ASN.1 DER only under explicit policy and verifies the result under the matching policy."},{"file":"tools/xmlsec1/tests/process_contract.rs","test":"cli_asn1_signature_mode_requires_the_explicit_donor_flag","description":"The native CLI flag controls both ASN.1 signing output and verification acceptance."}],
"negative": [{"file":"tests/xmlsec1_interop.rs","test":"asn1_ecdsa_compatibility_matches_pinned_xmlsec1","description":"Pinned donor and native standards defaults both reject the upstream DER fixture."},{"file":"tests/signing_digest.rs","test":"ecdsa_asn1_compatibility_is_explicit_across_the_signing_pipeline","description":"Standards-default native verification rejects DER emitted by compatibility-mode signing."},{"file":"tools/xmlsec1/tests/process_contract.rs","test":"cli_asn1_signature_mode_requires_the_explicit_donor_flag","description":"CLI verification without the compatibility flag rejects ASN.1 DER SignatureValue framing."}]
"positive": [{"file":"tests/xmlsec1_donor_interop.rs","test":"asn1_ecdsa_compatibility_matches_pinned_xmlsec1","description":"Pinned donor and native verification accept the upstream DER fixture only in explicit compatibility mode."},{"file":"tests/signing_digest.rs","test":"ecdsa_asn1_compatibility_is_explicit_across_the_signing_pipeline","description":"Native signing emits ASN.1 DER only under explicit policy and verifies the result under the matching policy."},{"file":"tools/xmlsec1/tests/process_contract.rs","test":"cli_asn1_signature_mode_requires_the_explicit_donor_flag","description":"The native CLI flag controls both ASN.1 signing output and verification acceptance."}],
"negative": [{"file":"tests/xmlsec1_donor_interop.rs","test":"asn1_ecdsa_compatibility_matches_pinned_xmlsec1","description":"Pinned donor and native standards defaults both reject the upstream DER fixture."},{"file":"tests/signing_digest.rs","test":"ecdsa_asn1_compatibility_is_explicit_across_the_signing_pipeline","description":"Standards-default native verification rejects DER emitted by compatibility-mode signing."},{"file":"tools/xmlsec1/tests/process_contract.rs","test":"cli_asn1_signature_mode_requires_the_explicit_donor_flag","description":"CLI verification without the compatibility flag rejects ASN.1 DER SignatureValue framing."}]
},
"c-boundary-state": {
"positive": [{"file":"tests/behavioral_compatibility.rs","test":"pinned_c_oracle_reports_context_callback_and_dom_contracts","description":"A compiled probe measures successful and invalid context state, retained references, pre-execution callback order, userData propagation, and in-place signing mutation against pinned libxmlsec1."},{"file":"tests/behavioral_compatibility.rs","test":"c_boundary_behaviors_are_explicitly_planned","description":"Measured donor-only context, callback, and in-place mutation contracts are named compatibility work."}],
Expand Down
4 changes: 2 additions & 2 deletions compatibility/libxmlsec1-1.3.13-behavior.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"asn1-signatures": {
"positive": [
{
"file": "tests/xmlsec1_interop.rs",
"file": "tests/xmlsec1_donor_interop.rs",
"test": "asn1_ecdsa_compatibility_matches_pinned_xmlsec1",
"description": "Pinned donor and native verification accept the upstream DER fixture only in explicit compatibility mode."
},
Expand All @@ -28,7 +28,7 @@
],
"negative": [
{
"file": "tests/xmlsec1_interop.rs",
"file": "tests/xmlsec1_donor_interop.rs",
"test": "asn1_ecdsa_compatibility_matches_pinned_xmlsec1",
"description": "Pinned donor and native standards defaults both reject the upstream DER fixture."
},
Expand Down
14 changes: 14 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
yanked = "deny"
ignore = []

[licenses]
# One list serves the production and fuzz graphs, which use different licences.
unused-allowed-license = "allow"
allow = [
"Apache-2.0",
"BSD-3-Clause",
"ISC",
"MIT",
"NCSA",
"OpenSSL",
"Unicode-3.0",
"Zlib",
]

[sources]
unknown-registry = "deny"
unknown-git = "deny"
Expand Down
17 changes: 15 additions & 2 deletions docs/ci-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,27 @@ cargo nextest run -p xml-sec-tools
cargo-deny 0.19.0 checks both the production and fuzz graphs, including optional
features and development dependencies. Vulnerable, unsound and notice advisories
fail under the tool's policy; yanked crates and unapproved git/registry sources
are refused. There is no blanket ignore list. Refresh an ignored local lockfile
when it retains a yanked dependency; do not weaken the gate to accept that lock.
are refused. Every crate must satisfy the licence allow-list in `deny.toml`.
There is no blanket ignore list. Refresh an ignored local lockfile when it
retains a yanked dependency; do not weaken the gate to accept that lock.

The native safety gate lints all workspace library targets with unsafe-code
refusal. Test/benchmark diagnostic allocator probes are not product library
permission to use unsafe. Native dependencies retain their separate upstream
safety and FIPS/module boundaries; this check is not an audit of external code.

## Packaged Crate

```sh
PKCS11_MODULE=/path/to/libsofthsm2.so \
cargo run -p xml-sec-tools -- hardening package /new/directory/outside/the/repository
```

The phase packages `xml-sec` with all features and runs every test target from
the unpacked archive, with the xmlsec1 oracle and a disposable token. A test that
reads a sibling package or the donor checkout cannot pass there, so it stays out
of the package through `include` in the root manifest and runs in the workspace.

## Pedantic Lint

`clippy::pedantic` is part of the workspace lint table in the root
Expand Down
1 change: 1 addition & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "xml-sec-fuzz"
version = "0.0.0"
license = "Apache-2.0 AND MIT"
publish = false
edition = "2024"

Expand Down
69 changes: 69 additions & 0 deletions tests/xmlsec1_donor_interop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Interoperability checks that read the donor checkout, which is not packaged.

use std::fs;
use std::path::Path;

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

use xml_sec::policy::{EcdsaSignatureValueEncoding, VerificationPolicy};
use xml_sec::xmldsig::{DsigStatus, SignatureAlgorithm, VerificationKey, VerifyContext};

#[test]
fn asn1_ecdsa_compatibility_matches_pinned_xmlsec1() {
// The upstream vector is intentionally invalid under XMLDSig framing and
// valid only when both implementations select libxmlsec1's ASN.1 mode.
if !xmlsec1::is_available() {
eprintln!("{}", xmlsec1::skip_reason());
return;
}
let fixture = Path::new(
"donors/xmlsec/tests/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256-with-asn1.xml",
);
let public_key = Path::new("donors/xmlsec/tests/keys/ec/ec-prime256v1-pubkey.pem");
let donor_key_option = "--pubkey-pem:TestKeyName-ec-prime256v1";

let donor_default = xmlsec1::command()
.args(["--verify", donor_key_option])
.arg(public_key)
.arg(fixture)
.output()
.expect("pinned xmlsec1 must run");
assert!(!donor_default.status.success());
let donor_compatible = xmlsec1::command()
.args([
"--verify",
"--enable-asn1-signatures-hack",
donor_key_option,
])
.arg(public_key)
.arg(fixture)
.output()
.expect("pinned xmlsec1 must run");
assert!(
donor_compatible.status.success(),
"{}",
String::from_utf8_lossy(&donor_compatible.stderr)
);

let xml = fs::read_to_string(fixture).expect("upstream ASN.1 fixture must load");
let (_, pem) = x509_parser::pem::parse_x509_pem(&fs::read(public_key).unwrap())
.expect("upstream EC public key must parse");
let key = VerificationKey {
algorithm: SignatureAlgorithm::EcdsaSha256,
public_key_bytes: pem.contents,
certificate_der: None,
name: Some("TestKeyName-ec-prime256v1".into()),
};
assert!(VerifyContext::new().key(&key).verify(&xml).is_err());
let policy = VerificationPolicy {
ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding::XmlSecAsn1Der,
..VerificationPolicy::default()
};
let native = VerifyContext::new()
.key(&key)
.policy(policy)
.verify(&xml)
.expect("native ASN.1 compatibility verification must run");
assert_eq!(native.status, DsigStatus::Valid);
}
62 changes: 1 addition & 61 deletions tests/xmlsec1_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ mod cryptographic;

use xml_sec::c14n::{C14nAlgorithm, C14nMode};
use xml_sec::policy::{
EcdsaSignatureValueEncoding, SameDocumentIdSemantics, SigningPolicy, TransformPolicy,
VerificationPolicy,
SameDocumentIdSemantics, SigningPolicy, TransformPolicy, VerificationPolicy,
};
use xml_sec::xmldsig::{
DefaultKeyResolver, DigestAlgorithm, DsaSigningKey, DsigStatus, EcdsaP256SigningKey,
Expand Down Expand Up @@ -194,65 +193,6 @@ fn xmlsec1_version_gate_requires_pinned_snapshot() {
}
}

#[test]
fn asn1_ecdsa_compatibility_matches_pinned_xmlsec1() {
// The upstream vector is intentionally invalid under XMLDSig framing and
// valid only when both implementations select libxmlsec1's ASN.1 mode.
if !xmlsec1::is_available() {
eprintln!("{}", xmlsec1::skip_reason());
return;
}
let fixture = Path::new(
"donors/xmlsec/tests/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256-with-asn1.xml",
);
let public_key = Path::new("donors/xmlsec/tests/keys/ec/ec-prime256v1-pubkey.pem");
let donor_key_option = "--pubkey-pem:TestKeyName-ec-prime256v1";

let donor_default = xmlsec1::command()
.args(["--verify", donor_key_option])
.arg(public_key)
.arg(fixture)
.output()
.expect("pinned xmlsec1 must run");
assert!(!donor_default.status.success());
let donor_compatible = xmlsec1::command()
.args([
"--verify",
"--enable-asn1-signatures-hack",
donor_key_option,
])
.arg(public_key)
.arg(fixture)
.output()
.expect("pinned xmlsec1 must run");
assert!(
donor_compatible.status.success(),
"{}",
String::from_utf8_lossy(&donor_compatible.stderr)
);

let xml = fs::read_to_string(fixture).expect("upstream ASN.1 fixture must load");
let (_, pem) = x509_parser::pem::parse_x509_pem(&fs::read(public_key).unwrap())
.expect("upstream EC public key must parse");
let key = VerificationKey {
algorithm: SignatureAlgorithm::EcdsaSha256,
public_key_bytes: pem.contents,
certificate_der: None,
name: Some("TestKeyName-ec-prime256v1".into()),
};
assert!(VerifyContext::new().key(&key).verify(&xml).is_err());
let policy = VerificationPolicy {
ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding::XmlSecAsn1Der,
..VerificationPolicy::default()
};
let native = VerifyContext::new()
.key(&key)
.policy(policy)
.verify(&xml)
.expect("native ASN.1 compatibility verification must run");
assert_eq!(native.status, DsigStatus::Valid);
}

#[test]
fn visa3d_direct_id_compatibility_matches_pinned_xmlsec1() {
// An apostrophe breaks libxmlsec1's default xpointer(id('...')) wrapper.
Expand Down
1 change: 1 addition & 0 deletions tools/capability-ledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "xml-sec-capability-ledger"
version = "0.0.0"
edition = "2024"
license = "Apache-2.0 AND MIT"
publish = false

[dependencies]
Expand Down
1 change: 1 addition & 0 deletions tools/xml-sec-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "xml-sec-tools"
version = "0.0.0"
edition = "2024"
license = "Apache-2.0 AND MIT"
publish = false

[dependencies]
Expand Down
Loading
Loading