structured-cms 0.12.0

Borrowed CMS signatures, digital envelopes and timestamps with explicit providers and resource accounting
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
//! RFC5755 §7.1 targeted encrypted attributes and ciphertext-stealing defense.
//!
//! The authenticated original AC contains `ContentInfo` values with independent
//! `EnvelopedData`. Its selected provider decrypts; the common core then checks
//! DER `ACClearAttrs`, issuer/serial and the original AA-control intersection.
//! Plaintext is a new logical coordinate space, never a physical ciphertext
//! offset or invertible source map. No encryption engine or key store is here.
//! <https://www.rfc-editor.org/rfc/rfc5755#section-7.1>

use super::{
    AttributeCertificate,
    validation::usage::{AttributeUse, Checked},
};
use crate::{
    Error, ErrorCode, Result,
    attribute::Attribute,
    enveloped::{EnvelopedData, oid_is},
    signer::required,
};
use structured_asn1::memory::{Execution, OwnedVec, Work};
use structured_asn1::{Element, Encoding, Integer, Oid, Reader, Span, Tag, universal};
use structured_pkix::general_name::{self, GeneralName};

/// id-aca-encAttrs, RFC5755 Appendix A.
pub const IDENTIFIER: Oid<'static> = Oid::from_bytes(&[43, 6, 1, 5, 5, 7, 10, 6]);
/// id-ct-attrCertEncAttrs, identifying the exact decrypted content syntax.
pub const CONTENT_TYPE: Oid<'static> =
    Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 9, 16, 1, 14]);
const FIELD: &str = "attributeCertificate.encAttrs";
const CLEAR: &str = "acClearAttrs";

/// DER plaintext inspection, not an authenticated/bound credential by itself.
#[derive(Debug)]
pub struct ClearAttrs<'a> {
    /// Original plaintext issuer name, to be bound before use.
    pub issuer: GeneralName<'a>,
    /// Full signed INTEGER, never truncated or projected through a sentinel.
    pub serial: Integer<'a>,
    /// Every decrypted attribute in original order. Duplicate types remain
    /// distinct: RFC5755 §7.1 explicitly allows clear/encrypted/multiple sets.
    pub attributes: OwnedVec<Attribute<'a>>,
    element: Element<'a>,
}
impl<'a> ClearAttrs<'a> {
    /// Read the entire required DER plaintext, including its original header.
    /// An outer BER reader does not relax RFC5755 §7.1's plaintext DER rule.
    pub fn read(parent: &mut Reader<'a>) -> Result<Self> {
        let element = parent
            .expect(Tag::constructed(universal::SEQUENCE))
            .map_err(|e| Error::encoding(e, CLEAR))?;
        let mut der = parent
            .reread(&element, Encoding::Der)
            .map_err(|e| Error::encoding(e, CLEAR))?;
        let element = der
            .expect(Tag::constructed(universal::SEQUENCE))
            .map_err(|e| Error::encoding(e, CLEAR))?;
        let mut inside = der.enter(&element).map_err(|e| Error::encoding(e, CLEAR))?;
        let value = required(&mut inside, "acClearAttrs.acIssuer")?;
        let issuer =
            general_name::read(&inside, &value, "acClearAttrs.acIssuer").map_err(Error::pkix)?;
        let serial = inside
            .read_integer()
            .map_err(|e| Error::encoding(e, "acClearAttrs.acSerial"))?;
        let sequence = inside
            .expect(Tag::constructed(universal::SEQUENCE))
            .map_err(|e| Error::encoding(e, "acClearAttrs.attrs"))?;
        let mut members = inside
            .enter(&sequence)
            .map_err(|e| Error::encoding(e, CLEAR))?;
        let work = inside.execution();
        let mut attributes = work.vector();
        while let Some(element) = members
            .next_element()
            .map_err(|e| Error::encoding(e, CLEAR))?
        {
            attributes
                .try_reserve(1)
                .map_err(|e| Error::resource(e, CLEAR, element.span().start))?;
            let value = Attribute::read(&members, &element, "acClearAttrs.attrs")?;
            attributes
                .try_push(value)
                .map_err(|e| Error::resource(e, CLEAR, element.span().start))?;
        }
        end(&inside)?;
        end(parent)?;
        Ok(Self {
            issuer,
            serial,
            attributes: attributes.into_owned(),
            element,
        })
    }
    /// Original logical plaintext range, not an offset into encrypted bytes.
    #[must_use]
    pub fn element(&self) -> &Element<'a> {
        &self.element
    }
}

