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