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
24 changes: 3 additions & 21 deletions crates/xml-sec-xades/src/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use structured_xades::certificate::DigestProvider;
use structured_xades::hashes::HashClass;
use structured_xades::xml::{Access, Name};
use xml_sec::document::NodeIdentity;
pub use xml_sec::xmldsig::DigestOutput;
use xml_sec::xmldsig::extension;
use xml_sec::xmldsig::{DigestAlgorithm, DigestMethodResolution, SessionDigest, constant_time_eq};

Expand Down Expand Up @@ -104,19 +105,6 @@ pub enum DigestIdentity {
Digest(DigestAlgorithm),
}

/// A finished certificate digest, held inline.
#[derive(Debug, Clone, Copy)]
pub struct DigestOutput {
bytes: [u8; 64],
len: usize,
}

impl AsRef<[u8]> for DigestOutput {
fn as_ref(&self) -> &[u8] {
&self.bytes[..self.len]
}
}

impl DigestIdentity {
/// Total order key: equal keys name the same digest, so outputs can be shared.
fn order_key(self) -> (u8, &'static str) {
Expand Down Expand Up @@ -159,9 +147,8 @@ impl<'a, W: Work> Digest<'a> for Document<'_, '_, '_, '_, W> {
let DigestIdentity::Digest(algorithm) = identity else {
return Err(Error::ProhibitedDigest);
};
execution.charge_work(1)?;
Ok(Resolution::Supported(
self.session.begin_digest(*algorithm)?,
self.session.begin_digest(*algorithm, execution)?,
))
}

Expand All @@ -179,12 +166,7 @@ impl<'a, W: Work> Digest<'a> for Document<'_, '_, '_, '_, W> {
state: SessionDigest<'a>,
execution: &Q,
) -> Result<DigestOutput, Error> {
let mut output = DigestOutput {
bytes: [0; 64],
len: state.output_len(),
};
state.finish(&mut output.bytes[..output.len], execution)?;
Ok(output)
Ok(state.finish_output(execution)?)
}

fn matches<Q: Work>(
Expand Down
18 changes: 15 additions & 3 deletions docs/crypto-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,21 @@ Without it, that selection is an explicit unavailable-provider error.

Every provider implements `CryptoProvider::begin_digest`, which returns a
provider-owned `DigestState`: input is absorbed in order across any number of
`update` calls, and `finalize` consumes the state. The one-shot `digest` is a
provided method built on it, so an engine implements hashing once. Custom
providers implement `begin_digest`; it replaces the former required `digest`.
`update` calls, and `finalize` writes the digest to an inline buffer, once. The
one-shot `digest` is a provided method built on it, so an engine implements hashing
once. Custom providers implement `begin_digest`; it replaces the former required
`digest`.

`begin_digest` receives the operation's `Memory`. A provider puts its state on the
heap with `charged_digest_state`, which admits the whole block first, keeps the
charge as long as the state and turns an allocator refusal into an error. A refusal
is `ProviderError::Resource`: it ends the operation with that cause and is never
reported as an unsupported algorithm or a digest mismatch. Sessions, CMS, XAdES and
detached payloads charge the caller's execution; plain signing and verification,
which hold one state per Reference, use an account of their own without a ceiling.
A PKCS#11 result passes through a buffer that `cryptoki` allocates itself: its width
is admitted with the state, before the token operation starts, but that one
allocation cannot report a refusal.

XMLDSig Reference processing streams the final transform output into this state
for both signing and verification: a final node set is canonicalized straight
Expand Down
9 changes: 5 additions & 4 deletions docs/xmldsig.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,13 +558,14 @@ Manifest references obey the same per-reference transform-count ceiling and tran
`<SignedInfo>` references; a violation is recorded in that Manifest reference's independent status.
They also share `ResourcePolicy::max_references` with `<SignedInfo>`: every parsed Manifest
reference consumes that operation-wide budget, including entries whose transforms are unsupported
and retained only as independent failure results.
and retained only as independent failure results. Exhausting an operation-wide resource limit while
processing a Manifest reference ends `verify()` with an error instead of a per-reference status.

Malformed `XMLDSig` structure, unsupported algorithms in core signature processing, disallowed URIs
in `<SignedInfo>` references, and inconsistent `KeyInfo` metadata are processing errors rather
than validity statuses. Manifest policy violations and unsupported transforms remain independent
per-reference statuses as described above. Treat both `Invalid(reason)` and an API error as a
rejected document; never continue an authentication flow after either outcome.
than validity statuses. Other Manifest policy violations and unsupported transforms remain
independent per-reference statuses as described above. Treat both `Invalid(reason)` and an API
error as a rejected document; never continue an authentication flow after either outcome.

External references are disabled by default. Verification and signing callers must both allow
their URI class with `UriTypeSet` and provide every payload through
Expand Down
1 change: 1 addition & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4.13"
structured-asn1 = { version = "0.20", default-features = false, features = ["alloc", "std"] }
xml-sec = { path = "..", default-features = false, features = ["xmlenc", "xslt", "xml-backends-all"] }
xml-sec-config = { path = "../crates/xml-sec-config" }
xml-sec-xslt = { path = "../crates/xml-sec-xslt" }
Expand Down
20 changes: 16 additions & 4 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,27 @@ pub fn provider_operations(data: &[u8]) {
if !provider.supports(ProviderCapability::Digest(algorithm)) {
continue;
}
let mut one = provider.begin_digest(algorithm).expect("advertised digest");
let memory = structured_asn1::memory::Memory::new(std::sync::Arc::new(
structured_asn1::memory::MemoryAccount::new(1 << 20, 1 << 20),
));
let finish = |state: &mut dyn xml_sec::provider::DigestState| {
let mut output = [0; xml_sec::xmldsig::MAX_DIGEST_OUTPUT];
let written = state.finalize(&mut output).expect("finalize");
output[..written].to_vec()
};
let mut one = provider
.begin_digest(algorithm, &memory)
.expect("advertised digest");
one.update(data).expect("digest input");
let one = one.finalize().expect("finalize");
let mut chunks = provider.begin_digest(algorithm).expect("advertised digest");
let one = finish(&mut *one);
let mut chunks = provider
.begin_digest(algorithm, &memory)
.expect("advertised digest");
let split = data.len() / 2;
chunks.update(&data[..split]).expect("first chunk");
chunks.update(&[]).expect("empty chunk");
chunks.update(&data[split..]).expect("last chunk");
assert_eq!(one, chunks.finalize().expect("finalize"), "digest chunking");
assert_eq!(one, finish(&mut *chunks), "digest chunking");
if let Some(expected) = &expected {
assert_eq!(&one, expected, "provider digest disagreement");
}
Expand Down
1 change: 1 addition & 0 deletions src/key_establishment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ mod tests {
fn begin_digest(
&self,
_: crate::xmldsig::DigestAlgorithm,
_: &structured_asn1::memory::Memory,
) -> Result<Box<dyn crate::provider::DigestState + '_>, ProviderError> {
unreachable!("no digest after failed entropy")
}
Expand Down
121 changes: 101 additions & 20 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,10 @@ pub enum ProviderError {
/// Operating-system randomness was unavailable.
#[error("operating-system random number generation failed: {0}")]
Random(String),
/// The operation's account or the allocator refused storage. This is a
/// spent ceiling, never a finding about the document.
#[error("provider resource refusal: {0}")]
Resource(structured_asn1::memory::MemoryError),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

/// Operational failure classes for an explicitly selected external provider.
Expand Down Expand Up @@ -921,11 +925,69 @@ pub trait DigestState {
/// Returns the selected provider's digest-update failure.
fn update(&mut self, data: &[u8]) -> Result<(), ProviderError>;

/// Return the digest of all absorbed bytes.
/// Write the digest of all absorbed bytes to the start of `output` and
/// return its length. The owner finalizes once, then drops the state.
///
/// # Errors
/// Returns the selected provider's digest-finalization failure.
fn finalize(self: Box<Self>) -> Result<Vec<u8>, ProviderError>;
fn finalize(
&mut self,
output: &mut [u8; crate::xmldsig::MAX_DIGEST_OUTPUT],
) -> Result<usize, ProviderError>;
}

/// A digest state with the charge for the block that holds it.
#[cfg(feature = "xmldsig")]
struct Charged<T> {
state: T,
// Inside the block it pays for, so it releases just before the block does:
// the one moment the account is lower than the allocator, and nothing runs in it.
_charge: structured_asn1::memory::MemoryReservation,
}

// One element, because a fallibly allocated `Box<[T; 1]>` is the only `Box` that
// stable Rust offers without `unsafe`.
#[cfg(feature = "xmldsig")]
impl<T: DigestState> DigestState for [Charged<T>; 1] {
fn update(&mut self, data: &[u8]) -> Result<(), ProviderError> {
self[0].state.update(data)
}

fn finalize(
&mut self,
output: &mut [u8; crate::xmldsig::MAX_DIGEST_OUTPUT],
) -> Result<usize, ProviderError> {
self[0].state.finalize(output)
}
}

/// Move `state` to the heap under `memory`: its complete block is admitted
/// first, the allocation can fail, and the charge lasts as long as the state.
///
/// # Errors
/// Returns [`ProviderError::Resource`] when the account or the allocator refuses.
#[cfg(feature = "xmldsig")]
pub fn charged_digest_state<'s, T: DigestState + 's>(
state: T,
memory: &structured_asn1::memory::Memory,
) -> Result<Box<dyn DigestState + 's>, ProviderError> {
use structured_asn1::memory::MemoryError;
let charge = memory
.reserve(core::mem::size_of::<Charged<T>>())
.map_err(ProviderError::Resource)?;
let mut block = Vec::new();
block
.try_reserve_exact(1)
.map_err(|_| ProviderError::Resource(MemoryError::OutOfMemory))?;
block.push(Charged {
state,
_charge: charge,
});
let block: Box<[Charged<T>; 1]> = match block.into_boxed_slice().try_into() {
Ok(block) => block,
Err(_) => unreachable!("the slice holds the one pushed element"),
};
Ok(block)
}

/// Provider operations used by the XML Security pipelines.
Expand Down Expand Up @@ -1108,25 +1170,34 @@ pub trait CryptoProvider: Send + Sync {
}

/// Start an incremental message digest; streamed input never needs to be
/// collected in memory first.
/// collected in memory first. Storage for the state is admitted in `memory`
/// before it exists, as [`charged_digest_state`] does.
///
/// # Errors
/// Returns an unsupported digest or the selected provider's state-creation failure.
/// Returns an unsupported digest, a resource refusal, or the selected
/// provider's state-creation failure.
#[cfg(feature = "xmldsig")]
fn begin_digest(
&self,
algorithm: DigestAlgorithm,
memory: &structured_asn1::memory::Memory,
) -> Result<Box<dyn DigestState + '_>, ProviderError>;

/// Compute the message digest of one contiguous input.
/// Compute the message digest of one contiguous input over the same
/// execution as streamed input.
///
/// # Errors
/// Propagates state creation, update and finalization failures unchanged.
#[cfg(feature = "xmldsig")]
fn digest(&self, algorithm: DigestAlgorithm, data: &[u8]) -> Result<Vec<u8>, ProviderError> {
let mut state = self.begin_digest(algorithm)?;
fn digest(
&self,
algorithm: DigestAlgorithm,
data: &[u8],
memory: &structured_asn1::memory::Memory,
) -> Result<crate::xmldsig::DigestOutput, ProviderError> {
let mut state = self.begin_digest(algorithm, memory)?;
state.update(data)?;
state.finalize()
crate::xmldsig::finish_digest(algorithm, &mut *state)
}

/// Sign bytes with an opaque key handle.
Expand Down Expand Up @@ -1523,10 +1594,17 @@ macro_rules! rust_crypto_digest {
Ok(())
}

fn finalize(self: Box<Self>) -> Result<Vec<u8>, ProviderError> {
fn finalize(
&mut self,
output: &mut [u8; crate::xmldsig::MAX_DIGEST_OUTPUT],
) -> Result<usize, ProviderError> {
use sha2::Digest as _;
Ok(match *self {
$($(#[$attr])* Self::$variant(hasher) => hasher.finalize().to_vec(),)+
Ok(match self {
$($(#[$attr])* Self::$variant(hasher) => {
let digest = hasher.finalize_reset();
output[..digest.len()].copy_from_slice(&digest);
digest.len()
})+
})
}
}
Expand Down Expand Up @@ -2074,12 +2152,13 @@ impl CryptoProvider for RustCryptoProvider {
fn begin_digest(
&self,
algorithm: DigestAlgorithm,
memory: &structured_asn1::memory::Memory,
) -> Result<Box<dyn DigestState + '_>, ProviderError> {
#[cfg(feature = "dstu4145")]
if crate::xmldsig::dstu4145::is_ukrainian_digest(algorithm) {
return crate::xmldsig::dstu4145::begin_digest(algorithm);
return crate::xmldsig::dstu4145::begin_digest(algorithm, memory);
}
Ok(Box::new(RustCryptoDigest::new(algorithm)))
charged_digest_state(RustCryptoDigest::new(algorithm), memory)
}

#[cfg(feature = "xmldsig")]
Expand Down Expand Up @@ -3332,8 +3411,8 @@ mod tests {
),
] {
assert_eq!(
RUST_CRYPTO_PROVIDER
.digest(algorithm, b"abc")
&*RUST_CRYPTO_PROVIDER
.digest(algorithm, b"abc", &crate::xmldsig::digest::digest_memory())
.expect("compiled digest must be supported"),
expected
);
Expand All @@ -3352,10 +3431,11 @@ mod tests {
fn update(&mut self, data: &[u8]) -> Result<(), ProviderError> {
self.0.update(data)
}
fn finalize(self: Box<Self>) -> Result<Vec<u8>, ProviderError> {
let mut digest = self.0.finalize()?;
digest.push(0);
Ok(digest)
fn finalize(
&mut self,
output: &mut [u8; crate::xmldsig::MAX_DIGEST_OUTPUT],
) -> Result<usize, ProviderError> {
Ok(self.0.finalize(output)? + 1)
}
}

Expand Down Expand Up @@ -3393,14 +3473,15 @@ mod tests {
fn begin_digest(
&self,
algorithm: DigestAlgorithm,
memory: &structured_asn1::memory::Memory,
) -> Result<Box<dyn DigestState + '_>, ProviderError> {
if self.reject_digest == Some(algorithm) {
return Err(ProviderError::Unsupported {
operation: ProviderOperation::Digest,
algorithm: Some(algorithm.uri().to_owned()),
});
}
let state = RUST_CRYPTO_PROVIDER.begin_digest(algorithm)?;
let state = RUST_CRYPTO_PROVIDER.begin_digest(algorithm, memory)?;
Ok(if self.extra_digest_byte {
Box::new(ExtraByteDigest(state))
} else {
Expand Down
21 changes: 16 additions & 5 deletions src/provider/aws_lc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,26 @@ fn digest_algorithm(algorithm: DigestAlgorithm) -> Option<&'static digest::Algor
})
}

struct AwsLcDigest(digest::Context);
// `finish` consumes the context, which `finalize` receives by reference.
struct AwsLcDigest(Option<digest::Context>);

impl super::DigestState for AwsLcDigest {
fn update(&mut self, data: &[u8]) -> Result<(), ProviderError> {
self.0.update(data);
self.0
.as_mut()
.expect("the owner finalizes last")
.update(data);
Ok(())
}

fn finalize(self: Box<Self>) -> Result<Vec<u8>, ProviderError> {
Ok(self.0.finish().as_ref().to_vec())
fn finalize(
&mut self,
output: &mut [u8; crate::xmldsig::MAX_DIGEST_OUTPUT],
) -> Result<usize, ProviderError> {
let digest = self.0.take().expect("the owner finalizes once").finish();
let digest = digest.as_ref();
output[..digest.len()].copy_from_slice(digest);
Ok(digest.len())
}
}

Expand Down Expand Up @@ -201,10 +211,11 @@ impl CryptoProvider for AwsLcFipsProvider {
fn begin_digest(
&self,
algorithm: DigestAlgorithm,
memory: &structured_asn1::memory::Memory,
) -> Result<Box<dyn super::DigestState + '_>, ProviderError> {
let native = digest_algorithm(algorithm)
.ok_or_else(|| unsupported(ProviderCapability::Digest(algorithm)))?;
Ok(Box::new(AwsLcDigest(digest::Context::new(native))))
super::charged_digest_state(AwsLcDigest(Some(digest::Context::new(native))), memory)
}

fn sign(
Expand Down
Loading
Loading