use crate::Error;
use crate::basic::{PathRestriction, ess_matches, exact};
use crate::prepare::Preparation;
use crate::source::{DocumentId, Node, Original};
use structured_asn1::memory::{OwnedVec, Work};
use structured_asn1::{Reader, Span};
use structured_cms::binding::Scope;
use structured_cms::cades::Role;
use structured_cms::counter::{Visit, WalkError};
use structured_cms::directory::{self, CertificateAgreement};
use structured_cms::prepare::{self, PrepareError};
use structured_cms::provider::{SignatureAnswer, SignatureProvider};
use structured_cms::verify::{Batch, ContentAgreement, PassError};
use structured_cms::{Octets, signer::SignerInfo};
use structured_ltv::pki::{CheckedPath, Counter, CounterReceipts};
use structured_ltv::source::Snapshot;
use structured_pkix::Certificate;
use structured_pkix::algorithm::digest::AsnDigest;
#[derive(Debug)]
pub enum CheckError<E> {
Processing(Error),
Prepare(PrepareError<E>),
Verify(PassError<structured_cms::octets::SourceError, E>),
Identifier(directory::Error),
Ess(structured_cms::ess::BindingError<E>),
Restriction(crate::basic::BindError<E>),
}
#[derive(Debug)]
pub enum Outcome {
Bound,
Differs,
Unsupported(prepare::Part),
Malformed(structured_cms::Error),
}
pub struct Binding<'model, 'document, 'path> {
source: Original<'model, 'document>,
parent: Node,
path: &'path CheckedPath<'document>,
outcome: Outcome,
profile: structured_cms::cades::Profile<'document>,
identifier: Option<CertificateAgreement>,
signing: Option<structured_cms::ess::Bindings>,
content: Option<ContentAgreement>,
signature: Option<SignatureAnswer>,
restrictions: Option<PathRestriction>,
}
impl<'m, 'd, 'p> Binding<'m, 'd, 'p> {
fn unproven(
source: Original<'m, 'd>,
parent: Span,
path: &'p CheckedPath<'d>,
profile: structured_cms::cades::Profile<'d>,
) -> Self {
Self {
source,
parent: Node::of(parent),
path,
outcome: Outcome::Differs,
profile,
identifier: None,
signing: None,
content: None,
signature: None,
restrictions: None,
}
}
#[must_use]
pub fn source(&self) -> Original<'m, 'd> {
self.source
}
#[must_use]
pub fn parent(&self) -> Node {
self.parent
}
#[must_use]
pub fn outcome(&self) -> &Outcome {
&self.outcome
}
#[must_use]
pub fn profile(&self) -> &structured_cms::cades::Profile<'d> {
&self.profile
}
#[must_use]
pub fn identifier(&self) -> Option<&CertificateAgreement> {
self.identifier.as_ref()
}
#[must_use]
pub fn signing(&self) -> Option<structured_cms::ess::Bindings> {
self.signing
}
#[must_use]
pub fn content(&self) -> Option<&ContentAgreement> {
self.content.as_ref()
}
#[must_use]
pub fn signature(&self) -> Option<SignatureAnswer> {
self.signature
}
#[must_use]
pub fn restrictions(&self) -> Option<PathRestriction> {
self.restrictions
}
}
pub struct Context<'scope, 'model, 'document> {
selected: Original<'model, 'document>,
signer: &'scope SignerInfo<'document>,
parent_signature: &'scope Octets<'document>,
parent: Span,
reader: &'scope Reader<'document>,
}
impl<'m, 'd> Context<'_, 'm, 'd> {
#[must_use]
pub fn signer(&self) -> &SignerInfo<'d> {
self.signer
}
pub fn check<'a, 'p, P>(
&'a self,
provider: &'a P,
certificate: &Certificate<'d>,
path: &'p CheckedPath<'d>,
) -> Result<Binding<'m, 'd, 'p>, CheckError<P::Error>>
where
'd: 'a,
P: SignatureProvider<'a> + AsnDigest<'a>,
{
let at = self.signer.span().start;
let processing = |error| CheckError::Processing(Error::Cms(error));
let work = self.reader.execution();
let resource = |cause| CheckError::Processing(Error::resource(cause, "cades.counter", at));
let profile =
structured_cms::cades::profile(self.signer, Role::Countersignature, self.reader)
.map_err(processing)?;
let conforms = profile_conforms(self.signer, &profile, self.reader)?;
let mut result = Binding::unproven(self.selected, self.parent, path, profile);
let Some(target) = path.members().last() else {
return Ok(result);
};
let same_certificate = exact(
target.certificate().as_bytes(),
certificate.as_bytes(),
&work,
)
.map_err(resource)?;
result.identifier = Some(
directory::matches_certificate_in(self.signer.sid(), certificate, self.reader)
.map_err(CheckError::Identifier)?,
);
if !same_certificate
|| !matches!(result.identifier, Some(CertificateAgreement::Matches))
|| !conforms
{
return Ok(result);
}
let plan = prepare::resolve(
provider,
self.signer,
target.key(),
self.reader,
Scope::CounterSignature,
)
.map_err(CheckError::Prepare)?;
let plan = match plan {
prepare::Preparation::Ready(plan) => plan,
prepare::Preparation::Unsupported(part) => {
result.outcome = Outcome::Unsupported(part);
return Ok(result);
}
prepare::Preparation::Malformed(error) => {
result.outcome = Outcome::Malformed(error);
return Ok(result);
}
};
let mut requests = work.vector();
requests.try_push(plan).map_err(resource)?;
let batch = Batch::prepare(provider, requests.into_owned(), &work, "cades.counter", at)
.map_err(|source| {
CheckError::Verify(structured_cms::stream::StreamError::Callback(source))
})?;
let mut original_parent = self.parent_signature;
let checks = batch
.run(&mut original_parent, &work)
.map_err(CheckError::Verify)?;
let Some(checked) = checks.into_iter().next() else {
return Err(CheckError::Processing(Error::Foreign { at }));
};
let (_, _, content, signature) = checked.into_parts();
let math = matches!(
content,
ContentAgreement::Direct | ContentAgreement::Matches
) && signature == Some(SignatureAnswer::Valid);
result.content = Some(content);
result.signature = signature;
let signing = result.profile.signing();
let ess = if signing.v1().is_some() || signing.v2().is_some() {
let bindings = signing
.bind_certificate(provider, certificate, self.reader)
.map_err(CheckError::Ess)?;
result.signing = Some(bindings);
ess_matches(bindings)
} else {
true
};
let restriction = crate::basic::restrictions(signing, Some(path), provider, self.reader)
.map_err(CheckError::Restriction)?;
result.restrictions = Some(restriction);
if math
&& ess
&& matches!(
restriction,
PathRestriction::Unstated | PathRestriction::Matches
)
{
result.outcome = Outcome::Bound;
}
Ok(result)
}
}
fn profile_conforms<E>(
signer: &SignerInfo<'_>,
profile: &structured_cms::cades::Profile<'_>,
reader: &Reader<'_>,
) -> Result<bool, CheckError<E>> {
let mut conforms = signer.faults().is_empty()
&& profile.faults().is_empty()
&& profile.signing().faults().is_empty();
for attribute in [profile.signing().v1(), profile.signing().v2()]
.into_iter()
.flatten()
{
reader
.charge(1)
.map_err(|cause| CheckError::Processing(Error::encoding(cause, "cades.counter")))?;
if !attribute.faults().is_empty() {
conforms = false;
}
}
Ok(conforms)
}
pub struct Bindings<'model, 'document, 'path> {
signature: Original<'model, 'document>,
values: OwnedVec<Option<Binding<'model, 'document, 'path>>>,
complete: bool,
}
impl<'m, 'd, 'p> Bindings<'m, 'd, 'p> {
#[must_use]
pub fn values(&self) -> &[Option<Binding<'m, 'd, 'p>>] {
&self.values
}
#[must_use]
pub fn paths(&self) -> Option<Paths<'_, 'm, 'd, 'p>> {
if self.complete {
Some(Paths { bindings: self })
} else {
None
}
}
}
pub struct Paths<'receipt, 'model, 'document, 'path> {
bindings: &'receipt Bindings<'model, 'document, 'path>,
}
impl<'m, 'd, 'p> CounterReceipts<'p, 'd> for Paths<'_, 'm, 'd, 'p> {
type Id = Node;
type Document = DocumentId<'m, 'd>;
type Generation = ();
fn snapshot(&self) -> Snapshot<DocumentId<'m, 'd>, ()> {
self.bindings.signature.snapshot
}
fn signature(&self) -> Node {
self.bindings.signature.node
}
fn len(&self) -> usize {
self.bindings.values.len()
}
fn counter(&self, ordinal: usize) -> Counter<'p, 'd, Node> {
let Some(entry) = &self.bindings.values[ordinal] else {
unreachable!("only a complete counter set exposes Paths")
};
Counter {
signature: entry.source.node,
property: entry.source.node,
path: entry.path,
}
}
}
pub type BindingsError<E> = WalkError<SelectionError<E>>;
#[derive(Debug)]
pub enum SelectionError<E> {
Callback(E),
Foreign {
at: u64,
},
}
pub fn bind<'m, 'd, 'p, E>(
prepared: &Preparation<'m, 'd>,
parent: &Reader<'d>,
mut select: impl FnMut(Context<'_, 'm, 'd>) -> Result<Binding<'m, 'd, 'p>, E>,
) -> Result<Bindings<'m, 'd, 'p>, BindingsError<E>> {
let mut complete = true;
let values = structured_cms::counter::walk(prepared.signer(), parent, |visit, reader| {
let Visit::Signer {
signer,
signature,
parent: owner,
} = visit
else {
complete = false;
return Ok(None);
};
let selected = prepared.document().locate(signer.span());
let result = select(Context {
selected,
signer,
parent_signature: signature,
parent: owner,
reader,
})
.map_err(SelectionError::Callback)?;
if result.source != selected || result.parent != Node::of(owner) {
return Err(SelectionError::Foreign {
at: signer.span().start,
});
}
if !matches!(result.outcome, Outcome::Bound) {
complete = false;
}
Ok(Some(result))
})?;
Ok(Bindings {
signature: prepared.document().locate(prepared.signer().span()),
values,
complete,
})
}