use crate::binary::{self, BinaryXml, Location};
use crate::binding::{Bound, SignatureXml};
use crate::container::XADES;
use crate::properties::{Error, PropertyResult, Reading};
use crate::xml::{Access, AccessError, Name, Snapshot, Xml};
use structured_asn1::{
Reader, Tag,
memory::{MemoryError, OwnedVec, Work},
universal,
};
use structured_cms::ess::IssuerSerialReading;
use structured_cms::provider::Resolution;
use structured_pkix::{Certificate, path::WorkingKey};
mod path;
mod time;
pub use path::{
CheckedPathCertificate, PathCheckResult, PathDigest, PathDigestResult, PathDigests, check_path,
match_path,
};
pub use time::{SignatureTime, SignatureTimeBinding, SignatureTimeError, SignatureTimeFailure};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAssociation {
Same,
Differs,
Unproven,
}
pub trait CertificateXml: SignatureXml {
fn key_association<W: Work>(
&self,
snapshot: Snapshot<Self::Document, Self::Generation>,
signature: Self::Id,
certificate: &Certificate<'_>,
key: WorkingKey<'_>,
execution: &W,
) -> Result<KeyAssociation, Self::Error>;
}
pub trait DigestProvider<X: Xml> {
type Identity;
type Error;
fn resolve<W: Work>(
&self,
access: &Access<'_, X, W>,
method: X::Id,
) -> Result<Resolution<Self::Identity>, Self::Error>;
fn classify(&self, identity: &Self::Identity) -> crate::hashes::HashClass;
fn width(&self, identity: &Self::Identity) -> Option<usize>;
fn matches<W: Work>(
&self,
identity: &Self::Identity,
certificate: &[u8],
expected: &[u8],
execution: &W,
) -> Result<bool, Self::Error>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Agreement {
ProhibitedAlgorithm,
Matches,
Differs,
Unsupported,
MalformedMethod,
MalformedValue,
}
#[derive(Debug)]
pub enum DigestError<E> {
ReferenceIndex {
index: usize,
},
Provider(E),
Resource(MemoryError),
}
#[derive(Debug, Clone, Copy)]
pub struct Reference<N> {
node: N,
method: N,
digest: N,
issuer: Option<N>,
}
impl<N: Copy> Reference<N> {
#[must_use]
pub fn node(&self) -> N {
self.node
}
#[must_use]
pub fn method(&self) -> N {
self.method
}
#[must_use]
pub fn digest(&self) -> N {
self.digest
}
#[must_use]
pub fn issuer_serial(&self) -> Option<N> {
self.issuer
}
}
pub struct References<N, D, G> {
snapshot: Snapshot<D, G>,
signature: N,
node: N,
entries: OwnedVec<Reference<N>>,
}
impl<N: Copy, D: Copy, G: Copy> References<N, D, G> {
#[must_use]
pub fn snapshot(&self) -> Snapshot<D, G> {
self.snapshot
}
#[must_use]
pub fn signature(&self) -> N {
self.signature
}
#[must_use]
pub fn node(&self) -> N {
self.node
}
#[must_use]
pub fn entries(&self) -> &[Reference<N>] {
&self.entries
}
}
pub fn compare<X: BinaryXml, P: DigestProvider<X>, W: Work>(
xml: &X,
reference: &Catalogue<X>,
index: usize,
certificate: &Certificate<'_>,
provider: &P,
execution: &W,
) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
let Some(entry) = reference.entries.get(index) else {
return Err(binary::Error::Callback {
location: Location {
snapshot: reference.snapshot,
node: reference.node,
},
cause: DigestError::ReferenceIndex { index },
});
};
let location = Location {
snapshot: reference.snapshot,
node: entry.method,
};
let access = Access::new(xml, reference.snapshot, execution);
if let Some(answer) = preflight_method(&access, entry.method)
.map_err(|cause| binary::Error::Access { location, cause })?
{
return Ok(answer);
}
binary::with_octets(
xml,
Location {
snapshot: reference.snapshot,
node: entry.digest,
},
execution,
|expected| {
Ok::<_, core::convert::Infallible>(compare_expected(
&access,
entry,
certificate,
provider,
expected,
))
},
)
.map_err(|error| error.map_callback(|never| match never {}))?
}
fn compare_expected<X: Xml, P: DigestProvider<X>, W: Work>(
access: &Access<'_, X, W>,
entry: &Reference<X::Id>,
certificate: &Certificate<'_>,
provider: &P,
expected: &[u8],
) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
let identity = match resolve_expected(access, entry, provider, expected)? {
Expected::Ready(identity) => identity,
Expected::Answer(answer) => return Ok(answer),
};
compare_resolved(access, entry, certificate, provider, &identity, expected)
}
pub(crate) enum Expected<I> {
Ready(FormatDigest<I>),
Answer(Agreement),
}
pub(crate) struct FormatDigest<I>(I);
impl<I> FormatDigest<I> {
pub(crate) fn identity(&self) -> &I {
&self.0
}
}
pub(crate) fn baseline_digest<X: Xml, P: DigestProvider<X>>(
identity: P::Identity,
provider: &P,
) -> Expected<P::Identity> {
match provider.classify(&identity) {
crate::hashes::HashClass::Md5 => Expected::Answer(Agreement::ProhibitedAlgorithm),
crate::hashes::HashClass::Intrinsic => Expected::Answer(Agreement::MalformedMethod),
crate::hashes::HashClass::OtherKnown => Expected::Ready(FormatDigest(identity)),
}
}
fn resolve_expected<X: Xml, P: DigestProvider<X>, W: Work>(
access: &Access<'_, X, W>,
entry: &Reference<X::Id>,
provider: &P,
expected: &[u8],
) -> binary::BinaryResult<Expected<P::Identity>, X, DigestError<P::Error>> {
let location = Location {
snapshot: access.snapshot(),
node: entry.method,
};
let resolved = access
.query(entry.method, |_, _, _| {
Ok(provider.resolve(access, entry.method))
})
.map_err(|cause| binary::Error::Access { location, cause })?
.map_err(|cause| binary::Error::Callback {
location,
cause: DigestError::Provider(cause),
})?;
let identity = match resolved {
Resolution::Supported(identity) => identity,
Resolution::Unsupported => return Ok(Expected::Answer(Agreement::Unsupported)),
Resolution::Malformed => return Ok(Expected::Answer(Agreement::MalformedMethod)),
};
let identity = match baseline_digest::<X, P>(identity, provider) {
Expected::Ready(identity) => identity,
answer @ Expected::Answer(_) => return Ok(answer),
};
if let Some(width) = provider.width(identity.identity())
&& expected.len() != width
{
return Ok(Expected::Answer(Agreement::MalformedValue));
}
Ok(Expected::Ready(identity))
}
fn compare_resolved<X: Xml, P: DigestProvider<X>, W: Work>(
access: &Access<'_, X, W>,
entry: &Reference<X::Id>,
certificate: &Certificate<'_>,
provider: &P,
identity: &FormatDigest<P::Identity>,
expected: &[u8],
) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
let execution = access.execution();
let location = Location {
snapshot: access.snapshot(),
node: entry.digest,
};
let check = || {
execution.charge_work(1).map_err(DigestError::Resource)?;
let matches = provider
.matches(
identity.identity(),
certificate.as_bytes(),
expected,
execution,
)
.map_err(DigestError::Provider)?;
Ok(if matches {
Agreement::Matches
} else {
Agreement::Differs
})
};
check().map_err(|cause| binary::Error::Callback { location, cause })
}
pub(crate) fn preflight_method<X: Xml, W: Work>(
access: &Access<'_, X, W>,
node: X::Id,
) -> Result<Option<Agreement>, AccessError<X::Error, X::Id>> {
access
.attribute(
node,
Name {
namespace: "",
local: "Algorithm",
},
|value| {
let Some(value) = value else {
return Ok(Some(Agreement::MalformedMethod));
};
let value = access.trim_xml_whitespace(value)?;
if access.equal(value, "http://www.w3.org/2001/04/xmldsig-more#md5")? {
return Ok(Some(Agreement::ProhibitedAlgorithm));
}
Ok(None)
},
)?
.map_err(|cause| AccessError::Resource { node, cause })
}
pub type Catalogue<X> = References<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
pub struct CheckedCertificate<'view, 'document, N, D, G> {
reference: &'view References<N, D, G>,
certificate: &'view Certificate<'document>,
key: KeyAssociation,
digest: Agreement,
}
impl<'view, 'document, N, D, G> CheckedCertificate<'view, 'document, N, D, G> {
#[must_use]
pub fn certificate(&self) -> &'view Certificate<'document> {
self.certificate
}
#[must_use]
pub fn reference(&self) -> &'view References<N, D, G> {
self.reference
}
#[must_use]
pub fn key(&self) -> KeyAssociation {
self.key
}
#[must_use]
pub fn digest(&self) -> Agreement {
self.digest
}
#[must_use]
pub fn is_bound(&self) -> bool {
self.key == KeyAssociation::Same && self.digest == Agreement::Matches
}
}
pub type Checked<'view, 'document, X> = CheckedCertificate<
'view,
'document,
<X as Xml>::Id,
<X as Xml>::Document,
<X as Xml>::Generation,
>;
#[derive(Debug)]
pub enum CheckError<E, PE, N, D, G> {
ForeignCatalogue {
signature: N,
property: N,
},
ForbiddenCertificateUri {
location: Location<N, D, G>,
},
Access(AccessError<E, N>),
Digest(binary::Error<E, DigestError<PE>, N, D, G>),
}
pub type CheckResult<'v, 'd, X, PE> = CertificateResult<Checked<'v, 'd, X>, X, PE>;
pub type CertificateResult<T, X, PE> = Result<
T,
CheckError<<X as Xml>::Error, PE, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
>;
#[derive(Debug)]
pub enum IssuerError<E> {
ReferenceIndex {
index: usize,
},
Encoding(structured_asn1::Error),
Reading(structured_cms::Error),
Trailing {
offset: u64,
},
Callback(E),
}
pub fn with_issuer_serial<X: BinaryXml, R, E>(
xml: &X,
reference: &Catalogue<X>,
index: usize,
operation: &Reader<'_>,
visitor: impl for<'source> FnOnce(IssuerSerialReading<'source>) -> Result<R, E>,
) -> binary::BinaryResult<Option<binary::LocatedValue<R, X>>, X, IssuerError<E>> {
let location = Location {
snapshot: reference.snapshot,
node: reference.node,
};
let Some(entry) = reference.entries.get(index) else {
return Err(binary::Error::Callback {
location,
cause: IssuerError::ReferenceIndex { index },
});
};
let execution = operation.execution();
Access::new(xml, reference.snapshot, &execution)
.query(entry.node, |_, _, _| Ok(()))
.map_err(|cause| binary::Error::Access { location, cause })?;
let Some(node) = entry.issuer else {
return Ok(None);
};
let location = Location {
snapshot: reference.snapshot,
node,
};
with_issuer_serial_at(xml, location, operation, visitor).map(Some)
}
pub(crate) fn with_issuer_serial_at<X: BinaryXml, R, E>(
xml: &X,
location: Location<X::Id, X::Document, X::Generation>,
operation: &Reader<'_>,
visitor: impl for<'source> FnOnce(IssuerSerialReading<'source>) -> Result<R, E>,
) -> binary::BinaryResult<binary::LocatedValue<R, X>, X, IssuerError<E>> {
binary::with_der_reader(xml, location, operation, |reader| {
let element = reader
.expect(Tag::constructed(universal::SEQUENCE))
.map_err(IssuerError::Encoding)?;
let reading = IssuerSerialReading::read(reader, &element, "xades.issuerSerialV2")
.map_err(IssuerError::Reading)?;
if !reader.is_empty() {
return Err(IssuerError::Trailing {
offset: reader.offset(),
});
}
visitor(reading)
.map(|value| binary::Located { location, value })
.map_err(IssuerError::Callback)
})
}
pub fn check<'view, 'document, X: BinaryXml + CertificateXml, P: DigestProvider<X>, W: Work>(
xml: &X,
bound: &Bound<X::Id, X::Document, X::Generation>,
reference: &'view Catalogue<X>,
certificate: &'view Certificate<'document>,
provider: &P,
execution: &W,
) -> CheckResult<'view, 'document, X, P::Error> {
let key = bind_key::<X, P::Error, W>(
xml,
bound,
reference,
certificate,
WorkingKey::standing(certificate.tbs().public_key()),
execution,
)?;
let digest =
compare(xml, reference, 0, certificate, provider, execution).map_err(CheckError::Digest)?;
Ok(CheckedCertificate {
reference,
certificate,
key,
digest,
})
}
fn bind_key<X: CertificateXml, PE, W: Work>(
xml: &X,
bound: &Bound<X::Id, X::Document, X::Generation>,
reference: &Catalogue<X>,
certificate: &Certificate<'_>,
key: WorkingKey<'_>,
execution: &W,
) -> CertificateResult<KeyAssociation, X, PE> {
let source = bound.containers();
if reference.snapshot != source.snapshot() {
return Err(CheckError::ForeignCatalogue {
signature: source.signature(),
property: reference.node,
});
}
let access = Access::new(xml, source.snapshot(), execution);
if !access
.equal_id(reference.signature, source.signature())
.map_err(|cause| {
CheckError::Access(AccessError::Resource {
node: reference.node,
cause,
})
})?
{
return Err(CheckError::ForeignCatalogue {
signature: source.signature(),
property: reference.node,
});
}
for index in 0..reference.entries.len() {
access
.query(reference.node, |_, _, work| Ok(work.charge_work(1)))
.map_err(CheckError::Access)?
.map_err(|cause| {
CheckError::Access(AccessError::Resource {
node: reference.node,
cause,
})
})?;
let entry = &reference.entries[index];
let written = access
.attribute(
entry.node,
Name {
namespace: "",
local: "URI",
},
|value| value.is_some(),
)
.map_err(CheckError::Access)?;
if written {
return Err(CheckError::ForbiddenCertificateUri {
location: Location {
snapshot: reference.snapshot,
node: entry.node,
},
});
}
}
access
.query(source.signature(), |xml, snapshot, work| {
xml.key_association(snapshot, source.signature(), certificate, key, work)
})
.map_err(CheckError::Access)
}
pub fn read<X: Xml, W: Work>(
access: &Access<'_, X, W>,
signed: &Reading<X>,
) -> PropertyResult<Catalogue<X>, X> {
let node = signed.signing_certificate();
if signed.snapshot() != access.snapshot() {
return Err(Error::Access(AccessError::Changed { node }));
}
let mut entries = access
.query(node, |_, _, work| Ok(work.vector()))
.map_err(Error::Access)?;
for child in access.children(node) {
let child = child.map_err(Error::Child)?;
expect_name(access, child, XADES, "Cert")?;
let entry = read_entry(access, child)?;
entries
.try_push(entry)
.map_err(|cause| Error::Resource { node: child, cause })?;
}
if entries.is_empty() {
return Err(Error::Missing {
parent: node,
local: "Cert",
});
}
Ok(References {
snapshot: signed.snapshot(),
signature: signed.signature(),
node,
entries: entries.into_owned(),
})
}
pub(crate) fn read_entry<X: Xml, W: Work>(
access: &Access<'_, X, W>,
node: X::Id,
) -> PropertyResult<Reference<X::Id>, X> {
let mut children = access.children(node);
let digest = required(&mut children, node, "CertDigest")?;
expect_name(access, digest, XADES, "CertDigest")?;
let issuer = children.next().transpose().map_err(Error::Child)?;
if let Some(issuer) = issuer {
expect_name(access, issuer, XADES, "IssuerSerialV2")?;
}
if let Some(extra) = children.next() {
return Err(Error::Unexpected {
node: extra.map_err(Error::Child)?,
});
}
let (method, value) = crate::properties::digest_value(access, digest)?;
Ok(Reference {
node,
method,
digest: value,
issuer,
})
}
fn required<X: Xml, W: Work>(
children: &mut crate::xml::Children<'_, '_, X, W>,
parent: X::Id,
local: &'static str,
) -> PropertyResult<X::Id, X> {
children
.next()
.transpose()
.map_err(Error::Child)?
.ok_or(Error::Missing { parent, local })
}
fn expect_name<X: Xml, W: Work>(
access: &Access<'_, X, W>,
node: X::Id,
namespace: &str,
local: &str,
) -> PropertyResult<(), X> {
if !access
.has_name(node, Name { namespace, local })
.map_err(Error::Access)?
{
return Err(Error::Unexpected { node });
}
Ok(())
}