/// One independent decryption result from the explicitly selected provider.
/// Capability/recipient/cryptographic rejection are completed answers, while
/// original processing/resource/backend errors use `Result::Err`.
pub enum Attempt<T> {
    /// The selected provider completed decryption and removed normative padding.
    Decrypted(T),
    /// No selected key/recipient applies to this envelope.
    NotRecipient,
    /// The selected provider cannot process its algorithms/recipient format.
    Unsupported,
    /// The selected provider completed and rejected its ciphertext/key operation.
    Rejected,
}
/// Completed negative plaintext binding. No attribute-use receipt exists.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rejection {
    /// Provider completed without a valid decryption result.
    Decryption,
    /// Actual issuer name differs; ciphertext cannot be transplanted across AAs.
    Issuer,
    /// Full INTEGER differs; ciphertext cannot be transplanted across ACs.
    Serial,
}
/// One original envelope's complete result, preserving unavailable versus false.
#[derive(Debug)]
pub enum Outcome<T> {
    /// Bound plaintext was delivered once; R owns any explicitly retained output.
    Processed(T),
    /// No selected recipient applies, without trying another engine implicitly.
    NotRecipient,
    /// Unsupported is neither malformed plaintext nor an empty successful set.
    Unsupported,
    /// Complete negative answer, without a successful plaintext prefix.
    Rejected(Rejection),
}
/// Logical plaintext context retained independently of temporary decrypted bytes.
/// `envelope` is in the enclosing AC's source coordinates. Any error produced by
/// the plaintext reader uses zero-based plaintext coordinates, not that source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlaintextSource {
    /// Original independent encAttrs value ordinal.
    pub value: usize,
    /// Whole original `EnvelopedData` enclosure; no inner offset projection exists.
    pub envelope: Span,
}
/// Terminal refusal, preserving the callback cause and its honest source space.
#[derive(Debug)]
pub enum DecryptError<P, V> {
    /// Original AC/source/current-operation refusal (not plaintext coordinates).
    Processing(Error),
    /// Selected provider's unchanged cause at this original envelope.
    Provider {
        /// Original encrypted source range.
        envelope: Span,
        /// Unchanged provider/source/resource cause.
        cause: P,
    },
    /// Plaintext syntax/resource refusal in the explicitly marked logical space.
    Plaintext {
        /// Unchanged located common failure; its offset is plaintext-relative.
        cause: Error,
        /// Original encrypted enclosure and coordinate discriminator.
        source: PlaintextSource,
    },
    /// Visitor's unchanged error, never swallowed after successful decryption.
    Visitor {
        /// Caller-controlled error; reader-derived offsets use plaintext space.
        cause: V,
        /// Same original enclosure and explicit logical-source context.
        source: PlaintextSource,
    },
}
/// Temporarily bound plaintext. Its type cannot escape the callback, and a
/// readable-but-unbound `ACClearAttrs` cannot be passed in its place.
pub struct Bound<'s> {
    clear: ClearAttrs<'s>,
    attributes: OwnedVec<AttributeUse>,
    source: PlaintextSource,
}
impl Bound<'_> {
    /// Every original decrypted attribute, including duplicate types.
    #[must_use]
    pub fn clear(&self) -> &ClearAttrs<'_> {
        &self.clear
    }
    /// One original AA-control answer per decrypted attribute, in source order.
    #[must_use]
    pub fn attributes(&self) -> &[AttributeUse] {
        &self.attributes
    }
    /// Explicit encrypted enclosure/zero-based plaintext coordinate context.
    #[must_use]
    pub fn source(&self) -> PlaintextSource {
        self.source
    }
}
/// Complete catalogue of independent original targeted envelopes. Reading all
/// values precedes the first provider call, so malformed later metadata cannot
/// leave a successful decrypted prefix. It authenticates no AC on its own.
pub struct Encrypted<'r, 'a> {
    certificate: &'r AttributeCertificate<'a>,
    envelopes: OwnedVec<EnvelopedData<'a>>,
}
impl<'r, 'a> Encrypted<'r, 'a> {
    /// Read the original unique encAttrs attribute's entire value set. Returns
    /// None only when this actual AC has no such attribute. Same producing
    /// Parser is required before lookup/work, not merely the same account.
    pub fn read(
        certificate: &'r AttributeCertificate<'a>,
        parent: &Reader<'a>,
    ) -> Result<Option<Self>> {
        if !core::ptr::eq(certificate.parser, parent.parser()) {
            return Err(Error::pkix(structured_pkix::Error::new(
                structured_pkix::ErrorCode::ForeignReader,
                FIELD,
                certificate.element().span().start,
            )));
        }
        let work = parent.execution();
        let mut selected = None;
        for attribute in &certificate.information().attributes {
            work.charge_work(1)
                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
            if attribute.kind().as_bytes().len() != IDENTIFIER.as_bytes().len() {
                continue;
            }
            work.charge_work(IDENTIFIER.as_bytes().len() as u64)
                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
            if attribute.kind() == IDENTIFIER {
                selected = Some(attribute);
                break;
            }
        }
        let Some(attribute) = selected else {
            return Ok(None);
        };
        let mut envelopes = work.vector();
        envelopes
            .try_reserve_exact(attribute.values().len())
            .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
        for ordinal in 0..attribute.values().len() {
            let mut reader = attribute
                .value_reader_in(ordinal, parent, FIELD)?
                .ok_or_else(|| {
                    Error::schema(ErrorCode::InvalidLocation, FIELD, attribute.span().start)
                })?;
            let envelope = EnvelopedData::read(&mut reader)?;
            oid_is(
                envelope.content.kind,
                CONTENT_TYPE,
                &reader,
                FIELD,
                envelope.element().span().start,
            )?;
            if envelope.content.ciphertext.is_none() {
                return Err(Error::schema(
                    ErrorCode::DetachedContent,
                    FIELD,
                    envelope.element().span().start,
                ));
            }
            envelopes
                .try_push(envelope)
                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
        }
        Ok(Some(Self {
            certificate,
            envelopes: envelopes.into_owned(),
        }))
    }
    /// All original envelopes. Caller inspection of borrowed metadata/bytes must
    /// account its own scans; `decrypt_all` admits all of its own traversal.
    #[must_use]
    pub fn envelopes(&self) -> &[EnvelopedData<'a>] {
        &self.envelopes
    }

