xml-sec 0.1.23

Pure Rust XML Security: XMLDSig, XMLEnc, C14N. Drop-in replacement for libxmlsec1.
Documentation
# 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, public RSA/EC, and libxmlsec1's private DSA extension. RSA private
keys are imported through the PEM/DER or PKCS#12 APIs, not `RSAKeyValue`.
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.
XML inventories also accept public SPKI in `dsig11:DEREncodedKeyValue`
([XMLDSig 1.1 §4.5.9](https://www.w3.org/TR/xmldsig-core1/#sec-DEREncodedKeyValue)).
With `experimental-pq`, ML-KEM SPKI follows RFC 9935 §4 and receives encryption
usage only. The element never carries private PKCS#8; private ML-KEM material
uses the existing PEM/DER or PKCS#12 import APIs and RFC 9935 §6 encoding.

With `experimental-pq`, automatic PKCS#12 import gives ML-KEM private keys
only `Decrypt` usage for decapsulation; explicitly requesting `Sign` is 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.
Named entries explicitly authorized for verification are caller-trusted keys;
lookup certificates remain candidates until an exact caller pin or certificate
path establishes authorization. The default `RequireTrustedKey` mode enforces
this distinction before signature verification. `CryptographicOnly` is an explicit
policy choice for mathematical checks and reports no signer trust. See
[XMLDSig key authorization](xmldsig.md) for result evidence and custom resolver integration.
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.
RSA private-key ingestion checks borrowed PKCS#1 components before constructing
big integers in every CLI container path, including plaintext PKCS#8 and
traditional PEM after decryption. Component safety failures are terminal during
lax key search. Adapters can reuse `preflight_rsa_pkcs1_components` without
creating an inventory or decoding the native key twice.
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.
Before constructing its owned resolver configuration, fallback accounts for both
the original configured certificates/enabled CRLs and their simultaneously live
copies, including document CRLs attached to a selected named certificate.
Embedded certificates and CRLs share one aggregate byte allowance with the
configured certificates and enabled CRLs before chain parsing or assembly.

Certificate paths, distinguished names and CRLs use the published
`structured-asn1` / `structured-pkix` mechanisms. Original DER and signed
ranges remain borrowed; one operation account covers ingestion, recursive
key lookup, path search, static validation and revocation. Displayed names or
phase-local metadata never establish certificate identity. Effective inherited
issuer parameters are projected from the authenticated path without rewriting
certificates or repeating their signature checks.
Both the core and native CLI use the published 0.3 API family (minimum 0.3.0).
Applications supplying public PKIX types such as `LeapTable` must use the same
compatible family; types from older 0.1 or 0.2 packages are not interchangeable.
GeneralName comparisons use the shared semantic core, including its accounting;
explicit CRL distribution-point and issuer fields retain the encoded-identity
rules required by RFC 5280 sections 5.2.5 and 4.2.1.13. Semantic URI/DNS
normalization does not override those encoded-identity requirements.

When CRL checking is enabled, supply authenticated UTC history with
`VerifyContext::utc_facts(LeapTable)` or `X509ChainOptions::utc_facts` for direct
chain validation. Standalone budget-aware resolvers accept the same fact through
`InspectedKeyCandidateBudget::new_with_utc_facts`. The core does not obtain a
table from the host, network or a built-in default. Missing facts return
`MissingUtcFacts`; insufficient or contradictory coverage returns
`CrlTimeUncertain`, never non-revocation. Cached complete CRLs permit the exact
`nextUpdate` boundary, but even one nanosecond later fails freshness. Revocation
dates are checked against the same lossless operation time. Unsupported critical
demands, entry conflicts and unpaired delta CRLs cannot authorize a key.
Applicable partial lists must collectively cover all revocation reasons;
otherwise `IncompleteCrlCoverage` is returned. This checks supplied evidence,
not an implicit online revocation fetch or a requirement to supply a CRL for
every path certificate.

The CLI accepts `verify --verify-crls --utc-facts leap-seconds.list` using the
published IERS/IANA NTP-timestamp and TAI-UTC-offset format. The first entry
starts coverage and `#@` ends it; offsets become relative corrections. The
caller must establish the file's authenticity and completeness. Loading it
does not authenticate its source, download updates or extend expired coverage.

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.
Public and private PEM imports preflight retained inventory plus simultaneously
live encoded input and decoded DER before allocating the decoded buffer.
PKCS#1 RSA public imports also preflight the final SPKI output
before wrapping borrowed key octets; decoding needs no normalized Base64 string.
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.
KDF work accumulates across imports in one inventory, including failed decrypts.
Encrypted PKCS#8 preflight uses the remaining work allowance before invoking a
password callback; an exhausted allowance cannot prompt for another secret.
The CLI carries that usage across temporary signing and decryption candidates,
so lax search cannot multiply the operation allowance. Temporary inventories
are released after extracting the candidate; accounting does not retain extra keys.
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.
Plaintext imports also preflight the retained DER copy alongside the still-live
input and decoded PEM. PKCS#1 normalization wraps borrowed octets directly in
one PKCS#8 output rather than re-encoding big integers into intermediate buffers.
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.
The legacy PKCS#12 MAC uses RFC 7292 B.1 BMPString password formatting even
when encryption uses PBES2. Supplementary Unicode characters therefore work
only when no legacy MAC or encryption KDF requires BMPString. UTF-16 surrogate
pairs used by some implementations are not accepted as BMPString; RFC 9879
section 6 separately specifies UTF-8 for PBMAC1, which is not implemented here.
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.
Core verification retains one `InspectedKeyCandidateBudget` across signatures,
including unsuccessful key attempts. `DefaultKeyResolver` and inventory-backed
resolvers use `KeyResolver::resolve_for_verification_with_budget`; their
certificate execution account is shared with direct and recursive KeyInfo
ingestion. Custom resolvers override that hook to charge each actual inspection
and use `budget.execution()` for common certificate mechanisms. The default
hook accounts for one custom-resolver dispatch; the application remains
responsible for any additional work its callback performs.
Certificate name selectors and supplied-path validation use the shared PKIX
mechanisms. Discovery compares original ASN.1 Names, not subject/issuer display
strings. Unknown name equality syntax is reported as unsupported rather than
guessed from its bytes. Display metadata is not a certificate identity.
When a validated path inherits public-key parameters (RFC 5280 section 6.1.4(e)),
the effective key reaches the XML signature verifier without rewriting the
certificate's signed bytes. Verification still uses the selected `CryptoProvider`.
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.
XML imports reserve the source together with decoded components, cloned names,
and private-key encoding workspace, rather than using the larger of source and
retained material. XML Base64 decoding borrows text nodes and uses one exact-size
output buffer without normalized text copies. Embedded X509Data binary values
obey the active per-resource and aggregate byte limits before decoding; repeated
KeyInfo parses in one session retain that charge, including failed candidates.
XML import sessions can also account for material retained outside the inventory;
the CLI seeds this usage from its shared certificate/file ledger before loading
`--keys-file`, so live certificate buffers cannot reuse the key-import allowance.
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.