Skip to main content

structured_cms/attribute_certificate/
encrypted.rs

1//! RFC5755 §7.1 targeted encrypted attributes and ciphertext-stealing defense.
2//!
3//! The authenticated original AC contains `ContentInfo` values with independent
4//! `EnvelopedData`. Its selected provider decrypts; the common core then checks
5//! DER `ACClearAttrs`, issuer/serial and the original AA-control intersection.
6//! Plaintext is a new logical coordinate space, never a physical ciphertext
7//! offset or invertible source map. No encryption engine or key store is here.
8//! <https://www.rfc-editor.org/rfc/rfc5755#section-7.1>
9
10use super::{
11    AttributeCertificate,
12    validation::usage::{AttributeUse, Checked},
13};
14use crate::{
15    Error, ErrorCode, Result,
16    attribute::Attribute,
17    enveloped::{EnvelopedData, oid_is},
18    signer::required,
19};
20use structured_asn1::memory::{Execution, OwnedVec, Work};
21use structured_asn1::{Element, Encoding, Integer, Oid, Reader, Span, Tag, universal};
22use structured_pkix::general_name::{self, GeneralName};
23
24/// id-aca-encAttrs, RFC5755 Appendix A.
25pub const IDENTIFIER: Oid<'static> = Oid::from_bytes(&[43, 6, 1, 5, 5, 7, 10, 6]);
26/// id-ct-attrCertEncAttrs, identifying the exact decrypted content syntax.
27pub const CONTENT_TYPE: Oid<'static> =
28    Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 9, 16, 1, 14]);
29const FIELD: &str = "attributeCertificate.encAttrs";
30const CLEAR: &str = "acClearAttrs";
31
32/// DER plaintext inspection, not an authenticated/bound credential by itself.
33#[derive(Debug)]
34pub struct ClearAttrs<'a> {
35    /// Original plaintext issuer name, to be bound before use.
36    pub issuer: GeneralName<'a>,
37    /// Full signed INTEGER, never truncated or projected through a sentinel.
38    pub serial: Integer<'a>,
39    /// Every decrypted attribute in original order. Duplicate types remain
40    /// distinct: RFC5755 §7.1 explicitly allows clear/encrypted/multiple sets.
41    pub attributes: OwnedVec<Attribute<'a>>,
42    element: Element<'a>,
43}
44impl<'a> ClearAttrs<'a> {
45    /// Read the entire required DER plaintext, including its original header.
46    /// An outer BER reader does not relax RFC5755 §7.1's plaintext DER rule.
47    pub fn read(parent: &mut Reader<'a>) -> Result<Self> {
48        let element = parent
49            .expect(Tag::constructed(universal::SEQUENCE))
50            .map_err(|e| Error::encoding(e, CLEAR))?;
51        let mut der = parent
52            .reread(&element, Encoding::Der)
53            .map_err(|e| Error::encoding(e, CLEAR))?;
54        let element = der
55            .expect(Tag::constructed(universal::SEQUENCE))
56            .map_err(|e| Error::encoding(e, CLEAR))?;
57        let mut inside = der.enter(&element).map_err(|e| Error::encoding(e, CLEAR))?;
58        let value = required(&mut inside, "acClearAttrs.acIssuer")?;
59        let issuer =
60            general_name::read(&inside, &value, "acClearAttrs.acIssuer").map_err(Error::pkix)?;
61        let serial = inside
62            .read_integer()
63            .map_err(|e| Error::encoding(e, "acClearAttrs.acSerial"))?;
64        let sequence = inside
65            .expect(Tag::constructed(universal::SEQUENCE))
66            .map_err(|e| Error::encoding(e, "acClearAttrs.attrs"))?;
67        let mut members = inside
68            .enter(&sequence)
69            .map_err(|e| Error::encoding(e, CLEAR))?;
70        let work = inside.execution();
71        let mut attributes = work.vector();
72        while let Some(element) = members
73            .next_element()
74            .map_err(|e| Error::encoding(e, CLEAR))?
75        {
76            attributes
77                .try_reserve(1)
78                .map_err(|e| Error::resource(e, CLEAR, element.span().start))?;
79            let value = Attribute::read(&members, &element, "acClearAttrs.attrs")?;
80            attributes
81                .try_push(value)
82                .map_err(|e| Error::resource(e, CLEAR, element.span().start))?;
83        }
84        end(&inside)?;
85        end(parent)?;
86        Ok(Self {
87            issuer,
88            serial,
89            attributes: attributes.into_owned(),
90            element,
91        })
92    }
93    /// Original logical plaintext range, not an offset into encrypted bytes.
94    #[must_use]
95    pub fn element(&self) -> &Element<'a> {
96        &self.element
97    }
98}
99
100/// One independent decryption result from the explicitly selected provider.
101/// Capability/recipient/cryptographic rejection are completed answers, while
102/// original processing/resource/backend errors use `Result::Err`.
103pub enum Attempt<T> {
104    /// The selected provider completed decryption and removed normative padding.
105    Decrypted(T),
106    /// No selected key/recipient applies to this envelope.
107    NotRecipient,
108    /// The selected provider cannot process its algorithms/recipient format.
109    Unsupported,
110    /// The selected provider completed and rejected its ciphertext/key operation.
111    Rejected,
112}
113/// Completed negative plaintext binding. No attribute-use receipt exists.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Rejection {
116    /// Provider completed without a valid decryption result.
117    Decryption,
118    /// Actual issuer name differs; ciphertext cannot be transplanted across AAs.
119    Issuer,
120    /// Full INTEGER differs; ciphertext cannot be transplanted across ACs.
121    Serial,
122}
123/// One original envelope's complete result, preserving unavailable versus false.
124#[derive(Debug)]
125pub enum Outcome<T> {
126    /// Bound plaintext was delivered once; R owns any explicitly retained output.
127    Processed(T),
128    /// No selected recipient applies, without trying another engine implicitly.
129    NotRecipient,
130    /// Unsupported is neither malformed plaintext nor an empty successful set.
131    Unsupported,
132    /// Complete negative answer, without a successful plaintext prefix.
133    Rejected(Rejection),
134}
135/// Logical plaintext context retained independently of temporary decrypted bytes.
136/// `envelope` is in the enclosing AC's source coordinates. Any error produced by
137/// the plaintext reader uses zero-based plaintext coordinates, not that source.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct PlaintextSource {
140    /// Original independent encAttrs value ordinal.
141    pub value: usize,
142    /// Whole original `EnvelopedData` enclosure; no inner offset projection exists.
143    pub envelope: Span,
144}
145/// Terminal refusal, preserving the callback cause and its honest source space.
146#[derive(Debug)]
147pub enum DecryptError<P, V> {
148    /// Original AC/source/current-operation refusal (not plaintext coordinates).
149    Processing(Error),
150    /// Selected provider's unchanged cause at this original envelope.
151    Provider {
152        /// Original encrypted source range.
153        envelope: Span,
154        /// Unchanged provider/source/resource cause.
155        cause: P,
156    },
157    /// Plaintext syntax/resource refusal in the explicitly marked logical space.
158    Plaintext {
159        /// Unchanged located common failure; its offset is plaintext-relative.
160        cause: Error,
161        /// Original encrypted enclosure and coordinate discriminator.
162        source: PlaintextSource,
163    },
164    /// Visitor's unchanged error, never swallowed after successful decryption.
165    Visitor {
166        /// Caller-controlled error; reader-derived offsets use plaintext space.
167        cause: V,
168        /// Same original enclosure and explicit logical-source context.
169        source: PlaintextSource,
170    },
171}
172/// Temporarily bound plaintext. Its type cannot escape the callback, and a
173/// readable-but-unbound `ACClearAttrs` cannot be passed in its place.
174pub struct Bound<'s> {
175    clear: ClearAttrs<'s>,
176    attributes: OwnedVec<AttributeUse>,
177    source: PlaintextSource,
178}
179impl Bound<'_> {
180    /// Every original decrypted attribute, including duplicate types.
181    #[must_use]
182    pub fn clear(&self) -> &ClearAttrs<'_> {
183        &self.clear
184    }
185    /// One original AA-control answer per decrypted attribute, in source order.
186    #[must_use]
187    pub fn attributes(&self) -> &[AttributeUse] {
188        &self.attributes
189    }
190    /// Explicit encrypted enclosure/zero-based plaintext coordinate context.
191    #[must_use]
192    pub fn source(&self) -> PlaintextSource {
193        self.source
194    }
195}
196/// Complete catalogue of independent original targeted envelopes. Reading all
197/// values precedes the first provider call, so malformed later metadata cannot
198/// leave a successful decrypted prefix. It authenticates no AC on its own.
199pub struct Encrypted<'r, 'a> {
200    certificate: &'r AttributeCertificate<'a>,
201    envelopes: OwnedVec<EnvelopedData<'a>>,
202}
203impl<'r, 'a> Encrypted<'r, 'a> {
204    /// Read the original unique encAttrs attribute's entire value set. Returns
205    /// None only when this actual AC has no such attribute. Same producing
206    /// Parser is required before lookup/work, not merely the same account.
207    pub fn read(
208        certificate: &'r AttributeCertificate<'a>,
209        parent: &Reader<'a>,
210    ) -> Result<Option<Self>> {
211        if !core::ptr::eq(certificate.parser, parent.parser()) {
212            return Err(Error::pkix(structured_pkix::Error::new(
213                structured_pkix::ErrorCode::ForeignReader,
214                FIELD,
215                certificate.element().span().start,
216            )));
217        }
218        let work = parent.execution();
219        let mut selected = None;
220        for attribute in &certificate.information().attributes {
221            work.charge_work(1)
222                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
223            if attribute.kind().as_bytes().len() != IDENTIFIER.as_bytes().len() {
224                continue;
225            }
226            work.charge_work(IDENTIFIER.as_bytes().len() as u64)
227                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
228            if attribute.kind() == IDENTIFIER {
229                selected = Some(attribute);
230                break;
231            }
232        }
233        let Some(attribute) = selected else {
234            return Ok(None);
235        };
236        let mut envelopes = work.vector();
237        envelopes
238            .try_reserve_exact(attribute.values().len())
239            .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
240        for ordinal in 0..attribute.values().len() {
241            let mut reader = attribute
242                .value_reader_in(ordinal, parent, FIELD)?
243                .ok_or_else(|| {
244                    Error::schema(ErrorCode::InvalidLocation, FIELD, attribute.span().start)
245                })?;
246            let envelope = EnvelopedData::read(&mut reader)?;
247            oid_is(
248                envelope.content.kind,
249                CONTENT_TYPE,
250                &reader,
251                FIELD,
252                envelope.element().span().start,
253            )?;
254            if envelope.content.ciphertext.is_none() {
255                return Err(Error::schema(
256                    ErrorCode::DetachedContent,
257                    FIELD,
258                    envelope.element().span().start,
259                ));
260            }
261            envelopes
262                .try_push(envelope)
263                .map_err(|e| Error::resource(e, FIELD, attribute.span().start))?;
264        }
265        Ok(Some(Self {
266            certificate,
267            envelopes: envelopes.into_owned(),
268        }))
269    }
270    /// All original envelopes. Caller inspection of borrowed metadata/bytes must
271    /// account its own scans; `decrypt_all` admits all of its own traversal.
272    #[must_use]
273    pub fn envelopes(&self) -> &[EnvelopedData<'a>] {
274        &self.envelopes
275    }
276
277    /// Process every independent value through the selected provider and a
278    /// scoped visitor. Decryption receives original fragmented ciphertext and
279    /// must admit its backend work/output through the supplied Execution. The
280    /// returned owned plaintext must belong to that same operation account.
281    ///
282    /// DER parsing inherits the actual ciphertext's original depth and source
283    /// and operation ceilings, without a new `Parser` or copied-buffer reset.
284    /// Plaintext and parsed models are dropped after the visitor. R/V/P cannot
285    /// borrow these temporary values. A late error drops all retained R/results;
286    /// external provider/visitor side effects cannot be rolled back.
287    ///
288    /// The visitor cannot return a borrowed plaintext view. Its higher-ranked
289    /// scope is the same restriction illustrated here:
290    /// ```compile_fail
291    /// use structured_cms::attribute_certificate::encrypted::Bound;
292    /// fn scoped<R>(_: impl for<'s> Fn(&Bound<'s>) -> R) {}
293    /// scoped(|bound| bound.clear().element().as_bytes());
294    /// ```
295    pub fn decrypt_all<'p, 'd, 'c, S, P, V, R>(
296        &self,
297        checked: &Checked<'_, 'p, 'd, 'c, S>,
298        parent: &Reader<'_>,
299        mut provider: impl FnMut(
300            &EnvelopedData<'a>,
301            &Execution<'_>,
302        ) -> core::result::Result<Attempt<OwnedVec<u8>>, P>,
303        mut visitor: impl for<'scope> FnMut(
304            &Bound<'scope>,
305            &Execution<'_>,
306        ) -> core::result::Result<R, V>,
307    ) -> core::result::Result<OwnedVec<Outcome<R>>, DecryptError<P, V>> {
308        let physical = DecryptError::Processing;
309        if !core::ptr::eq(self.certificate.parser, parent.parser())
310            || !core::ptr::eq(self.certificate, checked.certificate())
311        {
312            return Err(physical(Error::schema(
313                ErrorCode::InvalidLocation,
314                FIELD,
315                self.certificate.element().span().start,
316            )));
317        }
318        let work = parent.execution();
319        let mut results = work.vector();
320        results
321            .try_reserve_exact(self.envelopes.len())
322            .map_err(|e| {
323                physical(Error::resource(
324                    e,
325                    FIELD,
326                    self.certificate.element().span().start,
327                ))
328            })?;
329        for (ordinal, envelope) in self.envelopes.iter().enumerate() {
330            work.charge_work(1).map_err(|e| {
331                physical(Error::resource(e, FIELD, envelope.element().span().start))
332            })?;
333            let output = provider(envelope, &work).map_err(|cause| DecryptError::Provider {
334                envelope: envelope.element().span(),
335                cause,
336            })?;
337            let outcome = match output {
338                Attempt::NotRecipient => Outcome::NotRecipient,
339                Attempt::Unsupported => Outcome::Unsupported,
340                Attempt::Rejected => Outcome::Rejected(Rejection::Decryption),
341                Attempt::Decrypted(bytes) => {
342                    if !bytes.memory().same_account(parent.memory()) {
343                        return Err(physical(Error::schema(
344                            ErrorCode::InvalidLocation,
345                            FIELD,
346                            envelope.element().span().start,
347                        )));
348                    }
349                    self.visit(checked, parent, envelope, ordinal, &bytes, &mut visitor)?
350                }
351            };
352            results.try_push(outcome).map_err(|e| {
353                physical(Error::resource(e, FIELD, envelope.element().span().start))
354            })?;
355        }
356        Ok(results.into_owned())
357    }
358
359    fn visit<'p, 'd, 'c, S, P, V, R>(
360        &self,
361        checked: &Checked<'_, 'p, 'd, 'c, S>,
362        parent: &Reader<'_>,
363        envelope: &EnvelopedData<'a>,
364        ordinal: usize,
365        bytes: &[u8],
366        visitor: &mut impl for<'scope> FnMut(
367            &Bound<'scope>,
368            &Execution<'_>,
369        ) -> core::result::Result<R, V>,
370    ) -> core::result::Result<Outcome<R>, DecryptError<P, V>> {
371        let source = PlaintextSource {
372            value: ordinal,
373            envelope: envelope.element().span(),
374        };
375        let logical = |cause| DecryptError::Plaintext { cause, source };
376        // The ciphertext was required in the complete preflight, and no mutable
377        // envelope access is exposed by Encrypted. It contributes its actual
378        // nested source level, just as other carried ASN.1 payloads do.
379        let ciphertext = envelope
380            .content
381            .ciphertext
382            .as_ref()
383            .expect("encAttrs requires attached ciphertext");
384        let mut reader = self
385            .certificate
386            .parser
387            .read_nested_query_in(parent, bytes, 0, Encoding::Der, ciphertext.outer_depth())
388            .map_err(|e| logical(Error::encoding(e, CLEAR)))?;
389        let clear = ClearAttrs::read(&mut reader).map_err(logical)?;
390        let work = reader.execution();
391        let ac = self.certificate;
392        work.charge_work(3)
393            .map_err(|e| logical(Error::resource(e, CLEAR, clear.element.span().start)))?;
394        let left = clear.serial.as_bytes();
395        let right = ac.information().serial.as_bytes();
396        // Both are checked DER INTEGERs. Exact minimal two's-complement octets
397        // therefore implement full numeric equality, including wide/negative
398        // values, without a scalar projection or sentinel.
399        if left.len() != right.len() {
400            return Ok(Outcome::Rejected(Rejection::Serial));
401        }
402        work.charge_work(left.len() as u64 + right.len() as u64)
403            .map_err(|e| {
404                logical(Error::resource(
405                    e,
406                    "acClearAttrs.acSerial",
407                    clear.element.span().start,
408                ))
409            })?;
410        if left != right {
411            return Ok(Outcome::Rejected(Rejection::Serial));
412        }
413        // A successful original RFC5755 profile has exactly one issuer DN.
414        // Prepare operands separately so original-name failures keep physical
415        // AC coordinates and plaintext failures stay explicitly logical.
416        let Some([GeneralName::Directory(actual)]) = ac.information().issuer.names.as_deref()
417        else {
418            unreachable!("authenticated profile restricts issuer to one directoryName")
419        };
420        let GeneralName::Directory(claimed) = &clear.issuer else {
421            return Ok(Outcome::Rejected(Rejection::Issuer));
422        };
423        let actual = actual
424            .ordered(&work)
425            .map_err(|e| DecryptError::Processing(Error::pkix(e)))?;
426        let claimed = claimed
427            .ordered_in(&reader)
428            .map_err(|e| logical(Error::pkix(e)))?;
429        if claimed
430            .order(&actual, &work)
431            .map_err(|e| logical(Error::pkix(e)))?
432            != core::cmp::Ordering::Equal
433        {
434            return Ok(Outcome::Rejected(Rejection::Issuer));
435        }
436        let mut attributes = work.vector();
437        attributes
438            .try_reserve_exact(clear.attributes.len())
439            .map_err(|e| logical(Error::resource(e, CLEAR, clear.element.span().start)))?;
440        for (index, attribute) in clear.attributes.iter().enumerate() {
441            work.charge_work(1)
442                .map_err(|e| logical(Error::resource(e, CLEAR, attribute.span().start)))?;
443            // AA-control failures originate in the original authenticated AA
444            // source, not in a guessed plaintext-to-ciphertext mapping.
445            let permission = checked
446                .allows(attribute.kind(), &work)
447                .map_err(DecryptError::Processing)?;
448            attributes
449                .try_push(AttributeUse { index, permission })
450                .map_err(|e| logical(Error::resource(e, CLEAR, attribute.span().start)))?;
451        }
452        let bound = Bound {
453            clear,
454            attributes: attributes.into_owned(),
455            source,
456        };
457        let output =
458            visitor(&bound, &work).map_err(|cause| DecryptError::Visitor { cause, source })?;
459        Ok(Outcome::Processed(output))
460    }
461}
462fn end(parent: &Reader<'_>) -> Result<()> {
463    if parent.is_empty() {
464        Ok(())
465    } else {
466        Err(Error::schema(
467            ErrorCode::TrailingData,
468            CLEAR,
469            parent.offset(),
470        ))
471    }
472}