    /// Process every independent value through the selected provider and a
    /// scoped visitor. Decryption receives original fragmented ciphertext and
    /// must admit its backend work/output through the supplied Execution. The
    /// returned owned plaintext must belong to that same operation account.
    ///
    /// DER parsing inherits the actual ciphertext's original depth and source
    /// and operation ceilings, without a new `Parser` or copied-buffer reset.
    /// Plaintext and parsed models are dropped after the visitor. R/V/P cannot
    /// borrow these temporary values. A late error drops all retained R/results;
    /// external provider/visitor side effects cannot be rolled back.
    ///
    /// The visitor cannot return a borrowed plaintext view. Its higher-ranked
    /// scope is the same restriction illustrated here:
    /// ```compile_fail
    /// use structured_cms::attribute_certificate::encrypted::Bound;
    /// fn scoped<R>(_: impl for<'s> Fn(&Bound<'s>) -> R) {}
    /// scoped(|bound| bound.clear().element().as_bytes());
    /// ```
    pub fn decrypt_all<'p, 'd, 'c, S, P, V, R>(
        &self,
        checked: &Checked<'_, 'p, 'd, 'c, S>,
        parent: &Reader<'_>,
        mut provider: impl FnMut(
            &EnvelopedData<'a>,
            &Execution<'_>,
        ) -> core::result::Result<Attempt<OwnedVec<u8>>, P>,
        mut visitor: impl for<'scope> FnMut(
            &Bound<'scope>,
            &Execution<'_>,
        ) -> core::result::Result<R, V>,
    ) -> core::result::Result<OwnedVec<Outcome<R>>, DecryptError<P, V>> {
        let physical = DecryptError::Processing;
        if !core::ptr::eq(self.certificate.parser, parent.parser())
            || !core::ptr::eq(self.certificate, checked.certificate())
        {
            return Err(physical(Error::schema(
                ErrorCode::InvalidLocation,
                FIELD,
                self.certificate.element().span().start,
            )));
        }
        let work = parent.execution();
        let mut results = work.vector();
        results
            .try_reserve_exact(self.envelopes.len())
            .map_err(|e| {
                physical(Error::resource(
                    e,
                    FIELD,
                    self.certificate.element().span().start,
                ))
            })?;
        for (ordinal, envelope) in self.envelopes.iter().enumerate() {
            work.charge_work(1).map_err(|e| {
                physical(Error::resource(e, FIELD, envelope.element().span().start))
            })?;
            let output = provider(envelope, &work).map_err(|cause| DecryptError::Provider {
                envelope: envelope.element().span(),
                cause,
            })?;
            let outcome = match output {
                Attempt::NotRecipient => Outcome::NotRecipient,
                Attempt::Unsupported => Outcome::Unsupported,
                Attempt::Rejected => Outcome::Rejected(Rejection::Decryption),
                Attempt::Decrypted(bytes) => {
                    if !bytes.memory().same_account(parent.memory()) {
                        return Err(physical(Error::schema(
                            ErrorCode::InvalidLocation,
                            FIELD,
                            envelope.element().span().start,
                        )));
                    }
                    self.visit(checked, parent, envelope, ordinal, &bytes, &mut visitor)?
                }
            };
            results.try_push(outcome).map_err(|e| {
                physical(Error::resource(e, FIELD, envelope.element().span().start))
            })?;
        }
        Ok(results.into_owned())
    }

    fn visit<'p, 'd, 'c, S, P, V, R>(
        &self,
        checked: &Checked<'_, 'p, 'd, 'c, S>,
        parent: &Reader<'_>,
        envelope: &EnvelopedData<'a>,
        ordinal: usize,
        bytes: &[u8],
        visitor: &mut impl for<'scope> FnMut(
            &Bound<'scope>,
            &Execution<'_>,
        ) -> core::result::Result<R, V>,
    ) -> core::result::Result<Outcome<R>, DecryptError<P, V>> {
        let source = PlaintextSource {
            value: ordinal,
            envelope: envelope.element().span(),
        };
        let logical = |cause| DecryptError::Plaintext { cause, source };
        // The ciphertext was required in the complete preflight, and no mutable
        // envelope access is exposed by Encrypted. It contributes its actual
        // nested source level, just as other carried ASN.1 payloads do.
        let ciphertext = envelope
            .content
            .ciphertext
            .as_ref()
            .expect("encAttrs requires attached ciphertext");
        let mut reader = self
            .certificate
            .parser
            .read_nested_query_in(parent, bytes, 0, Encoding::Der, ciphertext.outer_depth())
            .map_err(|e| logical(Error::encoding(e, CLEAR)))?;
        let clear = ClearAttrs::read(&mut reader).map_err(logical)?;
        let work = reader.execution();
        let ac = self.certificate;
        work.charge_work(3)
            .map_err(|e| logical(Error::resource(e, CLEAR, clear.element.span().start)))?;
        let left = clear.serial.as_bytes();
        let right = ac.information().serial.as_bytes();
        // Both are checked DER INTEGERs. Exact minimal two's-complement octets
        // therefore implement full numeric equality, including wide/negative
        // values, without a scalar projection or sentinel.
        if left.len() != right.len() {
            return Ok(Outcome::Rejected(Rejection::Serial));
        }
        work.charge_work(left.len() as u64 + right.len() as u64)
            .map_err(|e| {
                logical(Error::resource(
                    e,
                    "acClearAttrs.acSerial",
                    clear.element.span().start,
                ))
            })?;
        if left != right {
            return Ok(Outcome::Rejected(Rejection::Serial));
        }
        // A successful original RFC5755 profile has exactly one issuer DN.
        // Prepare operands separately so original-name failures keep physical
        // AC coordinates and plaintext failures stay explicitly logical.
        let Some([GeneralName::Directory(actual)]) = ac.information().issuer.names.as_deref()
        else {
            unreachable!("authenticated profile restricts issuer to one directoryName")
        };
        let GeneralName::Directory(claimed) = &clear.issuer else {
            return Ok(Outcome::Rejected(Rejection::Issuer));
        };
        let actual = actual
            .ordered(&work)
            .map_err(|e| DecryptError::Processing(Error::pkix(e)))?;
        let claimed = claimed
            .ordered_in(&reader)
            .map_err(|e| logical(Error::pkix(e)))?;
        if claimed
            .order(&actual, &work)
            .map_err(|e| logical(Error::pkix(e)))?
            != core::cmp::Ordering::Equal
        {
            return Ok(Outcome::Rejected(Rejection::Issuer));
        }
        let mut attributes = work.vector();
        attributes
            .try_reserve_exact(clear.attributes.len())
            .map_err(|e| logical(Error::resource(e, CLEAR, clear.element.span().start)))?;
        for (index, attribute) in clear.attributes.iter().enumerate() {
            work.charge_work(1)
                .map_err(|e| logical(Error::resource(e, CLEAR, attribute.span().start)))?;
            // AA-control failures originate in the original authenticated AA
            // source, not in a guessed plaintext-to-ciphertext mapping.
            let permission = checked
                .allows(attribute.kind(), &work)
                .map_err(DecryptError::Processing)?;
            attributes
                .try_push(AttributeUse { index, permission })
                .map_err(|e| logical(Error::resource(e, CLEAR, attribute.span().start)))?;
        }
        let bound = Bound {
            clear,
            attributes: attributes.into_owned(),
            source,
        };
        let output =
            visitor(&bound, &work).map_err(|cause| DecryptError::Visitor { cause, source })?;
        Ok(Outcome::Processed(output))
    }
}
fn end(parent: &Reader<'_>) -> Result<()> {
    if parent.is_empty() {
        Ok(())
    } else {
        Err(Error::schema(
            ErrorCode::TrailingData,
            CLEAR,
            parent.offset(),
        ))
    }
}