Skip to main content

structured_cms/timestamp/
authority.rs

1//! TSA certificate restrictions, distinct from signature/path/trust results.
2
3use super::TokenInfo;
4use crate::{Error, ErrorCode};
5use structured_asn1::{Reader, Tag, memory::Work, universal};
6use structured_pkix::{
7    Certificate,
8    general_name::{self, GeneralName},
9    oid,
10    usage::ExtendedKeyUsage,
11};
12
13/// Source in whose original coordinates a refusal is located.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AuthoritySource {
16    /// Original `TSTInfo` source, which may have a composed logical source map.
17    Token,
18    /// Original certificate source and its independently retained provenance.
19    Certificate,
20}
21
22/// Execution refusal, never a negative TSA identity or usage answer.
23#[derive(Debug)]
24pub struct AuthorityError {
25    /// Original source role, including independently parsed certificate input.
26    pub source: AuthoritySource,
27    /// Original common processing/resource cause.
28    pub cause: Error,
29}
30
31/// RFC3161 §2.3 requires exclusive critical timestamping EKU.
32#[derive(Debug)]
33pub enum TsaUsage {
34    /// The critical extension admits timestamping and nothing else.
35    TimeStampingOnly,
36    /// No EKU was written.
37    Missing,
38    /// The written EKU is not critical.
39    NotCritical,
40    /// The purposes do not exclusively admit timestamping.
41    OtherPurposes,
42    /// The certificate's original lazy extension cannot be interpreted.
43    Unreadable(Error),
44}
45
46/// Optional TSA subject-name agreement, without a trust assertion.
47#[derive(Debug)]
48pub enum TsaIdentity {
49    /// The optional name was absent, not a mandatory-field failure.
50    Absent,
51    /// A shared subject/SAN comparison matched, with the whole SAN readable.
52    Matches,
53    /// No subject/SAN value matched.
54    Differs,
55    /// Inspection retained an unreadable present TSA name.
56    Unreadable,
57    /// Original certificate SAN syntax prevents an identity answer.
58    CertificateUnreadable(Error),
59}
60
61/// Independent usage/name facts; neither proves certificate trust or signature.
62#[derive(Debug)]
63pub struct AuthorityFacts {
64    /// Certificate purpose restriction.
65    pub usage: TsaUsage,
66    /// Token name binding when one was supplied.
67    pub identity: TsaIdentity,
68}
69
70impl TokenInfo<'_> {
71    /// Checks two original sources without rebuilding or selecting a trust path.
72    ///
73    /// RFC3161 §§2.3/2.4.2 require exclusive critical EKU and correspondence to
74    /// a certificate subject name. The supplied certificate must separately be
75    /// selected, signature-verified and trusted by the caller's common CMS/PKIX
76    /// checks. Lookup/comparison use token query work; lazy certificate values
77    /// retain their own producing session/depth and its current execution.
78    /// <https://www.rfc-editor.org/rfc/rfc3161.html#section-2.3>
79    pub fn authority(
80        &self,
81        certificate: &Certificate<'_>,
82        token_parent: &Reader<'_>,
83        certificate_parent: &Reader<'_>,
84    ) -> Result<AuthorityFacts, AuthorityError> {
85        self.imprint()
86            .require_session(token_parent)
87            .map_err(token_error)?;
88        if !certificate
89            .tbs()
90            .public_key()
91            .algorithm()
92            .same_session(certificate_parent)
93        {
94            return Err(cert_error(Error::pkix(structured_pkix::Error::new(
95                structured_pkix::ErrorCode::ForeignReader,
96                "certificate",
97                certificate.span().start,
98            ))));
99        }
100        Ok(AuthorityFacts {
101            usage: usage(certificate, token_parent)?,
102            identity: identity(self, certificate, token_parent)?,
103        })
104    }
105}
106
107fn token_error(cause: Error) -> AuthorityError {
108    AuthorityError {
109        source: AuthoritySource::Token,
110        cause,
111    }
112}
113fn cert_error(cause: Error) -> AuthorityError {
114    AuthorityError {
115        source: AuthoritySource::Certificate,
116        cause,
117    }
118}
119
120fn usage(certificate: &Certificate<'_>, query: &Reader<'_>) -> Result<TsaUsage, AuthorityError> {
121    let Some(extension) = certificate
122        .tbs()
123        .extensions()
124        .get(oid::EXT_KEY_USAGE, &query.execution())
125        .map_err(Error::pkix)
126        .map_err(cert_error)?
127    else {
128        return Ok(TsaUsage::Missing);
129    };
130    if !extension.is_critical() {
131        return Ok(TsaUsage::NotCritical);
132    }
133    let purposes = match ExtendedKeyUsage::read_in(extension, query).map_err(Error::pkix) {
134        Ok(purposes) => purposes,
135        Err(error) if error.is_terminal() => return Err(cert_error(error)),
136        Err(error) => return Ok(TsaUsage::Unreadable(error)),
137    };
138    if purposes.purposes().len() != 1 {
139        return Ok(TsaUsage::OtherPurposes);
140    }
141    query
142        .execution()
143        .charge_work(1 + purposes.purposes()[0].as_bytes().len() as u64)
144        .map_err(|reason| {
145            cert_error(Error::resource(
146                reason,
147                "extensions.extKeyUsage",
148                extension.span().start,
149            ))
150        })?;
151    Ok(if purposes.purposes()[0] == oid::TIME_STAMPING {
152        TsaUsage::TimeStampingOnly
153    } else {
154        TsaUsage::OtherPurposes
155    })
156}
157
158fn identity(
159    info: &TokenInfo<'_>,
160    certificate: &Certificate<'_>,
161    query: &Reader<'_>,
162) -> Result<TsaIdentity, AuthorityError> {
163    let Some(tsa) = info.tsa() else {
164        return Ok(TsaIdentity::Absent);
165    };
166    let Some(name) = tsa.name() else {
167        return Ok(TsaIdentity::Unreadable);
168    };
169    if let GeneralName::Directory(name) = name
170        && name
171            .matches_in(certificate.tbs().subject(), query)
172            .map_err(|cause| {
173                Error::pkix(structured_pkix::Error::new(
174                    cause.code(),
175                    "tstInfo.tsa",
176                    tsa.span().start,
177                ))
178            })
179            .map_err(token_error)?
180    {
181        return Ok(TsaIdentity::Matches);
182    }
183    let Some(extension) = certificate
184        .tbs()
185        .extensions()
186        .get(oid::SUBJECT_ALT_NAME, &query.execution())
187        .map_err(Error::pkix)
188        .map_err(cert_error)?
189    else {
190        return Ok(TsaIdentity::Differs);
191    };
192    match san_matches(extension, name, tsa.span().start, query) {
193        Ok(true) => Ok(TsaIdentity::Matches),
194        Ok(false) => Ok(TsaIdentity::Differs),
195        // The comparer deliberately locates its operation at the token's TSA
196        // field; certificate decoding failures retain their certificate field.
197        Err(error) if error.field() == "tstInfo.tsa" => Err(token_error(error)),
198        Err(error) if error.is_terminal() => Err(cert_error(error)),
199        Err(error) => Ok(TsaIdentity::CertificateUnreadable(error)),
200    }
201}
202
203fn san_matches(
204    extension: &structured_pkix::Extension<'_>,
205    name: &GeneralName<'_>,
206    at: u64,
207    query: &Reader<'_>,
208) -> crate::Result<bool> {
209    const FIELD: &str = "extensions.subjectAltName";
210    let mut reader = extension
211        .reader_in(query)
212        .map_err(|source| Error::encoding(source, FIELD))?;
213    let sequence = reader
214        .expect(Tag::constructed(universal::SEQUENCE))
215        .map_err(|source| Error::encoding(source, FIELD))?;
216    if !reader.is_empty() {
217        return Err(Error::schema(
218            ErrorCode::TrailingData,
219            FIELD,
220            reader.offset(),
221        ));
222    }
223    let mut inside = reader
224        .enter(&sequence)
225        .map_err(|source| Error::encoding(source, FIELD))?;
226    let mut matched = false;
227    while let Some(element) = inside
228        .next_element()
229        .map_err(|source| Error::encoding(source, FIELD))?
230    {
231        let alternative = general_name::read(&inside, &element, FIELD).map_err(Error::pkix)?;
232        if !matched
233            && general_name::matches(query, name, &alternative, at, "tstInfo.tsa")
234                .map_err(Error::pkix)?
235        {
236            matched = true;
237        }
238    }
239    Ok(matched)
240}