Skip to main content

oxideav_pdf/pubsec/
cms.rs

1//! Minimal CMS (Cryptographic Message Syntax, RFC 5652) parser for
2//! the `EnvelopedData` content type used by the PDF public-key
3//! security handler (ISO 32000-1 §7.6.4 — see `docs/document/pdf/PDF32000_2008.pdf`).
4//!
5//! Only the subset PDF readers actually need is implemented:
6//!
7//! * `ContentInfo` whose `contentType` is the OID
8//!   `1.2.840.113549.1.7.3` (`id-envelopedData`).
9//! * `EnvelopedData` versions 0, 2, and 3.
10//! * `RecipientInfo` of variant `KeyTransRecipientInfo` (RFC 5652
11//!   §6.2.1) and `KeyAgreeRecipientInfo` (§6.2.2 — round 12 decoder
12//!   side: ECDH / DH / static-static recipients; the originator
13//!   public key + UKM are surfaced; the wrapped CEK lands as a
14//!   `RecipientEncryptedKey` slot inside the KARI).
15//! * `RecipientIdentifier` of variant `IssuerAndSerialNumber` (CMS
16//!   v0) or `[0] SubjectKeyIdentifier` (CMS v2). Round 11 wires the
17//!   SKI variant through to the matcher (the pubsec module computes
18//!   the SHA-1 of the user cert's `SubjectPublicKeyInfo` BIT STRING
19//!   contents per RFC 5280 §4.2.1.2 and compares it to the recipient
20//!   slot's SKI octet string).
21//! * `EncryptedContentInfo` whose `contentType` is `id-data` and
22//!   whose `contentEncryptionAlgorithm` is one of the algorithm
23//!   identifiers the PDF spec allows (RC4 / AES-128-CBC /
24//!   AES-256-CBC).
25//!
26//! Provenance: RFC 5652 §6 only. Encoding test fixtures consume
27//! the symmetric writer side which lives in `cms_build.rs`.
28
29use crate::error::PdfError;
30
31use super::der::{
32    maybe_read_context, read_context, read_expected, read_integer_bytes, read_integer_u64,
33    read_octet_string, read_oid, read_sequence, read_set, Class,
34};
35
36/// OID 1.2.840.113549.1.7.3 — id-envelopedData.
37pub const OID_ENVELOPED_DATA: [u64; 7] = [1, 2, 840, 113549, 1, 7, 3];
38
39/// OID 1.2.840.113549.1.7.2 — id-signedData (RFC 5652 §5.1). Round-19
40/// adds parser-side recognition; verification is deferred (the round
41/// surfaces `SignedData` structurally so callers can route on the
42/// signed contents + per-signer attributes without a built-in verify
43/// dispatch).
44pub const OID_SIGNED_DATA: [u64; 7] = [1, 2, 840, 113549, 1, 7, 2];
45
46/// OID 1.2.840.113549.1.7.1 — id-data (the contentType inside
47/// `EncryptedContentInfo`).
48pub const OID_DATA: [u64; 7] = [1, 2, 840, 113549, 1, 7, 1];
49
50/// OID 1.2.840.113549.1.1.1 — rsaEncryption (RSAES-PKCS1-v1_5 in CMS).
51pub const OID_RSA_ENCRYPTION: [u64; 7] = [1, 2, 840, 113549, 1, 1, 1];
52
53/// OID 1.2.840.113549.3.4 — RC4. (PDF s3/s4 with `CFM=V2`).
54pub const OID_RC4: [u64; 6] = [1, 2, 840, 113549, 3, 4];
55
56/// OID 2.16.840.1.101.3.4.1.2 — id-aes128-CBC.
57pub const OID_AES128_CBC: [u64; 9] = [2, 16, 840, 1, 101, 3, 4, 1, 2];
58
59/// OID 2.16.840.1.101.3.4.1.42 — id-aes256-CBC.
60pub const OID_AES256_CBC: [u64; 9] = [2, 16, 840, 1, 101, 3, 4, 1, 42];
61
62/// OID 1.2.840.113549.3.2 — `rc2-cbc` (RFC 2268 + RFC 3217 §3). Used
63/// by legacy CMS envelopes whose `EncryptedContentInfo.contentEncryptionAlgorithm`
64/// names RC2-CBC. Round-17 adds read-only support so legacy
65/// PDF archives still open; PDF 2.0 deprecates RC2 entirely.
66///
67/// Parameters per RFC 3370 §5.1: SEQUENCE { rc2ParameterVersion INTEGER
68/// OPTIONAL DEFAULT 32, iv OCTET STRING (size 8) }. The
69/// `rc2ParameterVersion` ↔ effective-key-bits mapping (RFC 2268 §6) is:
70/// version 160 → 40 bits, 120 → 64 bits, 58 → 128 bits. We only
71/// support the 128-bit effective-key variant on decode (the 40 / 64-bit
72/// variants are export-grade legacy and would also be acceptable in
73/// principle, but the surface we surface today follows the most-common
74/// modern usage).
75pub const OID_RC2_CBC: [u64; 6] = [1, 2, 840, 113549, 3, 2];
76
77/// OID 1.2.840.113549.3.7 — `des-EDE3-CBC` (RFC 3370 §5.2 + RFC 5652
78/// §12.4). Used by legacy CMS envelopes whose
79/// `EncryptedContentInfo.contentEncryptionAlgorithm` names triple-DES
80/// in CBC mode. Round-17 adds read-only support; PDF 2.0 deprecates 3DES.
81///
82/// Parameters: OCTET STRING (size 8) — the 8-byte CBC IV.
83pub const OID_DES_EDE3_CBC: [u64; 6] = [1, 2, 840, 113549, 3, 7];
84
85/// Symmetric content-encryption algorithm extracted from the
86/// `EnvelopedData::encryptedContentInfo::contentEncryptionAlgorithm`
87/// field. Only the algorithms actually referenced by ISO 32000-1
88/// §7.6.4.3 + the read-only legacy round-17 set are listed.
89#[derive(Debug, Clone)]
90pub enum ContentEncryption {
91    /// RC4 stream cipher; the algorithm identifier carries no
92    /// parameters beyond the OID itself.
93    Rc4,
94    /// AES-128-CBC. The IV is the 16-byte OCTET STRING parameters
95    /// payload.
96    Aes128Cbc { iv: [u8; 16] },
97    /// AES-256-CBC. Same parameter shape as AES-128.
98    Aes256Cbc { iv: [u8; 16] },
99    /// **Round-17 read-only** — RC2-CBC (RFC 2268 + RFC 3217 §3). The
100    /// 8-byte CBC IV is carried in the parameters SEQUENCE alongside
101    /// the optional `rc2ParameterVersion` (which we surface as the
102    /// effective-key bit length per RFC 2268 §6 — 40 / 64 / 128).
103    /// PDF 2.0 deprecates RC2; we accept it on decode only.
104    Rc2Cbc {
105        /// RC2 effective-key bit length (RFC 2268 §6) — `40`, `64`, or
106        /// `128`. Defaults to `32` per RFC 3370 §5.1's
107        /// `rc2ParameterVersion` DEFAULT (which translates to 32 effective
108        /// bits — but RFC 3370 also explicitly lists 32 → 32 and writers
109        /// commonly omit the field entirely, in which case we fall back
110        /// to 32).
111        effective_key_bits: u32,
112        /// 8-byte CBC IV.
113        iv: [u8; 8],
114    },
115    /// **Round-17 read-only** — DES-EDE3-CBC (3DES, RFC 3370 §5.2). The
116    /// 8-byte CBC IV is the parameters payload. PDF 2.0 deprecates 3DES;
117    /// we accept it on decode only.
118    DesEde3Cbc {
119        /// 8-byte CBC IV.
120        iv: [u8; 8],
121    },
122}
123
124/// Identifier-and-serial-number pair (RFC 5280 §A.1) that points at
125/// one of the recipient's certificates. Bytes are the raw DER of the
126/// `Name` for `issuer` (so the matcher can byte-compare against the
127/// recipient's own certificate's `issuer` directly), and the raw
128/// big-endian two's-complement INTEGER body for `serial_number`.
129#[derive(Debug, Clone)]
130pub struct IssuerAndSerial {
131    /// DER-encoded `issuer` Name (a SEQUENCE OF RelativeDistinguishedName).
132    pub issuer_der: Vec<u8>,
133    /// Raw INTEGER body bytes of `serialNumber`. RFC 5280 §4.1.2.2
134    /// allows up to 20 octets; we keep the full big-endian body so a
135    /// byte-for-byte match against the user's cert serial is exact.
136    pub serial: Vec<u8>,
137}
138
139/// CMS `RecipientIdentifier` (RFC 5652 §6.2.1) — the CHOICE that picks
140/// between `IssuerAndSerialNumber` (CMS v0) and `SubjectKeyIdentifier`
141/// (CMS v2). The SKI form carries the bare 20-byte SHA-1 of the
142/// recipient cert's `SubjectPublicKeyInfo` BIT STRING contents — see
143/// RFC 5280 §4.2.1.2 method 1.
144#[derive(Debug, Clone)]
145pub enum RecipientId {
146    /// `IssuerAndSerialNumber` (CMS v0). The matcher compares this to
147    /// the user cert's `(issuer_der, serial)` pair byte-for-byte.
148    IssuerAndSerial(IssuerAndSerial),
149    /// `[0] SubjectKeyIdentifier` (CMS v2). The matcher compares the
150    /// raw octet-string body to the SHA-1 of the user cert's
151    /// `SubjectPublicKeyInfo` BIT STRING contents.
152    SubjectKeyIdentifier(Vec<u8>),
153}
154
155/// `KeyTransRecipientInfo` (RFC 5652 §6.2.1) — the only RecipientInfo
156/// flavour the public-key handler ever uses in practice. RSAES-PKCS1
157/// v1.5 is the one key-encryption algorithm we accept (per the
158/// pre-AES algorithm list in ISO 32000-1 §7.6.4.3 + the RFC 5652
159/// recommendation).
160#[derive(Debug, Clone)]
161pub struct KeyTransRecipientInfo {
162    /// Recipient identifier — IssuerAndSerial (v0) or SKI (v2).
163    pub rid: RecipientId,
164    /// Algorithm identifier of the key-encryption algorithm. We only
165    /// accept `OID_RSA_ENCRYPTION` here.
166    pub key_encryption_oid: Vec<u64>,
167    /// The encrypted content-encryption key, RSA-PKCS1-v1.5 wrapped
168    /// to the recipient's public RSA key.
169    pub encrypted_key: Vec<u8>,
170}
171
172/// `OriginatorPublicKey` (RFC 5652 §6.2.2) — the originator's
173/// ephemeral or static public key carried as a BIT STRING with an
174/// `AlgorithmIdentifier` describing the curve / group. Used by the
175/// recipient (along with their own private key) to derive the shared
176/// secret that wraps the content-encryption key in a KARI envelope.
177#[derive(Debug, Clone)]
178pub struct OriginatorPublicKey {
179    /// AlgorithmIdentifier OID (e.g. ecPublicKey, dhpublicnumber).
180    pub algorithm_oid: Vec<u64>,
181    /// Raw AlgorithmIdentifier `parameters` field bytes (e.g. an OID
182    /// for the named curve). Empty when the encoded form was a NULL
183    /// or absent.
184    pub algorithm_params: Vec<u8>,
185    /// `subjectPublicKey` BIT STRING contents (no leading unused-bits
186    /// byte). For ECDH this is the encoded EC point.
187    pub public_key: Vec<u8>,
188}
189
190/// `OriginatorIdentifierOrKey` (RFC 5652 §6.2.2) — the CHOICE that
191/// identifies the originator side of a key-agreement recipient. We
192/// surface every arm so callers can route on the originator type
193/// (an importing client may, for example, prefer one originator to
194/// another when multiple KARIs are present).
195#[derive(Debug, Clone)]
196pub enum OriginatorId {
197    /// Originator is identified by their `IssuerAndSerialNumber`.
198    IssuerAndSerial(IssuerAndSerial),
199    /// Originator is identified by their SubjectKeyIdentifier (the
200    /// 20-byte SHA-1 of their cert's SPKI BIT STRING contents).
201    SubjectKeyIdentifier(Vec<u8>),
202    /// Originator's public key is carried in-band (no certificate
203    /// reference required).
204    OriginatorKey(OriginatorPublicKey),
205}
206
207/// `OtherKeyAttribute` (RFC 5652 §10.2.7) — opaque application-defined
208/// recipient-key attribute carried inside a `RecipientKeyIdentifier`.
209///
210/// ```asn.1
211/// OtherKeyAttribute ::= SEQUENCE {
212///   keyAttrId    OBJECT IDENTIFIER,
213///   keyAttr      ANY DEFINED BY keyAttrId OPTIONAL
214/// }
215/// ```
216///
217/// We surface the raw bytes (both the OID arc list and the unparsed
218/// attribute body) so callers can interpret application-specific
219/// attributes per their own conventions.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct OtherKeyAttribute {
222    /// `keyAttrId` OID arcs.
223    pub key_attr_id: Vec<u64>,
224    /// Raw bytes of the optional `keyAttr` ANY value (DER-encoded — the
225    /// caller is expected to parse it per the OID's contract). Empty
226    /// when absent.
227    pub key_attr: Vec<u8>,
228}
229
230/// `KeyAgreeRecipientIdentifier` (RFC 5652 §6.2.2) — identifies one
231/// recipient inside a KARI's `recipientEncryptedKeys` SEQUENCE. Either
232/// the legacy `IssuerAndSerial` form or a `RecipientKeyIdentifier` —
233/// which carries an SKI plus optional `date` + `other` attributes.
234///
235/// Round 18: the OPTIONAL `date` (`GeneralizedTime`) and `other`
236/// (`OtherKeyAttribute`) fields of `RecipientKeyIdentifier` are now
237/// captured rather than silently discarded. The `date` field lets the
238/// originator pin "this envelope is valid for the recipient cert that
239/// was active at this instant" — useful for long-lived archives where
240/// multiple cert generations exist for the same SKI. See
241/// [`super::TrustStore::find_with_temporal_validity`].
242#[derive(Debug, Clone)]
243pub enum KeyAgreeRecipientId {
244    /// Legacy `IssuerAndSerialNumber` — same shape as KTRI v0.
245    IssuerAndSerial(IssuerAndSerial),
246    /// `[0] IMPLICIT RecipientKeyIdentifier` (RFC 5652 §6.2.2). The
247    /// `ski` is the recipient cert's SubjectKeyIdentifier; the OPTIONAL
248    /// `date` (RFC 5280 §4.1.2.5.2 `GeneralizedTime` — `YYYYMMDDHHMMSSZ`)
249    /// and `other` (`OtherKeyAttribute`) fields are surfaced as raw
250    /// bytes / structured arms when present.
251    RecipientKeyIdentifier {
252        /// 20-byte SHA-1 of the recipient cert's SPKI BIT STRING
253        /// contents (RFC 5280 §4.2.1.2 method 1).
254        ski: Vec<u8>,
255        /// OPTIONAL `date` field — raw `GeneralizedTime` ASCII bytes
256        /// per RFC 5280 §4.1.2.5.2 (e.g. `b"20260510120000Z"`). `None`
257        /// when the field was absent. Round 18.
258        date: Option<Vec<u8>>,
259        /// OPTIONAL `other` field — the parsed `OtherKeyAttribute`
260        /// SEQUENCE. `None` when absent. Round 18.
261        other: Option<OtherKeyAttribute>,
262    },
263}
264
265/// `RecipientEncryptedKey` (RFC 5652 §6.2.2) — one wrapped CEK inside
266/// a KARI envelope. The KARI itself holds a SEQUENCE OF these.
267#[derive(Debug, Clone)]
268pub struct RecipientEncryptedKey {
269    /// Recipient identifier (CHOICE issuerAndSerial / RKID-via-SKI).
270    pub rid: KeyAgreeRecipientId,
271    /// Wrapped content-encryption key. The unwrap algorithm is named
272    /// by the parent KARI's `keyEncryptionAlgorithm` (typically a
273    /// key-wrap algorithm such as `id-aes128-wrap` paired with a
274    /// key-derivation function like `dhSinglePass-stdDH-sha256kdf-scheme`).
275    pub encrypted_key: Vec<u8>,
276}
277
278/// `KeyAgreeRecipientInfo` (RFC 5652 §6.2.2) — the second
279/// `RecipientInfo` CHOICE arm. Used with ECDH / DH-based recipients
280/// (vs RSA-based KTRI). `version` is always 3.
281#[derive(Debug, Clone)]
282pub struct KeyAgreeRecipientInfo {
283    /// Originator side identifier / key.
284    pub originator: OriginatorId,
285    /// Optional UserKeyingMaterial — extra randomness mixed into the
286    /// KDF on both sides. Empty `Vec` means "absent".
287    pub ukm: Vec<u8>,
288    /// `keyEncryptionAlgorithm` OID — names the KDF + key-wrap
289    /// combination (e.g. `dhSinglePass-stdDH-sha256kdf-scheme`).
290    pub key_encryption_oid: Vec<u64>,
291    /// Raw `keyEncryptionAlgorithm` parameters bytes. For
292    /// `dhSinglePass-stdDH-sha*-kdf` this is itself a SEQUENCE
293    /// containing the key-wrap algorithm's OID + parameters.
294    pub key_encryption_params: Vec<u8>,
295    /// One or more recipient slots; each slot's `encryptedKey` is the
296    /// CEK wrapped using the shared secret derived from the originator
297    /// + recipient pair (and the `keyEncryptionAlgorithm`'s KDF).
298    pub recipient_encrypted_keys: Vec<RecipientEncryptedKey>,
299}
300
301/// `RecipientInfo` CHOICE — round 12 surfaces both KTRI (RSA) and
302/// KARI (DH/ECDH) variants. KEKRI (`[2] kekri`), PWRI (`[3] pwri`),
303/// and ORI (`[4] ori`) are still skipped — the PDF spec does not
304/// reference them and adding them would expand the threat surface
305/// without serving a use case.
306#[derive(Debug, Clone)]
307pub enum RecipientInfoVariant {
308    /// `KeyTransRecipientInfo` — RSA-based, the round-10/11 path.
309    KeyTrans(KeyTransRecipientInfo),
310    /// `KeyAgreeRecipientInfo` — DH/ECDH-based, round 12 decoder.
311    KeyAgree(KeyAgreeRecipientInfo),
312}
313
314/// `OriginatorInfo` (RFC 5652 §10.2.1) — the optional `[0] IMPLICIT`
315/// originator-side certificate + revocation chain that an envelope MAY
316/// carry alongside the recipient-info SET.
317///
318/// ```asn.1
319/// OriginatorInfo ::= SEQUENCE {
320///   certs [0] IMPLICIT CertificateSet OPTIONAL,
321///   crls  [1] IMPLICIT RevocationInfoChoices OPTIONAL
322/// }
323/// ```
324///
325/// Round 18 surfaces this structurally-parsed-but-previously-discarded
326/// chain so callers (e.g. validation pipelines) can inspect the
327/// originator's transmitted certificate / CRL bundle. Each `certs[]`
328/// element is the raw DER bytes of one CertificateChoices alternative —
329/// typically an X.509 v3 `Certificate` SEQUENCE; we surface every
330/// alternative shape as opaque DER so the caller can dispatch on the
331/// outer tag (`SEQUENCE` for the X.509 v3 / v1 form vs context-specific
332/// `[0]..[3]` for the extended-cert / attribute-cert / other-cert
333/// alternatives of RFC 5652 §10.2.2). Each `crls[]` element is the raw
334/// DER of one RevocationInfoChoices alternative — typically an X.509
335/// `CertificateList` SEQUENCE.
336///
337/// The bytes captured for each entry include the outer tag and length —
338/// they are byte-identical to what would be written back by a
339/// re-encoder, so a caller can re-parse the inner X.509 / CRL via the
340/// crate's [`super::x509::Certificate::parse`] helper without
341/// reconstruction.
342#[derive(Debug, Clone, Default)]
343pub struct OriginatorInfo {
344    /// Raw DER bytes of each entry in the OPTIONAL `certs[0]` set.
345    /// Empty when the field was absent.
346    pub certs: Vec<Vec<u8>>,
347    /// Raw DER bytes of each entry in the OPTIONAL `crls[1]` set.
348    /// Empty when the field was absent.
349    pub crls: Vec<Vec<u8>>,
350}
351
352impl OriginatorInfo {
353    /// `true` when both `certs[]` and `crls[]` are empty (i.e. the
354    /// envelope omitted `OriginatorInfo` or carried it as an empty
355    /// SEQUENCE).
356    pub fn is_empty(&self) -> bool {
357        self.certs.is_empty() && self.crls.is_empty()
358    }
359}
360
361/// Parsed CMS `EnvelopedData` reduced to the fields the PDF public-key
362/// handler consumes. Recipients of unsupported variants (`kekri`,
363/// `pwri`, `ori`) are skipped over silently so a pre-existing PDF
364/// written with mixed recipient types can still be opened by a
365/// supported variant's user.
366#[derive(Debug, Clone)]
367pub struct EnvelopedData {
368    /// Backwards-compatible KTRI-only view of the recipients SET. Code
369    /// written against round 10/11 keeps working — only KTRI slots are
370    /// surfaced through this list. Round 12 introduces [`Self::all`]
371    /// for callers that want the KARI slots too.
372    pub recipients: Vec<KeyTransRecipientInfo>,
373    /// Round-12 view: every recognised RecipientInfo (KTRI + KARI) in
374    /// declaration order. KEKRI / PWRI / ORI are still skipped.
375    pub all_recipients: Vec<RecipientInfoVariant>,
376    /// Symmetric algorithm used to protect the envelope's content.
377    pub content_encryption: ContentEncryption,
378    /// The encrypted enveloped data (the bytes that decrypt to the
379    /// 20-byte seed + 4-byte permissions blob).
380    pub encrypted_content: Vec<u8>,
381    /// Round-18: the OPTIONAL originator-side cert / CRL chain
382    /// (RFC 5652 §10.2.1 `OriginatorInfo`). Empty when the envelope
383    /// omitted the field.
384    pub originator_info: OriginatorInfo,
385}
386
387impl EnvelopedData {
388    /// Round-18: surface the originator-side cert + CRL chain when the
389    /// envelope carried `[0] IMPLICIT OriginatorInfo`. Returns `None`
390    /// when the field was absent or both `certs[]`/`crls[]` were empty.
391    pub fn originator_info(&self) -> Option<&OriginatorInfo> {
392        if self.originator_info.is_empty() {
393            None
394        } else {
395            Some(&self.originator_info)
396        }
397    }
398}
399
400/// Parse the `ContentInfo` envelope wrapping an `EnvelopedData` blob,
401/// returning the inner parsed structure.
402pub fn parse_envelope(data: &[u8]) -> Result<EnvelopedData, PdfError> {
403    // ContentInfo ::= SEQUENCE { contentType OID, content [0] EXPLICIT ANY }
404    let (body, rest) = read_sequence(data)?;
405    if !rest.is_empty() {
406        return Err(PdfError::other(
407            "CMS: trailing bytes after ContentInfo SEQUENCE",
408        ));
409    }
410    let (oid, rest) = read_oid(body)?;
411    if oid != OID_ENVELOPED_DATA {
412        return Err(PdfError::other(format!(
413            "CMS: ContentInfo contentType must be id-envelopedData (got {oid:?})"
414        )));
415    }
416    let (content, rest) = read_context(rest, 0)?;
417    if !rest.is_empty() {
418        return Err(PdfError::other(
419            "CMS: trailing bytes after [0] EXPLICIT content",
420        ));
421    }
422    parse_enveloped_data(content)
423}
424
425/// Parse a bare `EnvelopedData` SEQUENCE (no surrounding ContentInfo).
426pub fn parse_enveloped_data(data: &[u8]) -> Result<EnvelopedData, PdfError> {
427    // EnvelopedData ::= SEQUENCE {
428    //   version              CMSVersion,
429    //   originatorInfo       [0] IMPLICIT OriginatorInfo OPTIONAL,
430    //   recipientInfos       SET OF RecipientInfo,
431    //   encryptedContentInfo EncryptedContentInfo,
432    //   unprotectedAttrs     [1] IMPLICIT UnprotectedAttributes OPTIONAL
433    // }
434    let (body, rest) = read_sequence(data)?;
435    if !rest.is_empty() {
436        return Err(PdfError::other(
437            "CMS: trailing bytes after EnvelopedData SEQUENCE",
438        ));
439    }
440    let (version, body) = read_integer_u64(body)?;
441    if version > 4 {
442        return Err(PdfError::other(format!(
443            "CMS: unsupported EnvelopedData version {version}"
444        )));
445    }
446    // `[0] IMPLICIT OriginatorInfo OPTIONAL` — round 18 captures it.
447    // Body shape per RFC 5652 §10.2.1 / §10.2.2:
448    //   OriginatorInfo ::= SEQUENCE {
449    //     certs [0] IMPLICIT CertificateSet OPTIONAL,
450    //     crls  [1] IMPLICIT RevocationInfoChoices OPTIONAL
451    //   }
452    // The `[0] IMPLICIT OriginatorInfo` outer wrapper means the body
453    // bytes are themselves the SEQUENCE contents (the IMPLICIT tag
454    // replaces the SEQUENCE's universal tag).
455    let (orig_opt, body) = maybe_read_context(body, 0)?;
456    let originator_info = match orig_opt {
457        Some(b) => parse_originator_info(b)?,
458        None => OriginatorInfo::default(),
459    };
460
461    // RecipientInfos
462    let (ri_set, body) = read_set(body)?;
463    let mut recipients = Vec::new();
464    let mut all_recipients = Vec::new();
465    let mut cursor = ri_set;
466    while !cursor.is_empty() {
467        let (parsed, tail) = parse_recipient_info(cursor)?;
468        if let Some(p) = parsed {
469            if let RecipientInfoVariant::KeyTrans(ktri) = &p {
470                recipients.push(ktri.clone());
471            }
472            all_recipients.push(p);
473        }
474        cursor = tail;
475    }
476    if all_recipients.is_empty() {
477        return Err(PdfError::other(
478            "CMS: EnvelopedData has no recognised RecipientInfo entries",
479        ));
480    }
481
482    // EncryptedContentInfo ::= SEQUENCE {
483    //   contentType            OBJECT IDENTIFIER,
484    //   contentEncryptionAlgorithm AlgorithmIdentifier,
485    //   encryptedContent       [0] IMPLICIT OCTET STRING OPTIONAL
486    // }
487    let (eci, body) = read_sequence(body)?;
488    let (_ct_oid, eci_rest) = read_oid(eci)?;
489    let (alg_seq, eci_rest) = read_sequence(eci_rest)?;
490    let (alg_oid, alg_params) = read_oid(alg_seq)?;
491    let content_encryption = decode_content_alg(&alg_oid, alg_params)?;
492    // `[0] IMPLICIT OCTET STRING` — context-specific, primitive form.
493    let (enc_body, eci_rest) = read_expected(eci_rest, Class::ContextSpecific, 0)?;
494    if enc_body.constructed {
495        return Err(PdfError::other(
496            "CMS: encryptedContent constructed-form not supported",
497        ));
498    }
499    if !eci_rest.is_empty() {
500        return Err(PdfError::other(
501            "CMS: trailing bytes after EncryptedContentInfo",
502        ));
503    }
504    let encrypted_content = enc_body.body.to_vec();
505
506    // The trailing unprotectedAttrs is OPTIONAL — discard if present.
507    let _ = maybe_read_context(body, 1)?;
508
509    Ok(EnvelopedData {
510        recipients,
511        all_recipients,
512        content_encryption,
513        encrypted_content,
514        originator_info,
515    })
516}
517
518/// Round-18: parse an `OriginatorInfo` body (the bytes inside the
519/// `[0] IMPLICIT` wrapper of `EnvelopedData`). Both the `certs[0]` and
520/// `crls[1]` fields are OPTIONAL `IMPLICIT` SETs of `CertificateChoices` /
521/// `RevocationInfoChoices`. We capture every entry's raw DER bytes
522/// (including the outer tag/length) so callers can re-parse them
523/// without reconstructing the encoding.
524fn parse_originator_info(body: &[u8]) -> Result<OriginatorInfo, PdfError> {
525    let mut cursor = body;
526    let mut info = OriginatorInfo::default();
527    // `[0] IMPLICIT CertificateSet` — an IMPLICIT SET, so its tag is
528    // `[0]` constructed (the SET's universal tag is replaced).
529    if !cursor.is_empty() {
530        let (peek, _) = super::der::read_tlv(cursor)?;
531        if peek.class == Class::ContextSpecific && peek.tag_number == 0 {
532            let (set_body, after) = super::der::read_tlv(cursor)?;
533            info.certs = split_set_into_raw_entries(set_body.body)?;
534            cursor = after;
535        }
536    }
537    // `[1] IMPLICIT RevocationInfoChoices` — same wrapping.
538    if !cursor.is_empty() {
539        let (peek, _) = super::der::read_tlv(cursor)?;
540        if peek.class == Class::ContextSpecific && peek.tag_number == 1 {
541            let (set_body, after) = super::der::read_tlv(cursor)?;
542            info.crls = split_set_into_raw_entries(set_body.body)?;
543            cursor = after;
544        }
545    }
546    if !cursor.is_empty() {
547        return Err(PdfError::other(
548            "CMS: trailing bytes after OriginatorInfo SEQUENCE body",
549        ));
550    }
551    Ok(info)
552}
553
554/// Split a SET body (or any concatenation of TLVs) into a Vec where
555/// each entry is the raw DER bytes (tag + length + body) of one TLV.
556/// Used by [`parse_originator_info`] to surface `certs[]` / `crls[]`
557/// alternatives without dispatching on the inner CHOICE.
558fn split_set_into_raw_entries(set_body: &[u8]) -> Result<Vec<Vec<u8>>, PdfError> {
559    let mut out = Vec::new();
560    let mut cursor = set_body;
561    while !cursor.is_empty() {
562        let before_len = cursor.len();
563        let (_tlv, after) = super::der::read_tlv(cursor)?;
564        let consumed = before_len - after.len();
565        out.push(cursor[..consumed].to_vec());
566        cursor = after;
567    }
568    Ok(out)
569}
570
571/// Parse a single `RecipientInfo` element from the SET body. Returns
572/// `Ok(None)` for variants this implementation doesn't recognise
573/// (`[2] kekri`, `[3] pwri`, `[4] ori`); KTRI (untagged SEQUENCE) and
574/// KARI (`[1]` IMPLICIT) are surfaced as separate enum arms.
575fn parse_recipient_info(data: &[u8]) -> Result<(Option<RecipientInfoVariant>, &[u8]), PdfError> {
576    // Peek at the tag to decide which CHOICE branch we're in.
577    let (peek, peek_tail) = super::der::read_tlv(data)?;
578    if peek.class == Class::ContextSpecific {
579        match peek.tag_number {
580            1 => {
581                // [1] IMPLICIT KeyAgreeRecipientInfo — body is the
582                // KARI SEQUENCE contents (the implicit tag replaces
583                // the SEQUENCE's universal tag).
584                let kari = parse_kari(peek.body)?;
585                return Ok((Some(RecipientInfoVariant::KeyAgree(kari)), peek_tail));
586            }
587            // [2] kekri, [3] pwri, [4] ori — skipped silently.
588            _ => return Ok((None, peek_tail)),
589        }
590    }
591    // Otherwise it's a KeyTransRecipientInfo SEQUENCE.
592    let (ktri_body, tail) = read_sequence(data)?;
593    let (version, after_ver) = read_integer_u64(ktri_body)?;
594    // version 0 (uses IssuerAndSerialNumber) and 2 (uses
595    // SubjectKeyIdentifier) per RFC 5652 §6.2.1. Only v0 has the
596    // matching info our public-key handler can use.
597    if version > 2 {
598        return Err(PdfError::other(format!(
599            "CMS: unsupported KeyTransRecipientInfo version {version}"
600        )));
601    }
602    // RecipientIdentifier ::= CHOICE {
603    //   issuerAndSerialNumber  IssuerAndSerialNumber,  -- SEQUENCE
604    //   subjectKeyIdentifier  [0] SubjectKeyIdentifier  -- OCTET STRING
605    // }
606    let (rid, after_rid) = if version == 0 {
607        let (ias_body, rest) = read_sequence(after_ver)?;
608        // IssuerAndSerialNumber ::= SEQUENCE { issuer Name, serial INTEGER }
609        // We need the raw DER of `issuer` to byte-compare against the
610        // user's certificate's `issuer`, so reconstruct the slice
611        // including its tag+length header by computing the offset
612        // from `ias_body` to the start of the `serialNumber` TLV.
613        let (issuer_tlv, ias_after_issuer) = super::der::read_tlv(ias_body)?;
614        if issuer_tlv.class != Class::Universal
615            || issuer_tlv.tag_number != super::der::tag::SEQUENCE
616        {
617            return Err(PdfError::other(
618                "CMS: IssuerAndSerialNumber.issuer must be a SEQUENCE",
619            ));
620        }
621        let issuer_total = ias_body.len() - ias_after_issuer.len();
622        let issuer_der = ias_body[..issuer_total].to_vec();
623        let (serial_body, _) = read_integer_bytes(ias_after_issuer)?;
624        (
625            RecipientId::IssuerAndSerial(IssuerAndSerial {
626                issuer_der,
627                serial: serial_body.to_vec(),
628            }),
629            rest,
630        )
631    } else {
632        // [0] IMPLICIT OCTET STRING — context-specific primitive
633        // wrapping the recipient's SubjectKeyIdentifier (RFC 5652
634        // §6.2.1). The body bytes are the raw 20-byte SHA-1 of the
635        // recipient cert's `SubjectPublicKeyInfo` BIT STRING contents
636        // (RFC 5280 §4.2.1.2 method 1).
637        let (tlv, rest) = super::der::read_tlv(after_ver)?;
638        if tlv.class != Class::ContextSpecific || tlv.tag_number != 0 {
639            return Err(PdfError::other(format!(
640                "CMS: KeyTransRecipientInfo[v=2] expects [0] SubjectKeyIdentifier, got class={:?} tag={}",
641                tlv.class, tlv.tag_number
642            )));
643        }
644        if tlv.constructed {
645            return Err(PdfError::other(
646                "CMS: SubjectKeyIdentifier must be primitive [0] IMPLICIT OCTET STRING",
647            ));
648        }
649        (RecipientId::SubjectKeyIdentifier(tlv.body.to_vec()), rest)
650    };
651    // KeyEncryptionAlgorithm ::= AlgorithmIdentifier
652    let (alg_seq, after_alg) = read_sequence(after_rid)?;
653    let (kea_oid, _alg_params) = read_oid(alg_seq)?;
654    if kea_oid != OID_RSA_ENCRYPTION {
655        return Err(PdfError::other(format!(
656            "CMS: unsupported KeyEncryptionAlgorithm {kea_oid:?} (only rsaEncryption)"
657        )));
658    }
659    let (enc_key, after_key) = read_octet_string(after_alg)?;
660    if !after_key.is_empty() {
661        return Err(PdfError::other(
662            "CMS: trailing bytes after KeyTransRecipientInfo.encryptedKey",
663        ));
664    }
665    Ok((
666        Some(RecipientInfoVariant::KeyTrans(KeyTransRecipientInfo {
667            rid,
668            key_encryption_oid: kea_oid,
669            encrypted_key: enc_key.to_vec(),
670        })),
671        tail,
672    ))
673}
674
675/// Parse the body of a `[1] IMPLICIT KeyAgreeRecipientInfo` per RFC
676/// 5652 §6.2.2. The implicit tag replaces the SEQUENCE's universal
677/// tag, so `data` here is the KARI's body bytes — the same shape we
678/// would otherwise see *inside* a `read_sequence(...)` call.
679///
680/// ```asn.1
681/// KeyAgreeRecipientInfo ::= SEQUENCE {
682///   version                CMSVersion,                 -- always 3
683///   originator         [0] EXPLICIT OriginatorIdentifierOrKey,
684///   ukm                [1] EXPLICIT UserKeyingMaterial OPTIONAL,
685///   keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier,
686///   recipientEncryptedKeys RecipientEncryptedKeys
687/// }
688/// ```
689fn parse_kari(data: &[u8]) -> Result<KeyAgreeRecipientInfo, PdfError> {
690    let (version, body) = read_integer_u64(data)?;
691    if version != 3 {
692        return Err(PdfError::other(format!(
693            "CMS: KeyAgreeRecipientInfo version must be 3 (got {version})"
694        )));
695    }
696    // [0] EXPLICIT OriginatorIdentifierOrKey
697    let (orig_body, body) = read_context(body, 0)?;
698    let originator = parse_originator(orig_body)?;
699    // [1] EXPLICIT UserKeyingMaterial OPTIONAL
700    let (ukm_opt, body) = maybe_read_context(body, 1)?;
701    let ukm = match ukm_opt {
702        Some(b) => {
703            // The UKM body is itself an OCTET STRING.
704            let (ukm_bytes, rest) = read_octet_string(b)?;
705            if !rest.is_empty() {
706                return Err(PdfError::other(
707                    "CMS: KARI ukm context wrapper has trailing bytes",
708                ));
709            }
710            ukm_bytes.to_vec()
711        }
712        None => Vec::new(),
713    };
714    // KeyEncryptionAlgorithmIdentifier
715    let (alg_body, body) = read_sequence(body)?;
716    let (kea_oid, alg_params) = read_oid(alg_body)?;
717    // recipientEncryptedKeys SEQUENCE OF RecipientEncryptedKey
718    let (rek_body, body) = read_sequence(body)?;
719    if !body.is_empty() {
720        return Err(PdfError::other(
721            "CMS: KARI has trailing bytes after recipientEncryptedKeys",
722        ));
723    }
724    let mut recipient_encrypted_keys = Vec::new();
725    let mut cursor = rek_body;
726    while !cursor.is_empty() {
727        let (rek, tail) = parse_recipient_encrypted_key(cursor)?;
728        recipient_encrypted_keys.push(rek);
729        cursor = tail;
730    }
731    if recipient_encrypted_keys.is_empty() {
732        return Err(PdfError::other("CMS: KARI recipientEncryptedKeys is empty"));
733    }
734    Ok(KeyAgreeRecipientInfo {
735        originator,
736        ukm,
737        key_encryption_oid: kea_oid,
738        key_encryption_params: alg_params.to_vec(),
739        recipient_encrypted_keys,
740    })
741}
742
743/// Parse `OriginatorIdentifierOrKey` (RFC 5652 §6.2.2).
744///
745/// ```asn.1
746/// OriginatorIdentifierOrKey ::= CHOICE {
747///   issuerAndSerialNumber  IssuerAndSerialNumber,
748///   subjectKeyIdentifier  [0] SubjectKeyIdentifier,
749///   originatorKey         [1] OriginatorPublicKey
750/// }
751/// ```
752fn parse_originator(data: &[u8]) -> Result<OriginatorId, PdfError> {
753    let (peek, _) = super::der::read_tlv(data)?;
754    if peek.class == Class::ContextSpecific {
755        match peek.tag_number {
756            0 => {
757                // [0] IMPLICIT SubjectKeyIdentifier (OCTET STRING).
758                if peek.constructed {
759                    return Err(PdfError::other(
760                        "CMS: KARI originator [0] SKI must be primitive",
761                    ));
762                }
763                Ok(OriginatorId::SubjectKeyIdentifier(peek.body.to_vec()))
764            }
765            1 => {
766                // [1] IMPLICIT OriginatorPublicKey — body is the SPKI
767                // SEQUENCE contents.
768                let opk = parse_originator_public_key(peek.body)?;
769                Ok(OriginatorId::OriginatorKey(opk))
770            }
771            other => Err(PdfError::other(format!(
772                "CMS: KARI originator unknown context-tag {other}"
773            ))),
774        }
775    } else {
776        // Untagged SEQUENCE — IssuerAndSerialNumber.
777        let (ias_body, rest) = read_sequence(data)?;
778        if !rest.is_empty() {
779            return Err(PdfError::other(
780                "CMS: KARI originator IAS has trailing bytes",
781            ));
782        }
783        let (issuer_tlv, ias_after_issuer) = super::der::read_tlv(ias_body)?;
784        if issuer_tlv.class != Class::Universal
785            || issuer_tlv.tag_number != super::der::tag::SEQUENCE
786        {
787            return Err(PdfError::other(
788                "CMS: KARI originator IAS issuer must be a SEQUENCE",
789            ));
790        }
791        let issuer_total = ias_body.len() - ias_after_issuer.len();
792        let issuer_der = ias_body[..issuer_total].to_vec();
793        let (serial_body, _) = read_integer_bytes(ias_after_issuer)?;
794        Ok(OriginatorId::IssuerAndSerial(IssuerAndSerial {
795            issuer_der,
796            serial: serial_body.to_vec(),
797        }))
798    }
799}
800
801/// Parse `OriginatorPublicKey` (RFC 5652 §6.2.2). Body shape is
802/// `SEQUENCE { algorithm AlgorithmIdentifier, publicKey BIT STRING }`.
803fn parse_originator_public_key(data: &[u8]) -> Result<OriginatorPublicKey, PdfError> {
804    let (alg_body, after_alg) = read_sequence(data)?;
805    let (alg_oid, alg_params) = read_oid(alg_body)?;
806    // BIT STRING — body has a leading unused-bits byte we drop.
807    let (bs, rest) = super::der::read_tlv(after_alg)?;
808    if bs.class != Class::Universal || bs.tag_number != super::der::tag::BIT_STRING {
809        return Err(PdfError::other(
810            "CMS: KARI OriginatorPublicKey expects BIT STRING for publicKey",
811        ));
812    }
813    if !rest.is_empty() {
814        return Err(PdfError::other(
815            "CMS: KARI OriginatorPublicKey has trailing bytes",
816        ));
817    }
818    if bs.body.is_empty() {
819        return Err(PdfError::other(
820            "CMS: KARI OriginatorPublicKey BIT STRING empty",
821        ));
822    }
823    Ok(OriginatorPublicKey {
824        algorithm_oid: alg_oid,
825        algorithm_params: alg_params.to_vec(),
826        public_key: bs.body[1..].to_vec(),
827    })
828}
829
830/// Parse one `RecipientEncryptedKey` (RFC 5652 §6.2.2).
831///
832/// ```asn.1
833/// RecipientEncryptedKey ::= SEQUENCE {
834///   rid          KeyAgreeRecipientIdentifier,
835///   encryptedKey EncryptedKey
836/// }
837/// KeyAgreeRecipientIdentifier ::= CHOICE {
838///   issuerAndSerialNumber  IssuerAndSerialNumber,
839///   rKeyId             [0] IMPLICIT RecipientKeyIdentifier
840/// }
841/// RecipientKeyIdentifier ::= SEQUENCE {
842///   subjectKeyIdentifier SubjectKeyIdentifier,
843///   date GeneralizedTime OPTIONAL,
844///   other OtherKeyAttribute OPTIONAL
845/// }
846/// ```
847fn parse_recipient_encrypted_key(data: &[u8]) -> Result<(RecipientEncryptedKey, &[u8]), PdfError> {
848    let (rek_body, tail) = read_sequence(data)?;
849    let (peek, _) = super::der::read_tlv(rek_body)?;
850    let (rid, after_rid) = if peek.class == Class::ContextSpecific && peek.tag_number == 0 {
851        // [0] IMPLICIT RecipientKeyIdentifier — body is the RKID's
852        // SEQUENCE contents. Consume the [0] TLV from the parent
853        // body to compute `after`, then peel its body for the SKI +
854        // the round-18 OPTIONAL `date` and `other` fields.
855        //
856        //   RecipientKeyIdentifier ::= SEQUENCE {
857        //     subjectKeyIdentifier SubjectKeyIdentifier,  -- OCTET STRING
858        //     date GeneralizedTime OPTIONAL,
859        //     other OtherKeyAttribute OPTIONAL
860        //   }
861        let (rkid_tlv, after) = super::der::read_tlv(rek_body)?;
862        let (ski, after_ski) = read_octet_string(rkid_tlv.body)?;
863        let mut cursor = after_ski;
864        let mut date: Option<Vec<u8>> = None;
865        // GeneralizedTime is universal tag 24 (RFC 5280 §4.1.2.5.2).
866        const TAG_GENERALIZED_TIME: u32 = 24;
867        if !cursor.is_empty() {
868            let (peek_dt, _) = super::der::read_tlv(cursor)?;
869            if peek_dt.class == Class::Universal && peek_dt.tag_number == TAG_GENERALIZED_TIME {
870                let (dt_tlv, after_dt) = super::der::read_tlv(cursor)?;
871                date = Some(dt_tlv.body.to_vec());
872                cursor = after_dt;
873            }
874        }
875        let mut other: Option<OtherKeyAttribute> = None;
876        if !cursor.is_empty() {
877            // Either the next TLV is the `other` SEQUENCE, or there is
878            // trailing junk we should reject. RFC 5652 §6.2.2 marks
879            // `other` as a SEQUENCE with `keyAttrId` OID + optional
880            // `keyAttr` ANY.
881            let (peek_other, _) = super::der::read_tlv(cursor)?;
882            if peek_other.class == Class::Universal
883                && peek_other.tag_number == super::der::tag::SEQUENCE
884            {
885                let (oka_body, after_oka) = read_sequence(cursor)?;
886                let (oid_arcs, after_oid) = read_oid(oka_body)?;
887                let key_attr = after_oid.to_vec();
888                other = Some(OtherKeyAttribute {
889                    key_attr_id: oid_arcs,
890                    key_attr,
891                });
892                cursor = after_oka;
893            } else {
894                return Err(PdfError::other(format!(
895                    "CMS: RecipientKeyIdentifier trailing TLV class={:?} tag={} \
896                     is neither GeneralizedTime nor OtherKeyAttribute SEQUENCE",
897                    peek_other.class, peek_other.tag_number,
898                )));
899            }
900        }
901        if !cursor.is_empty() {
902            return Err(PdfError::other(
903                "CMS: RecipientKeyIdentifier has trailing bytes after \
904                 (subjectKeyIdentifier, date?, other?)",
905            ));
906        }
907        (
908            KeyAgreeRecipientId::RecipientKeyIdentifier {
909                ski: ski.to_vec(),
910                date,
911                other,
912            },
913            after,
914        )
915    } else {
916        // Untagged SEQUENCE → IssuerAndSerialNumber.
917        let (ias_body, after) = read_sequence(rek_body)?;
918        let (issuer_tlv, ias_after_issuer) = super::der::read_tlv(ias_body)?;
919        if issuer_tlv.class != Class::Universal
920            || issuer_tlv.tag_number != super::der::tag::SEQUENCE
921        {
922            return Err(PdfError::other(
923                "CMS: KARI REK IAS issuer must be a SEQUENCE",
924            ));
925        }
926        let issuer_total = ias_body.len() - ias_after_issuer.len();
927        let issuer_der = ias_body[..issuer_total].to_vec();
928        let (serial_body, _) = read_integer_bytes(ias_after_issuer)?;
929        (
930            KeyAgreeRecipientId::IssuerAndSerial(IssuerAndSerial {
931                issuer_der,
932                serial: serial_body.to_vec(),
933            }),
934            after,
935        )
936    };
937    let (enc_key, after_key) = read_octet_string(after_rid)?;
938    if !after_key.is_empty() {
939        return Err(PdfError::other(
940            "CMS: KARI RecipientEncryptedKey has trailing bytes",
941        ));
942    }
943    Ok((
944        RecipientEncryptedKey {
945            rid,
946            encrypted_key: enc_key.to_vec(),
947        },
948        tail,
949    ))
950}
951
952fn decode_content_alg(oid: &[u64], params: &[u8]) -> Result<ContentEncryption, PdfError> {
953    if oid == OID_RC4 {
954        // No parameters, or a NULL.
955        Ok(ContentEncryption::Rc4)
956    } else if oid == OID_AES128_CBC || oid == OID_AES256_CBC {
957        // Parameters are an OCTET STRING wrapping the IV.
958        let (iv, _) = read_octet_string(params)?;
959        if iv.len() != 16 {
960            return Err(PdfError::other(format!(
961                "CMS: AES-CBC IV must be 16 bytes (got {})",
962                iv.len()
963            )));
964        }
965        let mut iv_arr = [0u8; 16];
966        iv_arr.copy_from_slice(iv);
967        if oid == OID_AES128_CBC {
968            Ok(ContentEncryption::Aes128Cbc { iv: iv_arr })
969        } else {
970            Ok(ContentEncryption::Aes256Cbc { iv: iv_arr })
971        }
972    } else if oid == OID_RC2_CBC {
973        // Round-17: RC2-CBC. Parameters per RFC 3370 §5.1:
974        //   RC2CBCParameter ::= SEQUENCE {
975        //     rc2ParameterVersion INTEGER (0..255) DEFAULT 32,  -- effective-key version
976        //     iv OCTET STRING (size 8)
977        //   }
978        // Some legacy writers omit the parameter version; treat that as
979        // the DEFAULT 32 (mapped per RFC 2268 §6 to 32 effective bits).
980        // Other writers wrap params as a bare OCTET STRING (RFC 2268
981        // §6 wire form) — accept both.
982        let (param_seq, after_seq) = match read_sequence(params) {
983            Ok(parts) => parts,
984            Err(_) => {
985                // Bare OCTET STRING fallback (RFC 2268 §6).
986                let (iv_bytes, _) = read_octet_string(params)?;
987                if iv_bytes.len() != 8 {
988                    return Err(PdfError::other(format!(
989                        "CMS: RC2-CBC bare-OCTET-STRING IV must be 8 bytes (got {})",
990                        iv_bytes.len()
991                    )));
992                }
993                let mut iv_arr = [0u8; 8];
994                iv_arr.copy_from_slice(iv_bytes);
995                return Ok(ContentEncryption::Rc2Cbc {
996                    effective_key_bits: 32,
997                    iv: iv_arr,
998                });
999            }
1000        };
1001        let _ = after_seq;
1002        // SEQUENCE-wrapped params: optional INTEGER (rc2ParameterVersion),
1003        // mandatory OCTET STRING (iv).
1004        let mut cursor = param_seq;
1005        let mut effective_key_bits = 32u32;
1006        let (peek, _) = super::der::read_tlv(cursor)?;
1007        if peek.class == super::der::Class::Universal && peek.tag_number == super::der::tag::INTEGER
1008        {
1009            let (vers_u64, after) = read_integer_u64(cursor)?;
1010            // RFC 2268 §6 mapping: 160 → 40 bits, 120 → 64 bits, 58 →
1011            // 128 bits. Other values pass through as the literal
1012            // effective-key bit count (RFC 3370 §5.1's 32 default).
1013            effective_key_bits = match vers_u64 {
1014                160 => 40,
1015                120 => 64,
1016                58 => 128,
1017                v if v <= 255 => v as u32,
1018                _ => {
1019                    return Err(PdfError::other(format!(
1020                        "CMS: RC2 rc2ParameterVersion {vers_u64} out of range (RFC 2268 §6)"
1021                    )))
1022                }
1023            };
1024            cursor = after;
1025        }
1026        let (iv_bytes, rest) = read_octet_string(cursor)?;
1027        if !rest.is_empty() {
1028            return Err(PdfError::other(
1029                "CMS: RC2-CBC parameters trailing bytes after IV",
1030            ));
1031        }
1032        if iv_bytes.len() != 8 {
1033            return Err(PdfError::other(format!(
1034                "CMS: RC2-CBC IV must be 8 bytes (got {})",
1035                iv_bytes.len()
1036            )));
1037        }
1038        let mut iv_arr = [0u8; 8];
1039        iv_arr.copy_from_slice(iv_bytes);
1040        Ok(ContentEncryption::Rc2Cbc {
1041            effective_key_bits,
1042            iv: iv_arr,
1043        })
1044    } else if oid == OID_DES_EDE3_CBC {
1045        // Round-17: 3DES-CBC. Parameters per RFC 3370 §5.2 / RFC 5652
1046        // §12.4: OCTET STRING (size 8) — the 8-byte CBC IV.
1047        let (iv_bytes, _) = read_octet_string(params)?;
1048        if iv_bytes.len() != 8 {
1049            return Err(PdfError::other(format!(
1050                "CMS: DES-EDE3-CBC IV must be 8 bytes (got {})",
1051                iv_bytes.len()
1052            )));
1053        }
1054        let mut iv_arr = [0u8; 8];
1055        iv_arr.copy_from_slice(iv_bytes);
1056        Ok(ContentEncryption::DesEde3Cbc { iv: iv_arr })
1057    } else {
1058        Err(PdfError::other(format!(
1059            "CMS: unsupported contentEncryptionAlgorithm {oid:?}"
1060        )))
1061    }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066    use super::*;
1067    use crate::pubsec::cms_build::{build_envelope_aes256, RecipientPlain};
1068
1069    #[test]
1070    fn parse_handcrafted_aes256_envelope() {
1071        // Build a minimal envelope with one synthetic recipient.
1072        let issuer_der = super::super::der::write_sequence(b"");
1073        let recipient =
1074            RecipientPlain::ias(issuer_der.clone(), vec![0x01, 0x02, 0x03], vec![0xAA; 256]);
1075        let plaintext =
1076            b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13";
1077        let envelope = build_envelope_aes256(&[recipient], plaintext, &[0xBBu8; 32], &[0xCCu8; 16]);
1078        let parsed = parse_envelope(&envelope).expect("parse envelope");
1079        assert_eq!(parsed.recipients.len(), 1);
1080        match &parsed.recipients[0].rid {
1081            super::RecipientId::IssuerAndSerial(ias) => {
1082                assert_eq!(ias.serial, vec![0x01, 0x02, 0x03]);
1083                assert_eq!(ias.issuer_der, issuer_der);
1084            }
1085            other => panic!("unexpected rid: {other:?}"),
1086        }
1087        match parsed.content_encryption {
1088            ContentEncryption::Aes256Cbc { iv } => assert_eq!(iv, [0xCC; 16]),
1089            _ => panic!("expected AES256CBC"),
1090        }
1091    }
1092}