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    // RFC5652 §5.3 requires both written attribute sets to be nonempty.
152    // Inspection can retain this unique malformed shape, but even unsigned
153    // emptiness must refuse before any provider resolution or payload access.
154    // https://www.rfc-editor.org/rfc/rfc5652#section-5.3
155    for attributes in [signer.signed_attributes(), signer.unsigned_attributes()]
156        .into_iter()
157        .flatten()
158    {
159        if let Some(fault) = attributes.fault() {
160            return Err(fault);
161        }
162    }
163    check_placements(signer, parent)?;
164    let Some(attributes) = signer.signed_attributes() else {
165        if let Scope::Content(kind) = scope {
166            parent
167                .execution()
168                .charge_work(kind.as_bytes().len() as u64)
169                .map_err(|reason| {
170                    Error::resource(reason, "signerInfo.signedAttrs", signer.span().start)
171                })?;
172            if kind != oid::DATA {
173                return Err(Error::schema(
174                    ErrorCode::MissingField,
175                    "signerInfo.signedAttrs",
176                    signer.span().start,
177                ));
178            }
179        }
180        return Ok(Binding::Direct);
181    };
182    let content_type = attributes.unique_in(oid::CONTENT_TYPE, parent)?;
183    match scope {
184        Scope::CounterSignature => {
185            if let Some(attribute) = content_type {
186                return Err(Error::schema(
187                    ErrorCode::AttributePlacement,
188                    "signerInfo.signedAttrs.contentType",
189                    attribute.span().start,
190                ));
191            }
192        }
193        Scope::Content(expected) => {
194            let attribute = content_type.ok_or_else(|| {
195                Error::schema(
196                    ErrorCode::MissingField,
197                    "signerInfo.signedAttrs.contentType",
198                    attributes.element().span().start,
199                )
200            })?;
201            attribute.single("signerInfo.signedAttrs.contentType")?;
202            let mut reader = value_reader(attribute, parent, "signerInfo.signedAttrs.contentType")?;
203            let declared = reader
204                .read_oid()
205                .map_err(|error| Error::encoding(error, "signerInfo.signedAttrs.contentType"))?;
206            parent
207                .execution()
208                .charge_work(declared.as_bytes().len() as u64)
209                .map_err(|reason| {
210                    Error::resource(
211                        reason,
212                        "signerInfo.signedAttrs.contentType",
213                        attribute.span().start,
214                    )
215                })?;
216            if declared != expected {
217                return Err(Error::schema(
218                    ErrorCode::ContentTypeMismatch,
219                    "signerInfo.signedAttrs.contentType",
220                    attribute.span().start,
221                ));
222            }
223        }
224    }
225    let attribute = attributes
226        .unique_in(oid::MESSAGE_DIGEST, parent)?
227        .ok_or_else(|| {
228            Error::schema(
229                ErrorCode::MissingField,
230                "signerInfo.signedAttrs.messageDigest",
231                attributes.element().span().start,
232            )
233        })?;
234    attribute.single("signerInfo.signedAttrs.messageDigest")?;
235    let mut reader = value_reader(attribute, parent, "signerInfo.signedAttrs.messageDigest")?;
236    let element = crate::signer::required(&mut reader, "signerInfo.signedAttrs.messageDigest")?;
237    let digest = Octets::read(&reader, &element, "signerInfo.signedAttrs.messageDigest")?;
238    // Signed values have already been held to DER; OCTET STRING therefore has
239    // exactly one primitive range. Borrow that range instead of retaining a
240    // redundant segmented representation in every verification plan.
241    let digest = digest.as_one().ok_or_else(|| {
242        Error::schema(
243            ErrorCode::UnexpectedTag,
244            "signerInfo.signedAttrs.messageDigest",
245            element.span().start,
246        )
247    })?;
248    check_signing_time(attributes, parent)?;
249    let protection = read_protection(attributes, parent)?;
250    Ok(Binding::Attributes {
251        digest,
252        digest_span: element.span(),
253        protection,
254    })
255}
256
257fn check_placements(signer: &SignerInfo<'_>, parent: &Reader<'_>) -> Result<()> {
258    if let Some(unsigned) = signer.unsigned_attributes() {
259        unsigned.placement_faults_in(parent, Err)?;
260    }
261    if let Some(signed) = signer.signed_attributes() {
262        signed.placement_faults_in(parent, Err)?;
263    }
264    Ok(())
265}
266
267fn check_signing_time(attributes: &Attributes<'_>, parent: &Reader<'_>) -> Result<()> {
268    const FIELD: &str = "signerInfo.signedAttrs.signingTime";
269    let Some(attribute) = attributes.unique_in(oid::SIGNING_TIME, parent)? else {
270        return Ok(());
271    };
272    let value = attribute.single(FIELD)?;
273    parent
274        .execution()
275        .charge_work(9)
276        .map_err(|reason| Error::resource(reason, FIELD, value.span().start))?;
277    // General DER time validation already uses the shared calendar core. CMS
278    // adds its own selected encoding contract: UTC for 1950..2049, generalized
279    // otherwise, and no fractional seconds (RFC5652 §11.3).
280    // https://www.rfc-editor.org/rfc/rfc5652#section-11.3
281    let valid = if value.tag() == Tag::primitive(universal::UTC_TIME) {
282        value.contents().len() == 13
283    } else if value.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
284        value.contents().len() == 15
285            && !(value.contents()[..4] >= b"1950"[..] && value.contents()[..4] <= b"2049"[..])
286    } else {
287        false
288    };
289    if !valid {
290        return Err(Error::schema(
291            ErrorCode::AttributeSyntax,
292            FIELD,
293            value.span().start,
294        ));
295    }
296    Ok(())
297}
298
299fn value_reader<'a, 'query>(
300    attribute: &Attribute<'a>,
301    parent: &Reader<'query>,
302    field: &'static str,
303) -> Result<Reader<'query>>
304where
305    'a: 'query,
306{
307    attribute.value_reader_in(0, parent, field)?.ok_or_else(|| {
308        Error::schema(
309            ErrorCode::AttributeCardinality,
310            field,
311            attribute.span().start,
312        )
313    })
314}
315
316fn read_protection<'a>(
317    attributes: &Attributes<'a>,
318    parent: &Reader<'a>,
319) -> Result<Option<Protection<'a>>> {
320    const FIELD: &str = "signerInfo.signedAttrs.cmsAlgorithmProtection";
321    let Some(attribute) = attributes.unique_in(oid::ALGORITHM_PROTECTION, parent)? else {
322        return Ok(None);
323    };
324    attribute.single(FIELD)?;
325    let mut reader = value_reader(attribute, parent, FIELD)?;
326    let element = reader
327        .expect(Tag::constructed(universal::SEQUENCE))
328        .map_err(|error| Error::encoding(error, FIELD))?;
329    let mut inside = reader
330        .enter(&element)
331        .map_err(|error| Error::encoding(error, FIELD))?;
332    let digest = crate::signer::required(&mut inside, FIELD)?;
333    let digest = structured_pkix::algorithm::read(&inside, &digest, FIELD).map_err(Error::pkix)?;
334    let signature = crate::signer::required(&mut inside, FIELD)?;
335    let signature = structured_pkix::algorithm::read_implicit(&inside, &signature, 1, FIELD)
336        .map_err(Error::pkix)?;
337    if !inside.is_empty() {
338        return Err(Error::schema(
339            ErrorCode::TrailingData,
340            FIELD,
341            inside.offset(),
342        ));
343    }
344    Ok(Some(Protection { digest, signature }))
345}