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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ 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-xades --lib --no-default-features
--features xml-backend-roxmltree
--target thumbv7em-none-eabihf -- -D warnings
- uses: taiki-e/install-action@nextest
- name: Target coverage on the build without std
run: |
Expand Down
14 changes: 5 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,7 @@ snafu = { version = "0.9", default-features = false, features = ["rust_1_81"], o

# Crypto
cryptoki = { version = "0.12.1", optional = true }
# Keep the published consumer graph on bindgen 0.72: the newer sys release's
# bindgen 0.73 can resolve incompatible syn/prettyplease major versions.
# The sys dependency only constrains resolution; provider calls use aws-lc-rs.
aws-lc-rs = { version = "=1.18.2", default-features = false, features = ["fips"], optional = true }
aws-lc-fips-sys = { version = "=0.14.3", default-features = false, 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 }
sha1 = { version = "0.11", features = ["oid", "zeroize"], optional = true }
sha2 = { version = "0.11", features = ["oid", "zeroize"], optional = true }
Expand Down Expand Up @@ -356,10 +352,10 @@ pkcs12 = { version = "=0.2.0-pre.0", default-features = false, features = ["kdf"
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"], optional = true }

# X.509 certificates
structured-asn1 = { version = "0.18.2", default-features = false, features = ["alloc"], optional = true }
structured-pkix = { version = "0.18.2", default-features = false, features = ["alloc"], optional = true }
structured-asn1 = { version = "0.20.0", default-features = false, features = ["alloc"], optional = true }
structured-pkix = { version = "0.20.0", default-features = false, features = ["alloc"], optional = true }
# PKCS#12 container framing; this crate keeps the password, KDF and cipher work.
structured-cms = { version = "0.18.2", default-features = false, features = ["alloc"], optional = true }
structured-cms = { version = "0.20.0", default-features = false, features = ["alloc"], optional = true }
tinyvec = { version = "1.13", optional = true }
# `pem` is RFC 7468 decoding of caller key and certificate text.
der = { version = "0.8", default-features = false, features = ["alloc", "pem"], optional = true }
Expand Down Expand Up @@ -427,7 +423,7 @@ std = [
"p384?/precomputed-tables",
"p521?/precomputed-tables",
]
aws-lc-fips = ["std", "xmldsig", "dep:aws-lc-rs", "dep:aws-lc-fips-sys"]
aws-lc-fips = ["std", "xmldsig", "dep:aws-lc-rs"]
pkcs11 = ["std", "xmlenc", "dep:cryptoki"]
xml-backend-xmloxide = []
xml-backend-roxmltree = ["dep:memchr"]
Expand Down
37 changes: 14 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ Pure Rust XML Security for **XMLDSig**, **XML Encryption**, **C14N**, **SAML 2.0
**No C dependencies in the default build. No CMake. No system XML or crypto packages.
Just `cargo add xml-sec`.**

> [!WARNING]
> Early-stage pre-release. The API is unstable, XMLDSig/XMLEnc coverage is still incomplete,
> and this crate should not yet be used in production.

## Why xml-sec?

- **End-to-end XML security**: sign, verify, encrypt, and decrypt through public Rust APIs.
Expand All @@ -27,7 +23,7 @@ Just `cargo add xml-sec`.**
Direct KEM-to-CBC additionally requires caller-owned external authentication; GCM is the safe default path.
- **SAML-ready primitives**: enveloped signatures, encrypted assertions, X.509 keys, XPath,
canonicalization, same-document XPointer references, DTD/`xml:id` IDs, and namespace-scoped ID registration.
- **Pure Rust deployment**: RustCrypto, `x509-parser`, and selectable Rust XML backends replace
- **Pure Rust deployment**: RustCrypto, shared structured PKIX/CMS primitives, and selectable Rust XML backends replace
the libxml2/OpenSSL-style native dependency stack.
- **Optional AWS-LC FIPS provider**: select a native cryptographic engine explicitly through
the same provider contracts, without silent fallback to RustCrypto.
Expand Down Expand Up @@ -69,13 +65,13 @@ declarations.
Verification only, with no private-key import, password derivation or randomness source:

```toml
xml-sec = { version = "0.2", default-features = false, features = ["xmldsig", "c14n", "xml-backend-xmloxide"] }
xml-sec = { version = "0.4", default-features = false, features = ["xmldsig", "c14n", "xml-backend-xmloxide"] }
```

Verification and signing:

```toml
xml-sec = { version = "0.2", default-features = false, features = ["xmldsig-sign", "c14n", "xml-backend-xmloxide"] }
xml-sec = { version = "0.4", default-features = false, features = ["xmldsig-sign", "c14n", "xml-backend-xmloxide"] }
```

See [feature and provider qualification](docs/features.md) for supported combinations,
Expand All @@ -95,7 +91,9 @@ a published dependency. C source and binary ABI compatibility are not delivered.
| Optional XSLT transform | `xslt`: the XMLDSig XSLT transform on the safe-Rust XSLT 1.0 engine, without external resources and within operation-wide XSLT limits |
| XML encryption | AES-CBC/GCM, RSA-OAEP, AES Key Wrap, opt-in Camellia-CBC/Key Wrap and experimental ChaCha20/Poly1305 profiles, ECDH/X25519/X448/DH, bounded KDFs, referenced ciphertext, nested recipients, and Element/Content replacement |
| Optional compatibility algorithms | `legacy-algorithms`: MD5/RIPEMD-160 signatures and digests, AES-192, TripleDES-CBC/CMS wrap, and RSA-1.5 transport; library policy denies these by default |
| X.509 | Certificate key extraction, chain validation, CRLs, required key authorization by default, and typed trust evidence |
| XAdES | `xml-sec-xades`, an in-repository workspace crate: B-B, B-T, B-LT and B-LTA validation of verified signatures, including detached, parallel and countersigned ones, over the document's own timestamps and validation material; caller-supplied anchors, TSA policy accuracy, OCSP responders and status; nothing fetched |
| CMS | `cms`: the selected provider behind RFC 3161 timestamp and CMS signature checks |
| X.509 | Certificate key extraction, chain validation, complete and delta CRLs, required key authorization by default, and typed trust evidence |
| Key management | Caller-owned named inventory, usage-restricted keys, xmlsec `keys.xml`, encrypted PKCS#8, and bounded RustCrypto-backed PKCS#12 import |
| SAML 2.0 | Signed assertions and encrypted-assertion workflows covered by integration tests |
| XML input | Strict bounded byte decoding, entity/depth/node limits, stable node identities, and generation-safe mutation |
Expand Down Expand Up @@ -178,7 +176,7 @@ Use `xml-backend-roxmltree` for a thin alternative build, or compile `xml-backen
`Xmloxide`, `Roxmltree`, or fail-closed `Differential` parsing per operation:

```toml
xml-sec = { version = "0.2", default-features = false, features = ["xmldsig", "xmlenc", "c14n", "xml-backends-all"] }
xml-sec = { version = "0.4", default-features = false, features = ["xmldsig", "xmlenc", "c14n", "xml-backends-all"] }
```

```rust
Expand All @@ -202,17 +200,6 @@ metadata. UTF-8 remains borrowed when possible; other supported encodings are st
under the same materialization ceiling. Conflicting declarations, malformed byte sequences,
ambiguous BOM-less UTF-16/UTF-32, and unsupported EBCDIC variants fail explicitly.

**API migration for this pre-release change:** `encoding::decode_xml_octets` now requires a
maximum decoded-byte count as its second argument. `encoding::XmlEncodingError` is now the
shared, non-exhaustive `xml_sec::encoding::XmlEncodingError`; update matches to handle the new error
variants and include a fallback arm. Code constructing `ResourcePolicy` with every field must
also set `max_xml_namespace_bindings` and the `max_xslt_*` limits (or start from
`ResourcePolicy::default()` and override selected fields); matches on `xmldsig::Transform`
need a fallback arm. These changes keep decoding and namespace-scope allocation under explicit
resource limits. For trusted resolver-provided encoding metadata, use
`xml_input::decode_xml_bounded(bytes, Some(encoding), maximum_decoded_bytes)`; the unbounded
decoder remains internal.

## Native xmlsec1 CLI

```sh
Expand Down Expand Up @@ -249,7 +236,9 @@ surface and operation behavior with source and test evidence.
The [local XAdES interoperability corpus](interop/xades/README.md) compares exact Reference
and SignedInfo bytes with native libxmlsec1 and checks independent EU DSS profile expectations.
Its synthetic fixtures and reproducible commands distinguish transform/XMLDSig interoperability,
XAdES property binding, expected-object coverage, and trust-validation limits.
XAdES property binding, expected-object coverage, and trust-validation limits. Baseline levels
are compared with EU DSS conclusions on fixtures issued by OpenSSL, libxml2 and xmlsec1, and
checked on signatures DSS itself produced.

## Safe-Rust XSLT

Expand All @@ -262,8 +251,8 @@ request host-clock compatibility.
Resolver authorization also gates cached documents and retained stylesheet dependencies, with
typed expiry/revocation failures that cannot be hidden by resource fallback.

The engine remains an in-repository workspace crate rather than a separately published package;
`xml-sec` is the only crate published to crates.io. Its optional `xslt` feature compiles the same
The engine remains an in-repository workspace crate rather than a separately published package,
as does `xml-sec-xades`; `xml-sec` is the only crate published to crates.io. Its optional `xslt` feature compiles the same
engine sources and executes the XMLDSig XSLT transform with them; see
[XML Digital Signatures](docs/xmldsig.md#xslt-transform). The shared strict byte-decoding and
lexical source is compiled into `xml-sec` and reused by the internal XSLT workspace crate.
Expand All @@ -278,6 +267,8 @@ lexical source is compiled into `xml-sec` and reused by the internal XSLT worksp
| [XMLDSig 1.0/1.1](https://www.w3.org/TR/xmldsig-core1/) | Core sign/verify pipelines; complete Merlin, Phaos 3, XMLDSig 1.1, and Second Edition corpora classified and executed |
| [XML Encryption](https://www.w3.org/TR/xmlenc-core1/) | Core AES-CBC/GCM, RSA-OAEP, and AES-KW pipelines implemented; broader conformance work continues |
| [XSLT 1.0](https://www.w3.org/TR/xslt-10/) | Safe-Rust engine implemented; the XMLDSig XSLT transform executes with the optional `xslt` feature |
| [ETSI EN 319 132-1](https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf) (XAdES) | Baseline levels B-B to B-LTA validated through `xml-sec-xades`; signature creation is not provided |
| [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) | Timestamp tokens checked inside XAdES validation; no client or TSA |

The project tracks stable Rust and supports Rust 1.92 or newer. Detailed contracts and limitations
live in the [XMLDSig guide](docs/xmldsig.md), [XML Encryption guide](docs/xmlenc.md), and
Expand Down
12 changes: 6 additions & 6 deletions benchmarks/competitors/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 34 additions & 3 deletions crates/xml-sec-config/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use xml_sec::{
KeyEstablishmentPolicy, KeySourceKind, KeySourcePolicy, KeyTrustPolicy, ManifestProcessing,
OpcRelationshipEdition, PolicySet, PolicyValidity, ResourcePolicy, RsaKeyPolicy,
RsaPssPermission, SameDocumentIdSemantics, SignatureConstraints, SigningPolicy,
TransformPolicy, UriPolicy, VerificationPolicy, VerificationTrustMode, XmlInputPolicy,
XsltStylesheetSemantics,
TimestampTokenEncoding, TransformPolicy, UriPolicy, VerificationPolicy,
VerificationTrustMode, XmlInputPolicy, XsltStylesheetSemantics,
},
provider::{KeyEncapsulationAlgorithm, X509SignatureAlgorithm},
xmldsig::{
Expand Down Expand Up @@ -527,6 +527,14 @@ fn verification(p: &mut VerificationPolicy, t: &Table, syntax_only: bool) -> Res
DetachedDigestAttestation::Accepted
);
}
"timestamp_token_encoding" => {
p.timestamp_token_encoding = choice!(
value,
key,
TimestampTokenEncoding::Declared,
TimestampTokenEncoding::Ber
);
}
_ => signature_fields!(p, key, value),
}
}
Expand Down Expand Up @@ -1052,6 +1060,7 @@ fn verification_snapshot(p: &VerificationPolicy) -> Result<Value, Error> {
transforms: _,
manifest_processing: _,
detached_digest_attestation: _,
timestamp_token_encoding: _,
xml: _,
resources: _,
} = p;
Expand All @@ -1062,6 +1071,10 @@ fn verification_snapshot(p: &VerificationPolicy) -> Result<Value, Error> {
"detached_digest_attestation".into(),
named(p.detached_digest_attestation),
);
t.insert(
"timestamp_token_encoding".into(),
named(p.timestamp_token_encoding),
);
Ok(Value::Table(t))
}

Expand Down Expand Up @@ -1347,7 +1360,8 @@ fn direction(field: &str, old: Option<&Value>, new: Option<&Value>) -> Direction
| "Process"
| "RequireAuthenticatedCipher"
| "ExactOnly"
| "Forbidden",
| "Forbidden"
| "Declared",
),
)
| (Some("AllSupported"), Some("SecureSupported")) => Restrictive,
Expand Down Expand Up @@ -1566,4 +1580,21 @@ mod tests {
Direction::Relaxing
);
}

#[test]
fn holding_timestamp_tokens_to_their_declared_encoding_is_restrictive() {
// Composition treats `Declared` as the stricter reading; a rollout
// report must not call the same change a relaxation.
let field = "verification.timestamp_token_encoding";
let declared = Value::String("Declared".into());
let ber = Value::String("Ber".into());
assert_eq!(
direction(field, Some(&ber), Some(&declared)),
Direction::Restrictive
);
assert_eq!(
direction(field, Some(&declared), Some(&ber)),
Direction::Relaxing
);
}
}
21 changes: 21 additions & 0 deletions crates/xml-sec-config/tests/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,27 @@ fn detached_payload_settings_compose_restrictively() {
);
}

#[test]
fn a_profile_overlay_cannot_relax_the_timestamp_token_encoding() {
// RFC 5652 BER drops the EN 319 132-1 §5.1.3 assertion, so an overlay may
// not choose it over a base that holds tokens to their declared encoding.
use xml_sec::policy::TimestampTokenEncoding;
let relaxed = effective(
&config("[profiles.team.verification]\ntimestamp_token_encoding='Ber'"),
"team",
);
assert_eq!(
relaxed
.compiled()
.verification()
.unwrap()
.timestamp_token_encoding,
TimestampTokenEncoding::Declared
);
let output = relaxed.to_toml().unwrap();
assert!(output.contains("timestamp_token_encoding = \"Declared\""));
}

#[test]
fn bounded_readers_and_paths_admit_bytes_before_parsing() {
// The exact boundary passes, one extra byte fails without heap growth for
Expand Down
28 changes: 20 additions & 8 deletions crates/xml-sec-xades/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,28 @@ publish = false
description = "XAdES baseline validation of signatures verified by xml-sec"

[dependencies]
xml-sec = { path = "../..", default-features = false, features = ["std", "xmldsig", "c14n"] }
structured-xades = { version = "0.18.0", default-features = false, features = ["std"] }
structured-asn1 = { version = "0.18.0", default-features = false, features = ["std"] }
structured-pkix = { version = "0.18.0", default-features = false, features = ["std"] }
structured-cms = { version = "0.18.0", default-features = false, features = ["std"] }
structured-ltv = { version = "0.18.0", default-features = false, features = ["std"] }
thiserror = "2"
xml-sec = { path = "../..", default-features = false, features = ["xmldsig", "c14n", "cms"] }
structured-xades = { version = "0.20.0", default-features = false, features = ["alloc"] }
structured-asn1 = { version = "0.20.0", default-features = false, features = ["alloc"] }
structured-pkix = { version = "0.20.0", default-features = false, features = ["alloc"] }
structured-cms = { version = "0.20.0", default-features = false, features = ["alloc"] }
structured-ltv = { version = "0.20.0", default-features = false, features = ["alloc"] }
thiserror = { version = "2", default-features = false }

[dev-dependencies]
base64 = "0.22"

[features]
default = ["xml-backend-xmloxide"]
default = ["std", "xml-backend-xmloxide"]
std = [
"xml-sec/std",
"structured-xades/std",
"structured-asn1/std",
"structured-pkix/std",
"structured-cms/std",
"structured-ltv/std",
"thiserror/std",
]
xml-backend-xmloxide = ["xml-sec/xml-backend-xmloxide"]
xml-backend-roxmltree = ["xml-sec/xml-backend-roxmltree"]

Expand Down
Loading
Loading