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
3 changes: 3 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 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
140 changes: 140 additions & 0 deletions docs/key-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# 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.
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.
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.

`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.
`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. 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.
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.
Private keys and decrypted temporary buffers are zeroized. Nested safe bags
share the same candidate and KDF budgets; a count denial is not a password error.
Temporary import allocations share the aggregate allowance with material already
retained by the inventory, and KDF workspaces are checked before derivation.
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.
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.
23 changes: 15 additions & 8 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! generation atomically, so identities from an older generation cannot be
//! confused with nodes in the new tree.

use std::borrow::Cow;
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
use std::cell::Cell;
use std::collections::{HashMap, HashSet, hash_map::Entry};
Expand Down Expand Up @@ -3303,6 +3304,14 @@ fn decode_owned_xml(
maximum: usize,
budget: Option<&XmlParseWorkBudget>,
) -> Result<String, XmlDocumentError> {
decode_xml_with_budget(bytes, maximum, budget).map(Cow::into_owned)
}

pub(crate) fn decode_xml_with_budget<'a>(
bytes: &'a [u8],
maximum: usize,
budget: Option<&XmlParseWorkBudget>,
) -> Result<Cow<'a, str>, XmlDocumentError> {
if bytes.len() > maximum {
return Err(XmlDocumentError::DocumentTooLarge {
maximum,
Expand All @@ -3313,14 +3322,12 @@ fn decode_owned_xml(
// Charge it before encoding detection/transcoding and retain that charge
// in the same sticky budget used by preflight and semantic construction.
charge_parse_work(budget, bytes.len())?;
xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum)
.map(|xml| xml.into_owned())
.map_err(|error| match error {
xml_sec_xml_input::Error::DecodedLimit { actual, .. } => {
XmlDocumentError::DocumentTooLarge { maximum, actual }
}
error => XmlDocumentError::Encoding(error),
})
xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum).map_err(|error| match error {
xml_sec_xml_input::Error::DecodedLimit { actual, .. } => {
XmlDocumentError::DocumentTooLarge { maximum, actual }
}
error => XmlDocumentError::Encoding(error),
})
}

fn allocate_document_identity(counter: &AtomicU64) -> Result<DocumentIdentity, XmlDocumentError> {
Expand Down
11 changes: 11 additions & 0 deletions src/hard_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ pub(crate) const ENCRYPTION_RECIPIENT_CEILING: usize = 64;
/// Maximum symmetric keys attempted by one prepared decryption operation.
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
pub(crate) const KEY_CANDIDATE_CEILING: usize = 64;
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
pub(crate) const KEY_IMPORT_KDF_WORK_CEILING: u64 = 10_000_000;
/// Maximum workspace reserved by one imported password key derivation.
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
pub(crate) const KEY_IMPORT_KDF_MEMORY_CEILING: usize = 32 * 1024 * 1024;
/// Stack-safety ceiling for BER construction and nested PKCS#12 safe bags.
#[cfg(feature = "xmldsig")]
pub(crate) const PKCS12_NESTING_CEILING: usize = 32;
/// Maximum byte length of one imported DSA integer before big-integer work.
#[cfg(feature = "xmldsig")]
pub(crate) const DSA_KEY_COMPONENT_BYTE_CEILING: usize = 512;
/// Maximum nested `KeyInfoReference` dereference depth.
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
pub(crate) const KEY_INFO_REFERENCE_DEPTH_CEILING: usize = 8;
Expand Down
Loading
Loading