Skip to main content

rite_sdk/
cms.rs

1//! Read a CMS artifact back into a description of the wrap that produced it.
2//!
3//! The wrapping algorithms are not derivable from the wrapping key: the KEK
4//! size follows the content cipher, and the ECDH KDF digest is a library
5//! default. So the evidence has to come from the DER itself, which is also
6//! what an independent verifier has. This module walks that DER far enough to
7//! name the algorithm identifiers and the recipient, and no further; it never
8//! decrypts and never touches key material.
9//!
10//! Rite writes one recipient per wrap, so a `RecipientInfo` set is described by
11//! its first member. A foreign blob addressed to several recipients is
12//! described by the first of them, and the others are not reported.
13
14use crate::der;
15use crate::types::{Oid, RecipientInfoKind, WrapDescription};
16
17/// `id-envelopedData` (RFC 5652 §6).
18const ENVELOPED_DATA: &str = "1.2.840.113549.1.7.3";
19/// `id-ct-authEnvelopedData` (RFC 5083).
20const AUTH_ENVELOPED_DATA: &str = "1.2.840.113549.1.9.16.1.23";
21/// `id-ori-kem` (RFC 9629 §3), the `OtherRecipientInfo` carrying a KEM.
22const ORI_KEM: &str = "1.2.840.113549.1.9.16.13.3";
23
24/// A CMS artifact could not be read as a wrap.
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26#[error("CMS artifact is not readable as a wrap: {0}")]
27pub struct CmsReadError(String);
28
29/// What a CMS artifact says about the wrap that produced it.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CmsFacts {
32    /// The algorithm identifiers, as recorded on the wrap.
33    pub description: WrapDescription,
34    /// The recipient's subject key identifier, when the artifact names the
35    /// recipient that way. `None` when the recipient is named by issuer and
36    /// serial, which identifies a certificate rather than a key.
37    pub recipient_key_identifier: Option<Vec<u8>>,
38}
39
40/// Read a CMS `ContentInfo` DER into the facts of its wrap.
41///
42/// # Errors
43///
44/// Returns [`CmsReadError`] if the bytes are not a definite-length DER
45/// `ContentInfo` carrying `EnvelopedData` or `AuthEnvelopedData` with at
46/// least one recipient.
47pub fn describe(der: &[u8]) -> Result<CmsFacts, CmsReadError> {
48    let (content_info, rest) = read_tlv(der)?;
49    if !rest.is_empty() {
50        return Err(err("trailing bytes after ContentInfo"));
51    }
52    let body = content_info.expect_tag(TAG_SEQUENCE, "ContentInfo")?;
53
54    let (content_type, after_type) = read_tlv(body)?;
55    let content_type = oid_of(&content_type, "ContentInfo.contentType")?;
56    match content_type.as_str() {
57        ENVELOPED_DATA | AUTH_ENVELOPED_DATA => {}
58        other => {
59            return Err(err(format!(
60                "content type {other} is not an enveloped structure"
61            )));
62        }
63    }
64
65    let (explicit_content, _) = read_tlv(after_type)?;
66    let content = explicit_content.expect_tag(TAG_CONTEXT_0_CONSTRUCTED, "ContentInfo.content")?;
67    let (enveloped, _) = read_tlv(content)?;
68    let enveloped = enveloped.expect_tag(TAG_SEQUENCE, "EnvelopedData")?;
69
70    // version, then the optional implicit [0] originatorInfo, then the
71    // recipient set and the encrypted content.
72    let (_version, after_version) = read_tlv(enveloped)?;
73    let (next, after_next) = read_tlv(after_version)?;
74    let (recipients, after_recipients) = if next.tag == TAG_CONTEXT_0_CONSTRUCTED {
75        read_tlv(after_next)?
76    } else {
77        (next, after_next)
78    };
79    let recipients = recipients.expect_tag(TAG_SET, "recipientInfos")?;
80
81    let (first_recipient, _) = read_tlv(recipients)?;
82    let recipient = read_recipient(&first_recipient)?;
83
84    let (encrypted_content_info, _) = read_tlv(after_recipients)?;
85    let content_encryption = content_encryption_oid(&encrypted_content_info)?;
86
87    Ok(CmsFacts {
88        description: recipient
89            .description
90            .with_content_encryption(content_encryption),
91        recipient_key_identifier: recipient.key_identifier,
92    })
93}
94
95/// What one `RecipientInfo` contributes to the description.
96struct Recipient {
97    description: WrapDescription,
98    key_identifier: Option<Vec<u8>>,
99}
100
101fn read_recipient(info: &Tlv<'_>) -> Result<Recipient, CmsReadError> {
102    match info.tag {
103        TAG_SEQUENCE => read_ktri(info.value),
104        TAG_CONTEXT_1_CONSTRUCTED => read_kari(info.value),
105        TAG_CONTEXT_2_CONSTRUCTED => {
106            let kekri = read_kekri(info.value)?;
107            Ok(Recipient {
108                description: WrapDescription::new(RecipientInfoKind::Kekri, kekri.algorithm),
109                key_identifier: Some(kekri.key_identifier.to_vec()),
110            })
111        }
112        TAG_CONTEXT_3_CONSTRUCTED => Err(err("PasswordRecipientInfo is not a key wrap")),
113        TAG_CONTEXT_4_CONSTRUCTED => read_ori(info.value),
114        other => Err(err(format!("unknown RecipientInfo tag 0x{other:02x}"))),
115    }
116}
117
118/// `KeyTransRecipientInfo`: version, rid, keyEncryptionAlgorithm, encryptedKey.
119fn read_ktri(body: &[u8]) -> Result<Recipient, CmsReadError> {
120    let (_version, after_version) = read_tlv(body)?;
121    let (rid, after_rid) = read_tlv(after_version)?;
122    let (algorithm, _) = read_tlv(after_rid)?;
123
124    // rid is either IssuerAndSerialNumber or the implicit [0] subjectKeyIdentifier.
125    let key_identifier = (rid.tag == TAG_CONTEXT_0_PRIMITIVE).then(|| rid.value.to_vec());
126
127    Ok(Recipient {
128        description: WrapDescription::new(
129            RecipientInfoKind::Ktri,
130            algorithm_oid(&algorithm, "keyEncryptionAlgorithm")?,
131        ),
132        key_identifier,
133    })
134}
135
136/// The fields of a `KEKRecipientInfo` (RFC 5652 §6.2.3).
137struct Kekri<'a> {
138    key_identifier: &'a [u8],
139    algorithm: Oid,
140    encrypted_key: &'a [u8],
141}
142
143/// Walk a `KEKRecipientInfo`: version, kekid, keyEncryptionAlgorithm,
144/// encryptedKey.
145///
146/// The recipient is a key both sides already hold, so there is no public key
147/// and nothing to name one by. `kekid.keyIdentifier` is what identifies it,
148/// and Rite puts the key's check value there, which is the same value the
149/// transcript records and a token displays.
150///
151/// One walker for both readers: describing a wrap needs the identifier and the
152/// algorithm, opening one needs the identifier and the encrypted key, and two
153/// functions stepping through the same structure would be two places to get
154/// the field order wrong.
155fn read_kekri(body: &[u8]) -> Result<Kekri<'_>, CmsReadError> {
156    let (_version, after_version) = read_tlv(body)?;
157    let (kekid, after_kekid) = read_tlv(after_version)?;
158    let (algorithm, after_algorithm) = read_tlv(after_kekid)?;
159    let (encrypted_key, _) = read_tlv(after_algorithm)?;
160
161    let (key_identifier, _) = read_tlv(kekid.expect_tag(TAG_SEQUENCE, "KEKIdentifier")?)?;
162    Ok(Kekri {
163        key_identifier: key_identifier.expect_tag(TAG_OCTET_STRING, "kekid.keyIdentifier")?,
164        algorithm: algorithm_oid(&algorithm, "keyEncryptionAlgorithm")?,
165        encrypted_key: encrypted_key.expect_tag(TAG_OCTET_STRING, "encryptedKey")?,
166    })
167}
168
169/// `KeyAgreeRecipientInfo`: version, [0] originator, [1] ukm OPTIONAL,
170/// keyEncryptionAlgorithm, recipientEncryptedKeys.
171///
172/// RFC 5753 folds the KDF into the key-encryption OID itself, so the KDF is
173/// named there rather than in a field of its own. The algorithm's parameter
174/// is the identifier of the wrap applied to the CEK under the derived KEK.
175fn read_kari(body: &[u8]) -> Result<Recipient, CmsReadError> {
176    let (_version, after_version) = read_tlv(body)?;
177    let (originator, after_originator) = read_tlv(after_version)?;
178    if originator.tag != TAG_CONTEXT_0_CONSTRUCTED {
179        return Err(err("KeyAgreeRecipientInfo without an originator"));
180    }
181    let (next, after_next) = read_tlv(after_originator)?;
182    let (algorithm, after_algorithm) = if next.tag == TAG_CONTEXT_1_CONSTRUCTED {
183        read_tlv(after_next)?
184    } else {
185        (next, after_next)
186    };
187
188    let (scheme, parameters) = algorithm_parts(&algorithm, "keyEncryptionAlgorithm")?;
189    let mut description = WrapDescription::new(RecipientInfoKind::Kari, scheme);
190    if let Some(parameters) = parameters {
191        description = description.with_kek_wrap(algorithm_oid(&parameters, "keyWrapAlgorithm")?);
192    }
193
194    let (recipient_keys, _) = read_tlv(after_algorithm)?;
195    Ok(Recipient {
196        description,
197        key_identifier: agreed_key_identifier(recipient_keys.value)?,
198    })
199}
200
201/// `RecipientEncryptedKeys`: a SEQUENCE OF `RecipientEncryptedKey`, whose rid
202/// is either an `IssuerAndSerialNumber` or `[0] RecipientKeyIdentifier`.
203fn agreed_key_identifier(body: &[u8]) -> Result<Option<Vec<u8>>, CmsReadError> {
204    let (first, _) = read_tlv(body)?;
205    let (rid, _) = read_tlv(first.expect_tag(TAG_SEQUENCE, "RecipientEncryptedKey")?)?;
206    if rid.tag != TAG_CONTEXT_0_CONSTRUCTED {
207        return Ok(None);
208    }
209    let (key_id, _) = read_tlv(rid.value)?;
210    Ok(Some(
211        key_id
212            .expect_tag(TAG_OCTET_STRING, "subjectKeyIdentifier")?
213            .to_vec(),
214    ))
215}
216
217/// `OtherRecipientInfo`: an `oriType` OID and a value read according to it.
218fn read_ori(body: &[u8]) -> Result<Recipient, CmsReadError> {
219    let (oid, after_oid) = read_tlv(body)?;
220    let oid = oid_of(&oid, "OtherRecipientInfo.oriType")?;
221    if oid.as_str() != ORI_KEM {
222        // A form this build does not know: name it and read no further,
223        // rather than guess at a structure whose shape is defined elsewhere.
224        return Ok(Recipient {
225            description: WrapDescription::new(RecipientInfoKind::Ori, oid),
226            key_identifier: None,
227        });
228    }
229    let (kemri, _) = read_tlv(after_oid)?;
230    read_kemri(kemri.expect_tag(TAG_SEQUENCE, "KEMRecipientInfo")?)
231}
232
233/// `KEMRecipientInfo` (RFC 9629 §3): version, rid, kem, kemct, kdf,
234/// kekLength, [0] ukm OPTIONAL, wrap, encryptedKey.
235///
236/// Unlike the key-agreement form, a KEM names its KDF in a field of its own,
237/// which is what the `kdf_oid` on a description is for.
238fn read_kemri(body: &[u8]) -> Result<Recipient, CmsReadError> {
239    let (_version, after_version) = read_tlv(body)?;
240    let (rid, after_rid) = read_tlv(after_version)?;
241    let (kem, after_kem) = read_tlv(after_rid)?;
242    let (_kemct, after_kemct) = read_tlv(after_kem)?;
243    let (kdf, after_kdf) = read_tlv(after_kemct)?;
244    let (_kek_length, after_kek_length) = read_tlv(after_kdf)?;
245
246    let (next, after_next) = read_tlv(after_kek_length)?;
247    let (wrap, _) = if next.tag == TAG_CONTEXT_0_CONSTRUCTED {
248        read_tlv(after_next)?
249    } else {
250        (next, after_next)
251    };
252
253    let description = WrapDescription::new(
254        RecipientInfoKind::Kemri,
255        algorithm_oid(&kem, "KEMRecipientInfo.kem")?,
256    )
257    .with_kdf(algorithm_oid(&kdf, "KEMRecipientInfo.kdf")?)
258    .with_kek_wrap(algorithm_oid(&wrap, "KEMRecipientInfo.wrap")?);
259
260    Ok(Recipient {
261        description,
262        key_identifier: (rid.tag == TAG_CONTEXT_0_PRIMITIVE).then(|| rid.value.to_vec()),
263    })
264}
265
266/// The content cipher, from `EncryptedContentInfo`: contentType,
267/// contentEncryptionAlgorithm, then the optional encrypted content.
268fn content_encryption_oid(info: &Tlv<'_>) -> Result<Oid, CmsReadError> {
269    let body = info.expect_tag(TAG_SEQUENCE, "encryptedContentInfo")?;
270    let (_content_type, after_type) = read_tlv(body)?;
271    let (algorithm, _) = read_tlv(after_type)?;
272    algorithm_oid(&algorithm, "contentEncryptionAlgorithm")
273}
274
275/// The `algorithm` field of an `AlgorithmIdentifier`.
276fn algorithm_oid(algorithm: &Tlv<'_>, field: &str) -> Result<Oid, CmsReadError> {
277    Ok(algorithm_parts(algorithm, field)?.0)
278}
279
280/// The `algorithm` and `parameters` fields of an `AlgorithmIdentifier`.
281fn algorithm_parts<'a>(
282    algorithm: &Tlv<'a>,
283    field: &str,
284) -> Result<(Oid, Option<Tlv<'a>>), CmsReadError> {
285    let body = algorithm.expect_tag(TAG_SEQUENCE, field)?;
286    let (oid, rest) = read_tlv(body)?;
287    let oid = oid_of(&oid, field)?;
288    let parameters = if rest.is_empty() {
289        None
290    } else {
291        Some(read_tlv(rest)?.0)
292    };
293    Ok((oid, parameters))
294}
295
296const TAG_INTEGER: u8 = 0x02;
297const TAG_OCTET_STRING: u8 = 0x04;
298const TAG_OID: u8 = 0x06;
299const TAG_SEQUENCE: u8 = 0x30;
300const TAG_SET: u8 = 0x31;
301const TAG_CONTEXT_0_PRIMITIVE: u8 = 0x80;
302const TAG_CONTEXT_0_CONSTRUCTED: u8 = 0xa0;
303const TAG_CONTEXT_1_CONSTRUCTED: u8 = 0xa1;
304const TAG_CONTEXT_2_CONSTRUCTED: u8 = 0xa2;
305const TAG_CONTEXT_3_CONSTRUCTED: u8 = 0xa3;
306const TAG_CONTEXT_4_CONSTRUCTED: u8 = 0xa4;
307
308/// One DER element: its tag and the bytes of its value.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310struct Tlv<'a> {
311    tag: u8,
312    value: &'a [u8],
313}
314
315impl<'a> Tlv<'a> {
316    /// The value bytes, if the element carries the expected tag.
317    fn expect_tag(&self, tag: u8, field: &str) -> Result<&'a [u8], CmsReadError> {
318        if self.tag == tag {
319            Ok(self.value)
320        } else {
321            Err(err(format!(
322                "{field}: expected tag 0x{tag:02x}, found 0x{:02x}",
323                self.tag
324            )))
325        }
326    }
327}
328
329/// Split one element off the front of `input`, returning it and the remainder.
330///
331/// Definite-length DER only: an indefinite length is BER, which no CMS
332/// structure Rite produces or accepts uses.
333fn read_tlv(input: &[u8]) -> Result<(Tlv<'_>, &[u8]), CmsReadError> {
334    let (&tag, after_tag) = input.split_first().ok_or_else(|| err("truncated tag"))?;
335    let (&first_length, after_first) = after_tag
336        .split_first()
337        .ok_or_else(|| err("truncated length"))?;
338
339    let (length, after_length) = match first_length {
340        short if short < 0x80 => (usize::from(short), after_first),
341        0x80 => return Err(err("indefinite length is BER, not DER")),
342        long => {
343            let count = usize::from(long & 0x7f);
344            if count > std::mem::size_of::<usize>() {
345                return Err(err("length does not fit in a usize"));
346            }
347            let (bytes, rest) = after_first
348                .split_at_checked(count)
349                .ok_or_else(|| err("truncated long-form length"))?;
350            let length = bytes
351                .iter()
352                .fold(0usize, |acc, &b| (acc << 8) | usize::from(b));
353            (length, rest)
354        }
355    };
356
357    let (value, rest) = after_length
358        .split_at_checked(length)
359        .ok_or_else(|| err("value runs past the end of the input"))?;
360    Ok((Tlv { tag, value }, rest))
361}
362
363/// Decode an OBJECT IDENTIFIER element into dotted-decimal form.
364///
365/// X.690 §8.19: every arc is base-128 with a continuation bit, and the first
366/// subidentifier packs the first two arcs as `40 * first + second`. That
367/// packed value is a subidentifier like any other, so it may span several
368/// bytes: `2.100` encodes as `0x81 0x34`, not as a single byte.
369fn oid_of(element: &Tlv<'_>, field: &str) -> Result<Oid, CmsReadError> {
370    let bytes = element.expect_tag(TAG_OID, field)?;
371
372    let mut subidentifiers = Vec::new();
373    let mut value: u64 = 0;
374    let mut pending = false;
375    for &byte in bytes {
376        value = value
377            .checked_mul(128)
378            .and_then(|shifted| shifted.checked_add(u64::from(byte & 0x7f)))
379            .ok_or_else(|| err(format!("{field}: OID arc overflows")))?;
380        pending = byte & 0x80 != 0;
381        if !pending {
382            subidentifiers.push(value);
383            value = 0;
384        }
385    }
386    if pending {
387        return Err(err(format!("{field}: OID ends mid-arc")));
388    }
389
390    let (&packed, rest) = subidentifiers
391        .split_first()
392        .ok_or_else(|| err(format!("{field}: empty OID")))?;
393    // The first arc is 0, 1 or 2, and only arc 2 may carry a second arc above
394    // 39, which is why the packed value cannot simply be divided.
395    let (first, second) = match packed.checked_sub(80) {
396        Some(above) => (2, above),
397        None => (packed / 40, packed % 40),
398    };
399
400    let mut arcs = vec![first.to_string(), second.to_string()];
401    arcs.extend(rest.iter().map(u64::to_string));
402    Oid::new(&arcs.join(".")).map_err(|e| err(format!("{field}: {e}")))
403}
404
405fn err(message: impl Into<String>) -> CmsReadError {
406    CmsReadError(message.into())
407}
408
409/// `id-data` (RFC 5652 §4), the content type of a wrapped key.
410const DATA: &str = "1.2.840.113549.1.7.1";
411
412/// The authentication tag lengths `GCMParameters.aes-ICVlen` may name, in
413/// bytes (RFC 5084 §3.2: `AES-GCM-ICVlen ::= INTEGER (12 | 13 | 14 | 15 | 16)`).
414const GCM_ICV_LENGTHS: std::ops::RangeInclusive<usize> = 12..=16;
415
416/// The tag length Rite writes.
417///
418/// The longest the profile allows, and the one OpenSSL itself writes, so a
419/// blob of ours and a blob of theirs differ in no field.
420pub const GCM_ICV_LEN: usize = 16;
421
422/// The parts of an `AuthEnvelopedData` addressed to one symmetric key.
423///
424/// Carried as a struct rather than assembled in place because the same fields
425/// are written on the way out and read on the way back, and a field the two
426/// sides disagree about is the defect this shape makes impossible.
427///
428/// The crypto is the caller's: this module puts bytes in a structure and takes
429/// them out again, and never holds a key.
430///
431/// ```
432/// use rite_sdk::cms::{self, KekEnvelope};
433///
434/// // Values a backend produces: the CEK wrapped under the KEK, and the
435/// // content under that CEK.
436/// let envelope = KekEnvelope {
437///     key_identifier: vec![0x76, 0x3c, 0xbc],
438///     wrapped_cek: vec![0x11; 40],
439///     nonce: vec![0x22; 12],
440///     ciphertext: vec![0x33; 64],
441///     tag: vec![0x44; cms::GCM_ICV_LEN],
442/// };
443///
444/// let der = cms::write_kek_enveloped(&envelope)?;
445/// assert_eq!(cms::read_kek_enveloped(&der)?, envelope);
446///
447/// // And the same bytes describe the wrap that produced them.
448/// let facts = cms::describe(&der)?;
449/// assert_eq!(
450///     facts.recipient_key_identifier.as_deref(),
451///     Some(&[0x76, 0x3c, 0xbc][..])
452/// );
453/// # Ok::<(), rite_sdk::cms::CmsReadError>(())
454/// ```
455#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct KekEnvelope {
457    /// What names the key-encryption key, going in `kekid.keyIdentifier`.
458    pub key_identifier: Vec<u8>,
459    /// The content-encryption key, wrapped under the KEK with AES-KW.
460    pub wrapped_cek: Vec<u8>,
461    /// The 12-byte AES-GCM nonce.
462    pub nonce: Vec<u8>,
463    /// The content under AES-256-GCM.
464    pub ciphertext: Vec<u8>,
465    /// The AES-GCM authentication tag. Its length is what
466    /// `GCMParameters.aes-ICVlen` names, so the two cannot disagree.
467    pub tag: Vec<u8>,
468}
469
470/// Write an `AuthEnvelopedData` addressed to one symmetric key.
471///
472/// RFC 5083 for the structure, RFC 5652 §6.2.3 for the recipient, RFC 5084 for
473/// the `GCMParameters` the content algorithm carries. The key-encryption
474/// algorithm is `id-aes256-wrap`, matching the 256-bit KEK Rite writes.
475///
476/// # Errors
477///
478/// Returns [`CmsReadError`] if the tag is not a length RFC 5084 admits, or if
479/// one of this module's own object identifiers fails to encode, which is
480/// unreachable.
481pub fn write_kek_enveloped(envelope: &KekEnvelope) -> Result<Vec<u8>, CmsReadError> {
482    if !GCM_ICV_LENGTHS.contains(&envelope.tag.len()) {
483        return Err(err(format!(
484            "a {}-byte authentication tag is not one RFC 5084 admits",
485            envelope.tag.len()
486        )));
487    }
488    let oid = |dotted: &str| {
489        crate::der::object_identifier(dotted)
490            .ok_or_else(|| err(format!("{dotted} is not an object identifier")))
491    };
492
493    // KEKRecipientInfo, tagged [2] in the RecipientInfo CHOICE. Version is
494    // always 4 (RFC 5652 §6.2.3).
495    let kekri = der::tlv(
496        TAG_CONTEXT_2_CONSTRUCTED,
497        &[
498            der::small_integer(4),
499            der::sequence(&[der::octet_string(&envelope.key_identifier)]),
500            der::sequence(&[oid(crate::types::oid::AES_256_WRAP)?]),
501            der::octet_string(&envelope.wrapped_cek),
502        ]
503        .concat(),
504    );
505
506    // EncryptedContentInfo. The content is implicit [0], not an OCTET STRING,
507    // which is the tag a reader of this structure has to expect.
508    let content = der::sequence(&[
509        oid(DATA)?,
510        der::sequence(&[
511            oid(crate::types::oid::AES_256_GCM)?,
512            der::sequence(&[
513                der::octet_string(&envelope.nonce),
514                der::small_integer(u8::try_from(envelope.tag.len()).unwrap_or(0)),
515            ]),
516        ]),
517        der::tlv(TAG_CONTEXT_0_PRIMITIVE, &envelope.ciphertext),
518    ]);
519
520    // AuthEnvelopedData: version 0, no originatorInfo, no authAttrs, no
521    // unauthAttrs. Version is 0 because nothing here is a structure that
522    // RFC 5083 requires a higher one for.
523    let auth_enveloped = der::sequence(&[
524        der::small_integer(0),
525        der::set_of(&[kekri]),
526        content,
527        der::octet_string(&envelope.tag),
528    ]);
529
530    Ok(der::sequence(&[
531        oid(AUTH_ENVELOPED_DATA)?,
532        der::tlv(TAG_CONTEXT_0_CONSTRUCTED, &auth_enveloped),
533    ]))
534}
535
536/// Read back what [`write_kek_enveloped`] wrote.
537///
538/// Accepts an `AuthEnvelopedData` whose single recipient is a
539/// `KEKRecipientInfo` under `id-aes256-wrap` with AES-256-GCM content and no
540/// `authAttrs`, which is what the OpenSSL CLI writes for the same inputs.
541///
542/// `authAttrs` is refused rather than skipped. RFC 5083 makes it the AEAD's
543/// additional authenticated data, so a blob carrying it authenticates bytes
544/// this decrypt would not feed to the tag, and opening one by ignoring the
545/// field would either fail confusingly or, worse, treat attributes as
546/// unauthenticated that the producer meant to bind.
547///
548/// # Errors
549///
550/// Returns [`CmsReadError`] if the bytes are not that structure. The
551/// algorithms are checked here rather than assumed, because the caller is
552/// about to decrypt with whatever this returns.
553pub fn read_kek_enveloped(der_bytes: &[u8]) -> Result<KekEnvelope, CmsReadError> {
554    let (content_info, rest) = read_tlv(der_bytes)?;
555    if !rest.is_empty() {
556        return Err(err("trailing bytes after ContentInfo"));
557    }
558    let body = content_info.expect_tag(TAG_SEQUENCE, "ContentInfo")?;
559    let (content_type, after_type) = read_tlv(body)?;
560    if oid_of(&content_type, "ContentInfo.contentType")?.as_str() != AUTH_ENVELOPED_DATA {
561        return Err(err("not an AuthEnvelopedData"));
562    }
563
564    let (explicit, _) = read_tlv(after_type)?;
565    let (auth_enveloped, _) =
566        read_tlv(explicit.expect_tag(TAG_CONTEXT_0_CONSTRUCTED, "ContentInfo.content")?)?;
567    let auth_enveloped = auth_enveloped.expect_tag(TAG_SEQUENCE, "AuthEnvelopedData")?;
568
569    // version, then the optional implicit [0] originatorInfo, which `describe`
570    // also steps over. Rite writes none, and a foreign blob carrying one is
571    // still one this can open.
572    let (_version, after_version) = read_tlv(auth_enveloped)?;
573    let (next, after_next) = read_tlv(after_version)?;
574    let (recipients, after_recipients) = if next.tag == TAG_CONTEXT_0_CONSTRUCTED {
575        read_tlv(after_next)?
576    } else {
577        (next, after_next)
578    };
579    let recipients = recipients.expect_tag(TAG_SET, "recipientInfos")?;
580
581    let (recipient, more) = read_tlv(recipients)?;
582    if !more.is_empty() {
583        // Rite writes one. Several would mean choosing which key to try, and
584        // silently taking the first is how a blob opens with the wrong one.
585        return Err(err("more than one recipient"));
586    }
587    let kekri = read_kekri(recipient.expect_tag(TAG_CONTEXT_2_CONSTRUCTED, "KEKRecipientInfo")?)?;
588    if kekri.algorithm.as_str() != crate::types::oid::AES_256_WRAP {
589        // Checked here rather than in the walker, because describing a foreign
590        // blob should report the algorithm it names, and only opening one has
591        // to insist on the algorithm this crate can run.
592        return Err(err(format!(
593            "key encryption algorithm {} is not id-aes256-wrap",
594            kekri.algorithm
595        )));
596    }
597
598    let (content_info, after_content) = read_tlv(after_recipients)?;
599    let (nonce, icv_len, ciphertext) =
600        read_gcm_content(content_info.expect_tag(TAG_SEQUENCE, "authEncryptedContentInfo")?)?;
601
602    let (mac, _) = read_tlv(after_content)?;
603    if mac.tag == TAG_CONTEXT_1_CONSTRUCTED {
604        // authAttrs. RFC 5083 makes these the AEAD's additional authenticated
605        // data, which this decrypt leaves empty, so opening such a blob would
606        // compute the tag over different bytes than the producer did. Named
607        // here, since `expected mac` would send a reader looking at the wrong
608        // field.
609        return Err(err(
610            "this AuthEnvelopedData carries authAttrs, which are part of what its \
611             tag authenticates and which this reader does not cover",
612        ));
613    }
614    let tag = mac.expect_tag(TAG_OCTET_STRING, "mac")?;
615    if tag.len() != icv_len {
616        // The structure would then describe a tag other than the one it
617        // carries, and the decrypt would run against whichever the caller
618        // happened to use.
619        return Err(err(format!(
620            "GCMParameters names a {icv_len}-byte tag and the mac is {} bytes",
621            tag.len()
622        )));
623    }
624
625    Ok(KekEnvelope {
626        key_identifier: kekri.key_identifier.to_vec(),
627        wrapped_cek: kekri.encrypted_key.to_vec(),
628        nonce,
629        ciphertext,
630        tag: tag.to_vec(),
631    })
632}
633
634/// The nonce, the declared tag length, and the ciphertext out of an AES-GCM
635/// `EncryptedContentInfo`.
636fn read_gcm_content(body: &[u8]) -> Result<(Vec<u8>, usize, Vec<u8>), CmsReadError> {
637    let (_content_type, after_type) = read_tlv(body)?;
638    let (algorithm, after_algorithm) = read_tlv(after_type)?;
639
640    let (algorithm_body, parameters) = algorithm_parts(&algorithm, "contentEncryptionAlgorithm")?;
641    if algorithm_body.as_str() != crate::types::oid::AES_256_GCM {
642        return Err(err(format!(
643            "content encryption algorithm {algorithm_body} is not id-aes256-GCM"
644        )));
645    }
646    let parameters = parameters.ok_or_else(|| err("AES-GCM content carries no GCMParameters"))?;
647    let (nonce, after_nonce) = read_tlv(parameters.expect_tag(TAG_SEQUENCE, "GCMParameters")?)?;
648    let nonce = nonce.expect_tag(TAG_OCTET_STRING, "GCMParameters.aes-nonce")?;
649
650    // aes-ICVlen carries a DEFAULT of 12, so an absent field means 12 rather
651    // than an unspecified length. OpenSSL writes 16 explicitly.
652    let icv_len = match read_tlv(after_nonce) {
653        Ok((field, _)) => usize::from(
654            *field
655                .expect_tag(TAG_INTEGER, "GCMParameters.aes-ICVlen")?
656                .first()
657                .ok_or_else(|| err("GCMParameters.aes-ICVlen is empty"))?,
658        ),
659        Err(_) => 12,
660    };
661    if !GCM_ICV_LENGTHS.contains(&icv_len) {
662        // A shorter tag is a weaker one, and a decrypt would accept it without
663        // noticing, so the profile's floor is enforced here.
664        return Err(err(format!(
665            "GCMParameters names a {icv_len}-byte tag, which RFC 5084 does not admit"
666        )));
667    }
668
669    let (content, _) = read_tlv(after_algorithm)?;
670    Ok((
671        nonce.to_vec(),
672        icv_len,
673        content
674            .expect_tag(TAG_CONTEXT_0_PRIMITIVE, "encryptedContent")?
675            .to_vec(),
676    ))
677}
678
679#[cfg(test)]
680#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
681mod tests {
682    use super::*;
683    use base64ct::Encoding as _;
684
685    #[test]
686    fn reads_a_dotted_oid_from_its_der_encoding() {
687        // id-aes256-GCM, 2.16.840.1.101.3.4.1.46: the first byte packs 2.16 as
688        // 40*2+16 = 96, and 840 spans two base-128 bytes.
689        let der = [
690            0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2e,
691        ];
692        let (element, rest) = read_tlv(&der).unwrap();
693        assert!(rest.is_empty());
694        assert_eq!(
695            oid_of(&element, "test").unwrap().as_str(),
696            "2.16.840.1.101.3.4.1.46"
697        );
698    }
699
700    /// X.690 packs the first two arcs into one subidentifier, and that
701    /// subidentifier spans several bytes as soon as it exceeds 127. Reading
702    /// only its first byte decodes `2.100.3` as something else entirely.
703    #[test]
704    fn reads_a_first_arc_that_spans_two_bytes() {
705        // 2.100.3: the packed first subidentifier is 40*2 + 100 = 180,
706        // encoded as 0x81 0x34.
707        let der = [0x06, 0x03, 0x81, 0x34, 0x03];
708        let (element, _) = read_tlv(&der).unwrap();
709        assert_eq!(oid_of(&element, "test").unwrap().as_str(), "2.100.3");
710    }
711
712    #[test]
713    fn reads_a_long_form_length() {
714        let mut der = vec![0x04, 0x81, 0x80];
715        der.extend(std::iter::repeat_n(0xaa, 0x80));
716        let (element, rest) = read_tlv(&der).unwrap();
717        assert_eq!(element.value.len(), 0x80);
718        assert!(rest.is_empty());
719    }
720
721    #[test]
722    fn refuses_indefinite_length_and_truncation() {
723        assert!(read_tlv(&[0x30, 0x80, 0x00, 0x00]).is_err());
724        assert!(read_tlv(&[0x30, 0x05, 0x01]).is_err());
725        assert!(read_tlv(&[0x30]).is_err());
726        assert!(read_tlv(&[]).is_err());
727    }
728
729    /// The `EnvelopedData` of RFC 4134 section 5.1, verbatim as section 5.3
730    /// carries it in an S/MIME body.
731    ///
732    /// Line breaks follow the RFC text so the constant can be diffed against
733    /// it by eye. Decoded, it is 290 bytes and matches the offsets the RFC
734    /// prints beside its own ASN.1 dump.
735    const RFC4134_ENVELOPED_DATA: &str = concat!(
736        "MIIBHgYJKoZIhvcNAQcDoIIBDzCCAQsCAQAxgcAwgb0CAQAwJjASMRAwDgYDVQQDEwdDYXJ",
737        "sUlNBAhBGNGvHgABWvBHTbi7NXXHQMA0GCSqGSIb3DQEBAQUABIGAC3EN5nGIiJi2lsGPcP",
738        "2iJ97a4e8kbKQz36zg6Z2i0yx6zYC4mZ7mX7FBs3IWg+f6KgCLx3M1eCbWx8+MDFbbpXadC",
739        "DgO8/nUkUNYeNxJtuzubGgzoyEd8Ch4H/dd9gdzTd+taTEgS0ipdSJuNnkVY4/M652jKKHR",
740        "LFf02hosdR8wQwYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAgtaMXpRwZRNYAgDsiSf8Z9P43",
741        "LrY4OxUk660cu1lXeCSFOSOpOJ7FuVyU=",
742    );
743
744    fn sample_envelope() -> KekEnvelope {
745        KekEnvelope {
746            key_identifier: vec![0x76, 0x3c, 0xbc],
747            wrapped_cek: vec![0x11; 40],
748            nonce: vec![0x22; 12],
749            ciphertext: vec![0x33; 64],
750            tag: vec![0x44; 16],
751        }
752    }
753
754    /// The writer and the reader are one contract, so what goes in comes out.
755    #[test]
756    fn a_kek_envelope_round_trips() {
757        let envelope = sample_envelope();
758        let der = write_kek_enveloped(&envelope).unwrap();
759        assert_eq!(read_kek_enveloped(&der).unwrap(), envelope);
760    }
761
762    /// And `describe` reads the same bytes as a wrap, which is what puts the
763    /// algorithms in the transcript and in front of `rite verify`.
764    #[test]
765    fn a_kek_envelope_describes_itself() {
766        let der = write_kek_enveloped(&sample_envelope()).unwrap();
767        let facts = describe(&der).unwrap();
768
769        assert_eq!(
770            facts.description.recipient_info,
771            Some(RecipientInfoKind::Kekri)
772        );
773        assert_eq!(
774            facts.description.key_encryption_oid.as_str(),
775            crate::types::oid::AES_256_WRAP
776        );
777        assert_eq!(
778            facts
779                .description
780                .content_encryption_oid
781                .as_ref()
782                .map(Oid::as_str),
783            Some(crate::types::oid::AES_256_GCM)
784        );
785        assert_eq!(
786            facts.recipient_key_identifier,
787            Some(vec![0x76, 0x3c, 0xbc]),
788            "the recipient is named by the key's check value"
789        );
790    }
791
792    /// The content type is `id-data`, the generic one, so the container is
793    /// not key-specific: what it carries is bytes.
794    #[test]
795    fn a_kek_envelope_carries_bytes_under_the_generic_content_type() {
796        let der = write_kek_enveloped(&sample_envelope()).unwrap();
797        let data_oid = crate::der::object_identifier(DATA).unwrap();
798        assert!(
799            der.windows(data_oid.len()).any(|w| w == data_oid),
800            "the encrypted content is id-data, not a key-specific type"
801        );
802    }
803
804    /// A long body crosses the boundary where DER switches to the long-form
805    /// length, which is where a hand-written encoder stops agreeing with a
806    /// reader.
807    #[test]
808    fn a_kek_envelope_round_trips_across_the_length_boundary() {
809        for size in [0, 1, 127, 128, 129, 255, 256, 1024, 65_536] {
810            let envelope = KekEnvelope {
811                ciphertext: vec![0x5a; size],
812                ..sample_envelope()
813            };
814            let der = write_kek_enveloped(&envelope).unwrap();
815            assert_eq!(
816                read_kek_enveloped(&der).unwrap(),
817                envelope,
818                "a {size}-byte content did not survive the round trip"
819            );
820        }
821    }
822
823    /// The tag length is written from the tag, so the two cannot disagree, and
824    /// a length outside the profile is refused rather than written.
825    #[test]
826    fn a_kek_envelope_carries_the_tag_length_it_declares() {
827        for length in [12usize, 13, 14, 15, 16] {
828            let envelope = KekEnvelope {
829                tag: vec![0x44; length],
830                ..sample_envelope()
831            };
832            let der = write_kek_enveloped(&envelope).unwrap();
833            assert_eq!(read_kek_enveloped(&der).unwrap().tag.len(), length);
834        }
835        for length in [0usize, 8, 11, 17] {
836            let envelope = KekEnvelope {
837                tag: vec![0x44; length],
838                ..sample_envelope()
839            };
840            assert!(
841                write_kek_enveloped(&envelope).is_err(),
842                "a {length}-byte tag is not one RFC 5084 admits"
843            );
844        }
845    }
846
847    /// Reading is the half that takes untrusted bytes, so it refuses what it
848    /// cannot vouch for rather than guessing.
849    #[test]
850    fn reading_refuses_what_is_not_this_structure() {
851        let der = write_kek_enveloped(&sample_envelope()).unwrap();
852
853        assert!(read_kek_enveloped(&[]).is_err(), "empty input");
854        assert!(
855            read_kek_enveloped(&der[..der.len() - 1]).is_err(),
856            "truncated input"
857        );
858        let mut trailing = der.clone();
859        trailing.push(0x00);
860        assert!(read_kek_enveloped(&trailing).is_err(), "trailing bytes");
861
862        // The RFC 4134 example is a real CMS artifact of a different shape.
863        let other = base64ct::Base64::decode_vec(RFC4134_ENVELOPED_DATA).unwrap();
864        assert!(
865            read_kek_enveloped(&other).is_err(),
866            "an EnvelopedData with a key transport recipient is not this"
867        );
868    }
869
870    /// Read a published CMS artifact this codebase had no hand in producing.
871    ///
872    /// The round-trip tests in `rite-openssl` only prove the reader agrees
873    /// with the encoder sitting next to it. This one is a third party's bytes,
874    /// checked by two independent implementors before the RFC shipped, and it
875    /// exercises the branch OpenSSL never takes for Rite: a recipient named by
876    /// issuer and serial number rather than by key identifier.
877    #[test]
878    fn reads_the_rfc_4134_enveloped_data_example() {
879        let der = base64ct::Base64::decode_vec(RFC4134_ENVELOPED_DATA).unwrap();
880        assert_eq!(der.len(), 290, "RFC 4134 section 5.1 is 290 bytes");
881
882        let facts = describe(&der).unwrap();
883        assert_eq!(
884            facts.description.recipient_info,
885            Some(RecipientInfoKind::Ktri)
886        );
887        assert_eq!(
888            facts.description.key_encryption_oid.as_str(),
889            crate::types::oid::RSA_ENCRYPTION
890        );
891        assert_eq!(
892            facts
893                .description
894                .content_encryption_oid
895                .as_ref()
896                .map(Oid::as_str),
897            Some("1.2.840.113549.3.7"),
898            "des-EDE3-CBC, as the RFC's dump names it"
899        );
900        assert_eq!(
901            facts.description.kek_wrap_oid, None,
902            "no KEK in key transport"
903        );
904        assert_eq!(
905            facts.recipient_key_identifier, None,
906            "this recipient is named by issuer and serial, which identifies a \
907             certificate rather than a key"
908        );
909
910        // The reader describes what it found without vouching for it. This
911        // artifact predates authenticated encryption, and the scheme Rite
912        // produces refuses to label it.
913        assert!(!facts.description.content_is_authenticated());
914        assert!(!crate::types::WrapScheme::CmsAes256Gcm.permits(&facts.description));
915    }
916
917    /// A CMS wrap to an ML-KEM recipient, recorded rather than generated.
918    ///
919    /// Produced by the OpenSSL 3.6.3 command line on 2026-09-06, outside this
920    /// codebase:
921    ///
922    /// ```text
923    /// openssl genpkey -algorithm ML-KEM-768 -out kem.pem
924    /// openssl pkey -in kem.pem -pubout -out kem_pub.pem
925    /// openssl x509 -new -force_pubkey kem_pub.pem -key ca.pem \
926    ///     -subj "/CN=rite-keywrap" -days 1 -out kem.crt
927    /// openssl cms -encrypt -keyid -aes-256-gcm -binary -outform DER \
928    ///     -in secret -out ml_kem_768_authenveloped.der kem.crt
929    /// ```
930    ///
931    /// RFC 9629 ships no test vectors, so this is the closest thing available:
932    /// a third party's bytes at a known version. It also keeps the KEM branch
933    /// covered on a build with no ML-KEM provider, where the round-trip test in
934    /// `rite-openssl` is compiled out entirely.
935    #[test]
936    fn reads_a_recorded_ml_kem_recipient() {
937        let der = include_bytes!("../tests/fixtures/ml_kem_768_authenveloped.der");
938        let facts = describe(der).unwrap();
939        let description = &facts.description;
940
941        assert_eq!(description.recipient_info, Some(RecipientInfoKind::Kemri));
942        // id-alg-ml-kem-768, NIST CSOR.
943        assert_eq!(
944            description.key_encryption_oid.as_str(),
945            "2.16.840.1.101.3.4.4.2"
946        );
947        // id-alg-hkdf-with-sha256, RFC 8619. A KEM names its KDF separately,
948        // which is what that field on a description exists for.
949        assert_eq!(
950            description.kdf_oid.as_ref().map(Oid::as_str),
951            Some("1.2.840.113549.1.9.16.3.28")
952        );
953        assert_eq!(
954            description.kek_wrap_oid.as_ref().map(Oid::as_str),
955            Some(crate::types::oid::AES_256_WRAP)
956        );
957        assert!(description.content_is_authenticated());
958        assert!(
959            facts.recipient_key_identifier.is_some(),
960            "the blob names its recipient by key id"
961        );
962    }
963
964    /// A blob carrying `authAttrs` is refused, by name.
965    ///
966    /// RFC 5083 makes those attributes the AEAD's additional authenticated
967    /// data, which this reader leaves empty, so opening one would compute the
968    /// tag over different bytes than its producer did. The fixture is a
969    /// conforming structure: `openssl cms -decrypt -secretkey` returns its
970    /// payload, and flipping one byte inside the attribute makes OpenSSL refuse
971    /// it, so the field really is covered by the tag.
972    #[test]
973    fn refuses_an_auth_enveloped_data_carrying_auth_attrs() {
974        let der = base64ct::Base64::decode_vec(concat!(
975            "MIHGBgsqhkiG9w0BCRABF6CBtjCBswIBADFEokICAQQwBgQEAQIDBDALBglg",
976            "hkgBZQMEAS0EKHQahB32AHvkD3mve3AQWdpTNQ/d825fM9NJOwFCxtF9DZJ4",
977            "PXsjRgcwOgYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwDAwMDAwMDAwMD",
978            "AwMCARCADbWcgkV4nh1cv9wwVfGhGjAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN",
979            "AQcBBBCiz7sY6w+LFX3mIoJbWngX",
980        ))
981        .unwrap();
982
983        let error = read_kek_enveloped(&der).unwrap_err();
984        assert!(
985            error.to_string().contains("authAttrs"),
986            "the refusal should name the field: {error}"
987        );
988
989        // `describe` reads only the recipient, which this structure has, so it
990        // still reports the wrap rather than refusing it.
991        let facts = describe(&der).expect("the recipient is readable either way");
992        assert_eq!(
993            facts.description.recipient_info,
994            Some(RecipientInfoKind::Kekri)
995        );
996    }
997
998    #[test]
999    fn refuses_bytes_that_are_not_an_enveloped_structure() {
1000        // A SEQUENCE holding the signedData OID rather than an enveloped one.
1001        let der = [
1002            0x30, 0x0b, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02,
1003        ];
1004        let error = describe(&der).unwrap_err();
1005        assert!(
1006            error.to_string().contains("1.2.840.113549.1.7.2"),
1007            "the error should name what was found: {error}"
1008        );
1009    }
1010}