use crate::{
Error,
basic::SignerBinding,
counters::Paths,
prepare::Preparation,
properties::{OpenRole, OpenValue, Properties},
source::{DocumentId, Node, Original},
};
use structured_asn1::{
Reader, ScopedReader,
memory::{OwnedVec, Work},
};
use structured_ltv::source::Snapshot;
use structured_ltv::{
credential::Authentication,
pki::{self, Authorities, CounterReceipts, CredentialGroup, CredentialReceipts},
};
use structured_pkix::Certificate;
pub struct Context<'scope, 'model, 'document> {
source: Original<'model, 'document>,
signature: Node,
value: OpenValue<'document>,
signer: &'scope Certificate<'document>,
operation: &'scope ScopedReader<'document, 'scope>,
}
impl<'m, 'd> Context<'_, 'm, 'd> {
#[must_use]
pub fn source(&self) -> Original<'m, 'd> {
self.source
}
#[must_use]
pub fn signature(&self) -> Node {
self.signature
}
#[must_use]
pub fn signer(&self) -> &Certificate<'d> {
self.signer
}
#[must_use]
pub fn value(&self) -> OpenValue<'d> {
self.value
}
#[must_use]
pub fn operation(&self) -> &ScopedReader<'d, '_> {
self.operation
}
}
pub trait Provider<'path, 'document> {
type Evidence;
type Error;
fn authenticate(
&self,
context: Context<'_, '_, 'document>,
) -> Result<Authentication<'path, 'document, Self::Evidence>, Self::Error>;
}
pub enum Outcome<'path, 'document, E> {
Authentication(Authentication<'path, 'document, E>),
Absent,
MissingAuthorities(E),
}
pub struct Checked<'model, 'document, 'path, E> {
source: Original<'model, 'document>,
outcome: Outcome<'path, 'document, E>,
}
impl<'m, 'd, 'p, E> Checked<'m, 'd, 'p, E> {
#[must_use]
pub fn source(&self) -> Original<'m, 'd> {
self.source
}
#[must_use]
pub fn outcome(&self) -> &Outcome<'p, 'd, E> {
&self.outcome
}
}
pub struct Group<'model, 'document, 'path, E> {
signature: Node,
entries: OwnedVec<Checked<'model, 'document, 'path, E>>,
}
impl<'m, 'd, 'p, E> Group<'m, 'd, 'p, E> {
#[must_use]
pub fn entries(&self) -> &[Checked<'m, 'd, 'p, E>] {
&self.entries
}
}
impl<'d, 'p, E> CredentialGroup<'p, 'd> for Group<'_, 'd, 'p, E> {
type Id = Node;
fn signature(&self) -> Node {
self.signature
}
fn len(&self) -> usize {
self.entries.len()
}
fn credential(&self, ordinal: usize) -> pki::Credential<'p, 'd, Node> {
let entry = &self.entries[ordinal];
let Outcome::Authentication(Authentication::Authenticated { authorities, .. }) =
&entry.outcome
else {
unreachable!("only complete groups are exposed as credential receipts");
};
pki::Credential {
signature: self.signature,
node: entry.source.node,
authorities: *authorities,
}
}
}
pub struct Report<'model, 'document, 'path, E> {
signature: Original<'model, 'document>,
root: Group<'model, 'document, 'path, E>,
counters: OwnedVec<Group<'model, 'document, 'path, E>>,
complete: bool,
}
pub type CounterGroup<'view, 'm, 'd, 'p, E> = (Node, &'view [Checked<'m, 'd, 'p, E>]);
impl<'m, 'd, 'p, E> Report<'m, 'd, 'p, E> {
#[must_use]
pub fn root(&self) -> &[Checked<'m, 'd, 'p, E>] {
&self.root.entries
}
#[must_use]
pub fn counters(&self) -> usize {
self.counters.len()
}
pub fn counter<W: Work>(
&self,
ordinal: usize,
work: &W,
) -> Result<Option<CounterGroup<'_, 'm, 'd, 'p, E>>, structured_asn1::memory::MemoryError> {
work.charge_work(1)?;
Ok(self
.counters
.get(ordinal)
.map(|group| (group.signature, &*group.entries)))
}
#[must_use]
pub fn complete(&self) -> Option<Complete<'_, 'm, 'd, 'p, E>> {
if self.complete {
Some(Complete { report: self })
} else {
None
}
}
}
pub struct Complete<'receipt, 'model, 'document, 'path, E> {
pub(crate) report: &'receipt Report<'model, 'document, 'path, E>,
}
impl<'r, 'm, 'd, 'p, E> Complete<'r, 'm, 'd, 'p, E> {
#[must_use]
pub fn root_group(&self) -> Option<&'r Group<'m, 'd, 'p, E>> {
if self.report.root.entries.is_empty() {
None
} else {
Some(&self.report.root)
}
}
pub fn counter_group(
&self,
ordinal: usize,
work: &impl Work,
) -> Result<Option<&'r Group<'m, 'd, 'p, E>>, structured_asn1::memory::MemoryError> {
work.charge_work(1)?;
Ok(self.report.counters.get(ordinal))
}
}
impl<'m, 'd, 'p, E> CredentialReceipts<'p, 'd> for Complete<'_, 'm, 'd, 'p, E> {
type Id = Node;
type Document = DocumentId<'m, 'd>;
type Generation = ();
type Group = Group<'m, 'd, 'p, E>;
fn snapshot(&self) -> Snapshot<DocumentId<'m, 'd>, ()> {
self.report.signature.snapshot
}
fn signature(&self) -> Node {
self.report.signature.node
}
fn root(&self) -> Option<&Self::Group> {
if self.report.root.entries.is_empty() {
None
} else {
Some(&self.report.root)
}
}
fn counters(&self) -> usize {
self.report.counters.len()
}
fn counter(&self, ordinal: usize) -> (Node, &Self::Group) {
let group = &self.report.counters[ordinal];
(group.signature, group)
}
}
#[derive(Debug)]
pub enum CheckError<E> {
Processing(Error),
Unbound {
signature: Node,
},
Nonconforming {
signature: Node,
},
Provider {
node: Node,
cause: E,
},
}
pub fn check<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
prepared: &Preparation<'m, 'd>,
signer: &SignerBinding<'_, 'm, 'd, 'p>,
counters: &Paths<'_, 'm, 'd, 'p>,
provider: &P,
operation: &Reader<'d>,
) -> Result<Report<'m, 'd, 'p, E>, CheckError<P::Error>> {
prepared
.document()
.message()
.require_session(operation, "cades.credentials")
.map_err(|cause| CheckError::Processing(crate::Error::Cms(cause)))?;
operation.charge(1).map_err(|cause| {
CheckError::Processing(crate::Error::encoding(cause, "cades.credentials"))
})?;
if !prepared.basic_format_requirements_met() {
return Err(CheckError::Nonconforming {
signature: prepared.document().locate(prepared.signer().span()).node,
});
}
check_properties(prepared, signer, counters, provider, operation)
}
pub fn check_properties<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
prepared: &Preparation<'m, 'd>,
signer: &SignerBinding<'_, 'm, 'd, 'p>,
counters: &Paths<'_, 'm, 'd, 'p>,
provider: &P,
operation: &ScopedReader<'d, '_>,
) -> Result<Report<'m, 'd, 'p, E>, CheckError<P::Error>> {
let processing = CheckError::Processing;
prepared
.document()
.message()
.require_session(operation, "cades.credentials")
.map_err(|cause| processing(Error::Cms(cause)))?;
let selected = prepared.document().locate(prepared.signer().span());
operation
.charge(1)
.map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
if !core::ptr::eq(prepared, signer.prepared())
|| selected.snapshot != counters.snapshot()
|| selected.node != counters.signature()
|| counters.len() != prepared.counters().len()
{
return Err(processing(Error::Foreign {
at: selected.node.span().start,
}));
}
if !signer.is_bound() {
return Err(CheckError::Unbound {
signature: selected.node,
});
}
preflight_properties(prepared, counters, operation)?;
let mut complete = true;
let root = group(
prepared,
selected.node,
prepared.properties(),
signer.certificate(),
provider,
operation,
&mut complete,
)?;
let work = operation.execution();
let mut groups = work.vector();
for (ordinal, counter) in prepared.counters().iter().enumerate() {
operation
.charge(1)
.map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
let Some(properties) = counter.properties() else {
return Err(CheckError::Unbound {
signature: counter.node(),
});
};
let path = counters.counter(ordinal).path;
let target = path.members().last().ok_or(CheckError::Unbound {
signature: counter.node(),
})?;
let group = group(
prepared,
counter.node(),
properties,
target.certificate(),
provider,
operation,
&mut complete,
)?;
if !group.entries.is_empty() {
groups.try_push(group).map_err(|cause| {
processing(Error::resource(
cause,
"cades.credentials",
counter.node().span().start,
))
})?;
}
}
Ok(Report {
signature: selected,
root,
counters: groups.into_owned(),
complete,
})
}
fn preflight_properties<E>(
prepared: &Preparation<'_, '_>,
counters: &Paths<'_, '_, '_, '_>,
operation: &ScopedReader<'_, '_>,
) -> Result<(), CheckError<E>> {
if !prepared.signer().faults().is_empty()
|| !prepared.profile().faults().is_empty()
|| !prepared.profile().signing().faults().is_empty()
|| !prepared.properties().faults().is_empty()
{
return Err(CheckError::Nonconforming {
signature: prepared.document().locate(prepared.signer().span()).node,
});
}
for (ordinal, expected) in prepared.counters().iter().enumerate() {
operation
.charge(1)
.map_err(|cause| CheckError::Processing(Error::encoding(cause, "cades.credentials")))?;
if counters.counter(ordinal).signature != expected.node() {
return Err(CheckError::Processing(Error::Foreign {
at: expected.node().span().start,
}));
}
if expected.malformed().is_some()
|| expected
.properties()
.is_none_or(|value| !value.faults().is_empty())
{
return Err(CheckError::Nonconforming {
signature: expected.node(),
});
}
}
Ok(())
}
fn group<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
prepared: &Preparation<'m, 'd>,
signature: Node,
properties: &Properties<'d>,
signer: &Certificate<'d>,
provider: &P,
operation: &ScopedReader<'d, '_>,
complete: &mut bool,
) -> Result<Group<'m, 'd, 'p, E>, CheckError<P::Error>> {
let work = operation.execution();
let mut entries = work.vector();
for value in properties.open_values() {
work.charge_work(1).map_err(|cause| {
CheckError::Processing(Error::resource(
cause,
"cades.credentials",
signature.span().start,
))
})?;
if !matches!(
value.role,
OpenRole::AttributeCertificate
| OpenRole::OtherAttributeCertificate
| OpenRole::SignedAssertion
) {
continue;
}
let source = prepared.document().locate(value.enclosure.span());
let outcome = if value.value.is_none() {
*complete = false;
Outcome::Absent
} else {
match provider
.authenticate(Context {
source,
signature,
value: *value,
signer,
operation,
})
.map_err(|cause| CheckError::Provider {
node: source.node,
cause,
})? {
Authentication::Authenticated {
evidence,
authorities: Authorities::Paths([]),
} => {
*complete = false;
Outcome::MissingAuthorities(evidence)
}
Authentication::Unsupported => {
*complete = false;
Outcome::Authentication(Authentication::Unsupported)
}
Authentication::Rejected => {
*complete = false;
Outcome::Authentication(Authentication::Rejected)
}
authenticated @ Authentication::Authenticated { .. } => {
Outcome::Authentication(authenticated)
}
}
};
entries
.try_push(Checked { source, outcome })
.map_err(|cause| {
CheckError::Processing(Error::resource(
cause,
"cades.credentials",
source.node.span().start,
))
})?;
}
Ok(Group {
signature,
entries: entries.into_owned(),
})
}