Skip to content
Closed
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: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
xml-backend: fat-runtime
cargo-args: --no-default-features --features xmldsig,xmlenc,c14n,xml-backends-all
- rust: stable
xml-backend: xmlenc-only
xml-backend: xmlenc-inventory
cargo-args: --no-default-features --features xmlenc,xml-backend-xmloxide
- rust: "1.92.0"
xml-backend: xmloxide
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ cbc = { version = "0.2.1", optional = true }
des = { version = "0.9", optional = true }
md-5 = { version = "0.11", optional = true }
pem = { version = "4", optional = true }
pkcs12 = { version = "=0.2.0-pre.0", default-features = false, features = ["kdf"], optional = true }
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"], optional = true }

# X.509 certificates
x509-parser = { version = "0.18", features = ["verify"], optional = true }
Expand Down Expand Up @@ -152,6 +154,10 @@ xmldsig = [ # XML Digital Signatures (sign + verify)
"dep:hmac",
"dep:md-5",
"dep:pem",
"dep:pkcs12",
"dep:pbkdf2",
"dep:aes",
"dep:cbc",
"dep:peresil",
"dep:p256",
"dep:p384",
Expand All @@ -171,6 +177,8 @@ xmldsig = [ # XML Digital Signatures (sign + verify)
]
xmlenc = [ # XML Encryption (encrypt + decrypt)
"std",
# The shared key inventory stores XMLDSig KeyInfo for named RSA recipients.
"xmldsig",
"dep:aes",
"dep:aes-gcm",
"dep:aes-kw",
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ xml-sec = { version = "0.1", default-features = false, features = ["xmldsig", "c
| XML signatures | XMLDSig signing and verification, RSA/DSA/ECDSA/HMAC, XPath transforms, `Manifest`, `KeyInfo`, and caller-provided references |
| XML encryption | AES-CBC/GCM, RSA-OAEP, AES Key Wrap, multiple recipients, and Element/Content replacement |
| X.509 | Certificate key extraction, chain validation, CRLs, and policy-controlled trust |
| 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 |
| Crypto | Provider-neutral contracts and opaque key handles with pure-Rust RustCrypto as the default implementation |
Expand All @@ -83,6 +84,8 @@ The signing and verification pipelines support same-document and caller-provided
XPath 1.0 and XPath Filter 2 transforms, `Manifest`, structured `KeyInfo`, and policy-controlled
X.509 validation. See [XML Digital Signatures](docs/xmldsig.md) for algorithms, transform semantics,
key resolution, failure handling, and current interoperability boundaries.
See [Key management](docs/key-management.md) for inventory ownership, format import,
password handling, and CLI key-store behavior.

## XML Encryption

Expand All @@ -104,6 +107,9 @@ fn example() -> Result<(), Box<dyn std::error::Error>> {
}
```

RSA encryption and decryption default to a 2048-bit minimum. Applications accepting legacy
keys must explicitly select a lower operation-policy minimum; importing a key does not bypass it.

See [XML Encryption](docs/xmlenc.md) for reciprocal decryption, key transport, recipient selection,
document replacement, and parser policy.

Expand Down
1 change: 0 additions & 1 deletion crates/xml-sec-xslt/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,6 @@ impl Document {
Ok(())
}

#[must_use]
pub fn nodes(&self) -> impl ExactSizeIterator<Item = (NodeId, &Node)> {
self.nodes
.iter()
Expand Down
10 changes: 10 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ headers, or DER structure select encrypted versus plain decoding first: a
supplied password is ignored for a plain key, while a missing or wrong password
for an encrypted key fails without a plaintext fallback, before output is
committed, and is never included in diagnostics.
`--keys-file FILE` imports a bounded xmlsec `keys.xml` store for sign, verify,
encrypt, and decrypt. Named HMAC/DSA signers, named HMAC/RSA/EC public
verification keys, direct AES content keys, and RSA-OAEP recipients are selected
from the same caller-owned inventory; an imported key never bypasses the
operation policy. `--pkcs12[:NAME] FILE --pwd PASSWORD` supplies one private
key and its certificate chain for sign or RSA decryption. Bundles with multiple
private keys are rejected rather than selecting an arbitrary bag. Neither
option triggers network lookup or implicit key discovery. See
[Key management](key-management.md) for the byte-oriented library API and trust
model.

Verification accepts `-` as the conventional stdin marker. Verification starts
at the document root and uses the first descendant `Signature` in document order.
Expand Down
210 changes: 210 additions & 0 deletions docs/key-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Key management

`xml_sec::key_manager::KeyInventory` is a caller-owned inventory of named key
material. The library imports bytes supplied by the caller; it never discovers
files, reads the environment, or fetches network resources. Applications keep
the inventory for as long as its keys are needed and pass the selected signer or
resolver to the normal XMLDSig/XMLEnc operation context. Import and execution
are both bounded by the operation's `ResourcePolicy` and cryptographic policy.
`KeyInventory::from_xml_bytes` accepts the same signing, verification,
encryption, or decryption policy snapshot used by the operation, so XML parser
allowances and resource limits cannot diverge. `decryption_resolver` also
requires the decryption snapshot and checks selected key material before
copying or decoding it. A permitted document `KeyName` may select a caller-owned
public key even when document-supplied key bytes are disabled; all sources in
the document's original `KeyInfo` remain subject to the source policy.
The `xmlenc` Cargo feature also enables `xmldsig`: the shared inventory uses
XMLDSig `KeyInfo` to represent named public recipients. Thus the inventory API
is available when an application selects `xmlenc` and an XML backend without
separately naming `xmldsig`.

The inventory accepts raw HMAC and AES secrets, public SPKI DER or PEM (also
PKCS#1 RSA public PEM), private PKCS#8 DER or PEM (including password-protected
PKCS#8), RSA PKCS#1 private DER or PEM, PKCS#12 bundles, DER X.509 certificates
and CRLs, and libxmlsec1 `keys.xml` bytes. The `keys.xml` importer recognizes
HMAC, AES, RSA, EC, and libxmlsec1's private DSA extension. DES key entries
are rejected because this build has no DES encryption operation. Unknown
algorithms in a mixed xmlsec key store are skipped; malformed supported entries
and ambiguous names fail. A PKCS#12 bundle with more than one private key is
rejected rather than assigning arbitrary aliases. A matching leaf certificate
is retained with its imported private key; byte-identical duplicate leaf bags
count as one certificate. Other certificates are retained as
untrusted chain material. `matching_certificate_chain()` returns a chain only
when its first certificate matches the private key; a CA-only PKCS#12 bundle
can still sign without emitting an unrelated signing certificate. A private
bundle's certificates do not become verification lookup candidates implicitly;
register a certificate explicitly for lookup or trust when needed.

Each imported key has an explicit `KeyUsages` set. For example, a key registered
for `Verify` cannot sign, and an `Encrypt`-only key cannot decrypt. Imported
public and PKCS#12 keys can be restricted at import with
`add_public_der_with_usages`, `add_public_pem_with_usages`, and
`add_pkcs12_with_usages`; the shorter methods authorize only operations
supported by that key family. An EC/DSA private key may sign but cannot be
assigned RSA decryption usage. Incompatible or empty usage sets are rejected.
EC and DSA public keys can verify but cannot be authorized as RSA encryption
recipients, including when imported from `keys.xml`. Imported certificates
are lookup candidates, **not trust anchors**, unless the caller
explicitly registers them as trusted. The operation's immutable policy still
decides algorithm acceptance, key minima, certificate validation, CRL checks,
and resource limits. An imported key is never permission to bypass that policy.
Caller-provided key names are bounded before import and charged to the retained
material budget for every stored copy, including public-key `KeyName` metadata.
Import and selection methods return `KeyStoreError::Policy` for operation-policy denials,
distinct from candidate-local `KeyStoreError::Selection` failures. Callers
must not retry another key after a policy rejection.
Certificate and CRL imports check candidate and aggregate capacity before DER
parsing, so an exhausted inventory returns a policy error even for malformed input.
An already-selected public entry can expose its RSA recipient key directly via
`StoredPublicKey::rsa_encryption_key(&encryption_policy)` without a second
inventory name lookup. The operation policy is required so source sizes are
checked before RSA decoding. Direct and XML key-store imports accept only
16-, 24-, or 32-byte AES keys; unsupported public-key algorithms are rejected
at import rather than acquiring verification permission.
Public DSA entries must contain independently usable parameters; the inventory
does not infer missing parameters from another entry. Verification validates the
complete policy snapshot before selecting or copying any key, including HMAC.
EC SPKI and certificate imports use the verifier's uncompressed SEC1 profile;
compressed points are rejected before granting verification usage. Each complete
KeyValue is one resource for selection limits, not one resource per component.
When a named certificate is selected, enabled CRL checking retains both inventory
and document CRLs, with their combined resource budget checked before copying.
Configured X.509 fallback resumes after previously inspected sources. If no key
resolves, it retains the first deferred key mismatch in source order; terminal
errors stop resolution immediately rather than becoming fallback candidates.
Embedded certificates and CRLs share one aggregate byte allowance with the
configured certificates and enabled CRLs before chain parsing or assembly.
RSA decryption selection checks borrowed public components before bigint
decoding. `DecryptionPolicy::rsa_keys` defaults to a 2048-bit minimum; the same
snapshot is enforced again before provider recovery, including opaque keys.
Applications accepting legacy input must explicitly lower this minimum; import
permission and a resolver selected under a weaker policy do not weaken a later operation.

`add_private_der_with_password_callback` asks the caller for a zeroizing byte
password only for encrypted PKCS#8; plaintext input does not invoke it.
Incompatible or empty private-key usages are rejected before requesting a password.
`add_pkcs12_with_password_callback` obtains a zeroizing string password before
decoding the bundle, after checking encoded size, visible bag/container counts,
and all visible MAC/encryption KDF parameters against one aggregate work budget.
KDF parameters inside encrypted SafeContents cannot be inspected without the
password: they are checked immediately after outer decryption, before running
the inner derivation (RFC 7292 sections 4.1 and 4.2.2). A missing
or wrong password returns a redacted error and never
triggers an unprotected fallback. PEM private-key labels must match their
payload: `ENCRYPTED PRIVATE KEY` cannot contain plaintext PKCS#8, and
`PRIVATE KEY` cannot contain an encrypted container. RFC 7468 section 2
permits reinterpretation, but this API deliberately forbids it to preserve
the protected-key contract. Similarly, `PUBLIC KEY` requires SubjectPublicKeyInfo
(RFC 7468 section 13); certificates are accepted through the certificate APIs
or generic DER import, not by reinterpreting a public-key PEM label.
Oversized encoded bundles return a typed
resource-policy error without invoking the callback.
`ResourcePolicy::max_key_import_kdf_work` and
`max_key_import_kdf_memory_bytes` can tighten protected-key derivation; both
are capped by implementation safety ceilings and checked before decryption.
Exceeding a recognized KDF's work or memory limit returns a policy error;
missing or incorrect passwords remain protected-container errors.
PBKDF2 work includes every output block required by the cipher's key width
(RFC 8018 section 5.2). Scrypt workspaces must fit both the KDF-specific ceiling
and the remaining aggregate allowance alongside retained inventory, key name,
and imported container; these checks precede password callbacks. The ciphertext-sized
PKCS#8 decryption buffer also counts toward the peak, even when decryption fails.
It is decrypted in place and retained without a second plaintext copy. For PEM imports,
the encoded input and decoded DER coexist and both count toward that peak.
PKCS#8 and PKCS#12 passwords count toward per-resource and aggregate live-byte limits before
derivation; callback buffers are charged by capacity, not just length. Work includes
one unit per 64 password bytes per HMAC initialization (two passes for scrypt)
in addition to the KDF's derivation work. Each PKCS#12 PBES2 derivation charges
password preprocessing to the shared budget, including encrypted nested bags;
legacy BMP password conversion consumes separate live memory when needed.
Ciphertext output capacity is checked before password derivation, including
after lazy BMP conversion, without allocating the output until decryption needs it.
Plaintext PKCS#8 imports still ignore unused passwords.
These limits are product policy, not format syntax.
The CLI applies the same pre-decryption KDF limits to explicit protected PKCS#8
PEM/DER keys, including the generic private-key options, as to inventory imports.
When the PKCS#12 parser rejects an oversized salt, that distinct resource
rejection also returns a typed policy error.
The importer uses RustCrypto primitives with borrowed BER views; it supports
PBES2/PBKDF2 with AES-CBC and legacy SHA-1/3DES containers, plus SHA-1/SHA-2 MACs.
Unsupported digest, PRF, KDF, and cipher algorithms return a selection error,
not a protected-container error; RC2 containers are not supported.
Private keys and decrypted temporary buffers are zeroized. Nested safe bags
share the same candidate and KDF budgets with ContentInfo records; a count
denial is not a password error. Lax CLI verification also shares one inspection
budget across all stored candidates and stops on a policy denial even after
another candidate resolved. Custom bag attributes accept BER high-tag-number
identifiers (X.690 section 8.1.2.4) without retaining their values.
Shared XMLDSig candidate accounting is constructed from the operation's
`VerificationPolicy`, not a separate caller-supplied numeric limit.
For multiple XML stores, use `XmlKeyStoreImporter::new(&policy, backend)`, call
`import(bytes)` for each source, then `finish()` to obtain the inventory. The CLI
uses this session for repeated `--keys-file`: candidate inspections and XML parsing
work share one operation allowance rather than resetting for each file. Failed
imports preserve existing keys but retain their work charges; retained material
from earlier files reduces capacity before decoding the next source.
BER PrivateKeyInfo framing and constructed private-key OCTET STRINGs are
normalized to bounded PKCS#8 DER before storage. CMS EncryptedData accepts
unprotected attributes with version 2, while requiring version 0 without them
(RFC 5652 section 8); metadata framing is validated before password processing.
The outer CMS attribute collection must be nonempty, but an unknown attribute's
generic `attrValues SET OF` has no minimum cardinality (RFC 5652 sections 5.3
and 6.1). Attribute-specific requirements are not inferred for unknown OIDs.
Temporary import allocations share the aggregate allowance with material already
retained by the inventory and the still-live caller-owned PFX input; KDF workspaces
are checked before derivation. AES-CBC IVs accept primitive and constructed BER
OCTET STRINGs, including nested and indefinite segmentation, without heap
flattening; their decoded length must still be exactly 16 bytes (X.690 section 8.7).
Canonical positive KDF iteration INTEGERs exceeding the work limit return typed
policy denials even when larger than a machine integer. Malformed INTEGER sign
encodings remain protected-container errors, not policy errors.
Named direct AES keys participate only in direct content-key resolution, not
in recipient-key unwrapping, so recipient hints cannot duplicate their candidate.

```rust
use xml_sec::key_manager::{KeyInventory, KeyUsages, SymmetricKeyKind};
use xml_sec::policy::ResourcePolicy;

let mut keys = KeyInventory::default();
keys.add_symmetric(
"signer".into(),
SymmetricKeyKind::Hmac,
b"caller-owned-secret".to_vec(),
KeyUsages::SIGN,
&ResourcePolicy::default(),
)?;
```

The CLI is the explicit file-I/O compatibility boundary. `xmlsec1
sign|verify|encrypt|decrypt --keys-file keys.xml` loads one or more bounded
xmlsec key stores. `sign` and `decrypt` also accept `--pkcs12[:NAME] file.p12
--pwd PASSWORD`; password handling happens before protected-key decoding, and
a wrong or missing password fails without a plaintext fallback. Key files are
not silently combined with conflicting explicit key options. `KeyName` in a
signature or encryption template selects the corresponding inventory entry;
distinct matches are ambiguous unless the caller explicitly requests the CLI's
compatibility search mode. The CLI uses the same signing, verification,
encryption, and decryption policy checks as direct key options. During
decryption, a named direct AES key from `--keys-file` can be selected even
when `EncryptedData` also contains an `EncryptedKey` recipient.
For multiple RSA encryption recipients, `--lax-key-search` prefers an exact
name and then tries remaining compatible entries in store order. Each selected
entry is consumed once for that operation; insufficient entries fail before
any encrypted output is written.
Stored RSA recipient policy denials are terminal even with `--lax-key-search`;
they are never downgraded to a candidate mismatch. Traditional encrypted RSA,
DSA, and EC PEM also fail terminally when CBC padding succeeds but the decoded
key is invalid, because padding does not authenticate the decrypted bytes.
Traditional EC PEM is decoded once before selecting a curve; all supported
curve decoders borrow the same zeroizing plaintext buffer.
Entries explicitly named by later recipient slots are reserved before assigning
fallbacks only when they match that slot's key metadata. An unnamed slot cannot
consume a later compatible exact match, but a stale name contradicted by metadata
does not reserve an incompatible key.
Reservation retains a decoded matching RSA candidate. Assignment moves that
candidate from the cache without decoding or charging it again; the candidate
work limit counts actual inspections, not reuse of an already inspected key.

For production applications, do not put passwords on a process command line:
load them through the application's secret channel and call the byte-oriented
library import API instead.
Loading
Loading