Skip to main content

structured_cms/
binding.rs

1//! Signed content binding and algorithm protection before cryptographic callbacks.
2
3use crate::attribute::{Attribute, Attributes};
4use crate::signer::SignerInfo;
5use crate::{Error, ErrorCode, Octets, Result, oid};
6use structured_asn1::memory::Work;
7use structured_asn1::{Oid, Reader, Span, Tag, universal};
8use structured_pkix::AlgorithmIdentifier;
9
10/// Result of comparing a declared imprint with complete typed digest bytes.
11#[derive(Debug)]
12pub enum DigestBinding {
13    /// Admitted bytes agree under the selected provider's comparison contract.
14    Matches,
15    /// Admitted, correctly sized bytes differ.
16    Differs,
17    /// The declared value has an impossible width; no comparison callback ran.
18    Malformed(Error),
19}
20
21/// Execution failure distinct from a malformed imprint or byte mismatch.
22#[derive(Debug)]
23pub enum BindError<E> {
24    /// Common admission failed at the declared value's original location.
25    Processing(Error),
26    /// Original comparison callback failure, including backend resource errors.
27    Callback(E),
28    /// A completed output conflicts with the selected identity's fixed width.
29    OutputWidth {
30        /// Width promised by the full resolved identity.
31        expected: usize,
32        /// Actual completed output size.
33        actual: usize,
34    },
35}
36
37/// Checks actual output width before comparing a declared digest.
38///
39/// This applies even when `digest_width` is None. `field`/`span` identify the
40/// original declaration, not the output or a synthesized DER element. Provider
41/// instance identity is checked before work or callback. The borrowed provider
42/// retained by the finalized output cannot be replaced or moved while in use.
43/// The callback
44/// receives bytes already finalized; no resolver, hashing or copy is performed.
45pub fn matches_digest<'a, P: crate::provider::DigestProvider<'a>, W: Work>(
46    provider: &P,
47    digest: crate::provider::Prehash<'_, '_, P, P::Identity>,
48    expected: &[u8],
49    execution: &W,
50    field: &'static str,
51    span: Span,
52) -> core::result::Result<DigestBinding, BindError<P::Error>> {
53    crate::provider::require_instance(provider, field, span.start)
54        .map_err(BindError::Processing)?;
55    if !digest.from_provider(provider) {
56        return Err(BindError::Processing(Error::schema(
57            ErrorCode::ProviderMismatch,
58            field,
59            span.start,
60        )));
61    }
62    execution
63        .charge_work(1)
64        .map_err(|reason| BindError::Processing(Error::resource(reason, field, span.start)))?;
65    let actual = digest.as_bytes().len();
66    if let Some(width) = provider.digest_width(digest.identity())
67        && width != actual
68    {
69        return Err(BindError::OutputWidth {
70            expected: width,
71            actual,
72        });
73    }
74    if expected.len() != actual {
75        return Ok(DigestBinding::Malformed(Error::schema(
76            ErrorCode::DigestWidth,
77            field,
78            span.start,
79        )));
80    }
81    if provider
82        .matches_digest(digest, expected, execution)
83        .map_err(BindError::Callback)?
84    {
85        Ok(DigestBinding::Matches)
86    } else {
87        Ok(DigestBinding::Differs)
88    }
89}
90
91/// The enclosing protocol selects what this signer covers.
92#[derive(Debug, Clone, Copy)]
93pub enum Scope<'a> {
94    /// Normal `SignedData` content type, from its enclosing content-info.
95    Content(Oid<'a>),
96    /// A countersignature covers the parent signature octets, not message content.
97    CounterSignature,
98}
99
100/// Original identifiers covered by `CMSAlgorithmProtection` (RFC6211 §2).
101#[derive(Debug, Clone, Copy)]
102pub struct Protection<'a> {
103    digest: AlgorithmIdentifier<'a>,
104    signature: AlgorithmIdentifier<'a>,
105}
106
107impl<'a> Protection<'a> {
108    /// Original protected digest identifier; defaults need selected-provider resolution.
109    #[must_use]
110    pub fn digest(&self) -> AlgorithmIdentifier<'a> {
111        self.digest
112    }
113    /// Original protected signature identifier, retaining its IMPLICIT `[1]` tag.
114    #[must_use]
115    pub fn signature(&self) -> AlgorithmIdentifier<'a> {
116        self.signature
117    }
118}
119
120/// Content bytes are signed directly or bound through original signed attributes.
121#[derive(Debug)]
122pub enum Binding<'a> {
123    /// The signature input is the logical content (or parent signature) itself.
124    Direct,
125    /// Signed attributes bind the content digest; signature input is their original SET spelling.
126    Attributes {
127        /// Original expected digest octets; no algorithm is inferred from length.
128        digest: &'a [u8],
129        /// Original digest value's source range for subsequent comparison failures.
130        digest_span: Span,
131        /// Original algorithm protection, when present. Semantic comparison remains mandatory.
132        protection: Option<Protection<'a>>,
133    },
134}
135
136/// Checks content/type placement and singleton syntax before provider dispatch or payload I/O.
137///
138/// RFC5652 §§5.3, 11.1–11.4 requires content-type/message-digest for ordinary
139/// signed attributes, forbids content-type in countersignatures, and requires
140/// attributes for non-id-data. RFC6211 §2 requires one signed protection value
141/// and the signature alternative in `SignedData`. This returns binding facts,
142/// not a signature/trust verdict; protected identities still need the provider.
143/// <https://www.rfc-editor.org/rfc/rfc5652#section-11>
144/// <https://www.rfc-editor.org/rfc/rfc6211#section-2>
145pub fn inspect<'a>(
146    signer: &SignerInfo<'a>,
147    parent: &Reader<'a>,
148    scope: Scope<'a>,
149) -> Result<Binding<'a>> {
150    signer.require_session(parent, "signerInfo")?;
151    check_placements(signer, parent)?;
152    let Some(attributes) = signer.signed_attributes() else {
153        if let Scope::Content(kind) = scope {
154            parent
155                .execution()
156                .charge_work(kind.as_bytes().len() as u64)
157                .map_err(|reason| {
158                    Error::resource(reason, "signerInfo.signedAttrs", signer.span().start)
159                })?;
160            if kind != oid::DATA {
161                return Err(Error::schema(
162                    ErrorCode::MissingField,
163                    "signerInfo.signedAttrs",
164                    signer.span().start,
165                ));
166            }
167        }
168        return Ok(Binding::Direct);
169    };
170    let content_type = attributes.unique_in(oid::CONTENT_TYPE, parent)?;
171    match scope {
172        Scope::CounterSignature => {
173            if let Some(attribute) = content_type {
174                return Err(Error::schema(
175                    ErrorCode::AttributePlacement,
176                    "signerInfo.signedAttrs.contentType",
177                    attribute.span().start,
178                ));
179            }
180        }
181        Scope::Content(expected) => {
182            let attribute = content_type.ok_or_else(|| {
183                Error::schema(
184                    ErrorCode::MissingField,
185                    "signerInfo.signedAttrs.contentType",
186                    attributes.element().span().start,
187                )
188            })?;
189            attribute.single("signerInfo.signedAttrs.contentType")?;
190            let mut reader = value_reader(attribute, parent, "signerInfo.signedAttrs.contentType")?;
191            let declared = reader
192                .read_oid()
193                .map_err(|error| Error::encoding(error, "signerInfo.signedAttrs.contentType"))?;
194            parent
195                .execution()
196                .charge_work(declared.as_bytes().len() as u64)
197                .map_err(|reason| {
198                    Error::resource(
199                        reason,
200                        "signerInfo.signedAttrs.contentType",
201                        attribute.span().start,
202                    )
203                })?;
204            if declared != expected {
205                return Err(Error::schema(
206                    ErrorCode::ContentTypeMismatch,
207                    "signerInfo.signedAttrs.contentType",
208                    attribute.span().start,
209                ));
210            }
211        }
212    }
213    let attribute = attributes
214        .unique_in(oid::MESSAGE_DIGEST, parent)?
215        .ok_or_else(|| {
216            Error::schema(
217                ErrorCode::MissingField,
218                "signerInfo.signedAttrs.messageDigest",
219                attributes.element().span().start,
220            )
221        })?;
222    attribute.single("signerInfo.signedAttrs.messageDigest")?;
223    let mut reader = value_reader(attribute, parent, "signerInfo.signedAttrs.messageDigest")?;
224    let element = crate::signer::required(&mut reader, "signerInfo.signedAttrs.messageDigest")?;
225    let digest = Octets::read(&reader, &element, "signerInfo.signedAttrs.messageDigest")?;
226    // Signed values have already been held to DER; OCTET STRING therefore has
227    // exactly one primitive range. Borrow that range instead of retaining a
228    // redundant segmented representation in every verification plan.
229    let digest = digest.as_one().ok_or_else(|| {
230        Error::schema(
231            ErrorCode::UnexpectedTag,
232            "signerInfo.signedAttrs.messageDigest",
233            element.span().start,
234        )
235    })?;
236    check_signing_time(attributes, parent)?;
237    let protection = read_protection(attributes, parent)?;
238    Ok(Binding::Attributes {
239        digest,
240        digest_span: element.span(),
241        protection,
242    })
243}
244
245fn check_placements(signer: &SignerInfo<'_>, parent: &Reader<'_>) -> Result<()> {
246    if let Some(unsigned) = signer.unsigned_attributes() {
247        // RFC5652 §§11.1–11.3 and RFC6211 §2 define these as signed attributes.
248        // https://www.rfc-editor.org/rfc/rfc5652#section-11
249        for kind in [
250            oid::CONTENT_TYPE,
251            oid::MESSAGE_DIGEST,
252            oid::SIGNING_TIME,
253            oid::ALGORITHM_PROTECTION,
254        ] {
255            if let Some(result) = unsigned.all_in(kind, parent).next() {
256                let attribute = result?;
257                return Err(Error::schema(
258                    ErrorCode::AttributePlacement,
259                    "signerInfo.unsignedAttrs",
260                    attribute.span().start,
261                ));
262            }
263        }
264    }
265    if let Some(signed) = signer.signed_attributes()
266        && let Some(result) = signed.all_in(oid::COUNTERSIGNATURE, parent).next()
267    {
268        let attribute = result?;
269        return Err(Error::schema(
270            ErrorCode::AttributePlacement,
271            "signerInfo.signedAttrs.counterSignature",
272            attribute.span().start,
273        ));
274    }
275    Ok(())
276}
277
278fn check_signing_time(attributes: &Attributes<'_>, parent: &Reader<'_>) -> Result<()> {
279    const FIELD: &str = "signerInfo.signedAttrs.signingTime";
280    let Some(attribute) = attributes.unique_in(oid::SIGNING_TIME, parent)? else {
281        return Ok(());
282    };
283    let value = attribute.single(FIELD)?;
284    parent
285        .execution()
286        .charge_work(9)
287        .map_err(|reason| Error::resource(reason, FIELD, value.span().start))?;
288    // General DER time validation already uses the shared calendar core. CMS
289    // adds its own selected encoding contract: UTC for 1950..2049, generalized
290    // otherwise, and no fractional seconds (RFC5652 §11.3).
291    // https://www.rfc-editor.org/rfc/rfc5652#section-11.3
292    let valid = if value.tag() == Tag::primitive(universal::UTC_TIME) {
293        value.contents().len() == 13
294    } else if value.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
295        value.contents().len() == 15
296            && !(value.contents()[..4] >= b"1950"[..] && value.contents()[..4] <= b"2049"[..])
297    } else {
298        false
299    };
300    if !valid {
301        return Err(Error::schema(
302            ErrorCode::AttributeSyntax,
303            FIELD,
304            value.span().start,
305        ));
306    }
307    Ok(())
308}
309
310fn value_reader<'a, 'query>(
311    attribute: &Attribute<'a>,
312    parent: &Reader<'query>,
313    field: &'static str,
314) -> Result<Reader<'query>>
315where
316    'a: 'query,
317{
318    attribute.value_reader_in(0, parent, field)?.ok_or_else(|| {
319        Error::schema(
320            ErrorCode::AttributeCardinality,
321            field,
322            attribute.span().start,
323        )
324    })
325}
326
327fn read_protection<'a>(
328    attributes: &Attributes<'a>,
329    parent: &Reader<'a>,
330) -> Result<Option<Protection<'a>>> {
331    const FIELD: &str = "signerInfo.signedAttrs.cmsAlgorithmProtection";
332    let Some(attribute) = attributes.unique_in(oid::ALGORITHM_PROTECTION, parent)? else {
333        return Ok(None);
334    };
335    attribute.single(FIELD)?;
336    let mut reader = value_reader(attribute, parent, FIELD)?;
337    let element = reader
338        .expect(Tag::constructed(universal::SEQUENCE))
339        .map_err(|error| Error::encoding(error, FIELD))?;
340    let mut inside = reader
341        .enter(&element)
342        .map_err(|error| Error::encoding(error, FIELD))?;
343    let digest = crate::signer::required(&mut inside, FIELD)?;
344    let digest = structured_pkix::algorithm::read(&inside, &digest, FIELD).map_err(Error::pkix)?;
345    let signature = crate::signer::required(&mut inside, FIELD)?;
346    let signature = structured_pkix::algorithm::read_implicit(&inside, &signature, 1, FIELD)
347        .map_err(Error::pkix)?;
348    if !inside.is_empty() {
349        return Err(Error::schema(
350            ErrorCode::TrailingData,
351            FIELD,
352            inside.offset(),
353        ));
354    }
355    Ok(Some(Protection { digest, signature }))
356}