Skip to main content

enclavia_protocol/
kms_recipient.rs

1//! Attestation-gated KMS decrypt: the `CiphertextForRecipient` envelope.
2//!
3//! When a Nitro enclave calls `kms:Decrypt` with a `Recipient` parameter,
4//! KMS does NOT return the plaintext on the wire. Instead it returns
5//! `CiphertextForRecipient`: a CMS (RFC 5652) `EnvelopedData` structure in
6//! which the plaintext is encrypted under a fresh symmetric content key,
7//! and that content key is RSA-OAEP-wrapped to the **ephemeral public key
8//! the enclave embedded in its attestation document**. Only the enclave
9//! (which holds the matching ephemeral private key inside the VM) can
10//! unwrap it, so the parent that proxies the KMS call sees only ciphertext.
11//!
12//! AWS uses, for the Nitro recipient envelope:
13//!   * key encryption: RSAES-OAEP with SHA-256 (algorithm
14//!     `RSAES_OAEP_SHA_256`, the only `KeyEncryptionAlgorithm` Nitro
15//!     accepts), and
16//!   * content encryption: AES-256-CBC with PKCS#7 padding.
17//!
18//! This module is the matched pair:
19//!   * [`decode`] — the ENCLAVE side (`enclavia-crypto`): parse the
20//!     EnvelopedData KMS returned and recover the plaintext with the
21//!     ephemeral private key. This is the production-critical path.
22//!   * [`encode`] — the MOCK-KMS side: produce the same envelope for the
23//!     QEMU end-to-end test, so the in-enclave Recipient code path is
24//!     exercised exactly as it is against real KMS.
25//!
26//! The [`tests`] round-trip (`encode` then `decode`) is the deterministic
27//! correctness gate for the OAEP + AES-CBC + DER layering, independent of
28//! any enclave boot.
29
30use aes::cipher::block_padding::Pkcs7;
31use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
32use cms::content_info::ContentInfo;
33use cms::enveloped_data::{
34    EncryptedContentInfo, EnvelopedData, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo,
35    RecipientInfos,
36};
37use der::asn1::{ObjectIdentifier, OctetString, SetOfVec};
38use der::{Any, Decode, Encode};
39use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
40use spki::AlgorithmIdentifierOwned;
41
42type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
43type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
44
45// OIDs (RFC 5652 / PKCS#1 / NIST).
46const ID_ENVELOPED_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.3");
47const ID_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
48const RSAES_OAEP: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.7");
49const AES_256_CBC: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.42");
50
51/// Errors from building or opening a recipient envelope.
52#[derive(Debug, thiserror::Error)]
53pub enum RecipientError {
54    #[error("DER encode/decode: {0}")]
55    Der(String),
56    #[error("RSA: {0}")]
57    Rsa(String),
58    #[error("AES-CBC: {0}")]
59    Aes(String),
60    #[error("malformed envelope: {0}")]
61    Malformed(String),
62}
63
64/// Wrap `plaintext` as a `CiphertextForRecipient` CMS `EnvelopedData`
65/// encrypted to `recipient` (the enclave's ephemeral RSA public key). Used
66/// by mock-kms (and the round-trip test); real KMS produces the same shape.
67pub fn encode(recipient: &RsaPublicKey, plaintext: &[u8]) -> Result<Vec<u8>, RecipientError> {
68    let mut rng = rand::rngs::OsRng;
69
70    // Fresh content-encryption key + IV.
71    let mut cek = [0u8; 32];
72    let mut iv = [0u8; 16];
73    {
74        use rand::RngCore;
75        rng.fill_bytes(&mut cek);
76        rng.fill_bytes(&mut iv);
77    }
78
79    // AES-256-CBC encrypt the plaintext (PKCS#7 padding).
80    let encrypted_content = Aes256CbcEnc::new(&cek.into(), &iv.into())
81        .encrypt_padded_vec_mut::<Pkcs7>(plaintext);
82
83    // RSA-OAEP-SHA256 wrap the CEK to the recipient's ephemeral key.
84    let encrypted_key = recipient
85        .encrypt(&mut rng, Oaep::new::<sha2::Sha256>(), &cek)
86        .map_err(|e| RecipientError::Rsa(e.to_string()))?;
87
88    // KeyTransRecipientInfo. The recipient id is irrelevant to the enclave
89    // (it holds exactly one ephemeral key), so use an empty
90    // SubjectKeyIdentifier rather than a synthetic cert.
91    let ktri = KeyTransRecipientInfo {
92        version: cms::content_info::CmsVersion::V0,
93        rid: RecipientIdentifier::SubjectKeyIdentifier(x509_cert::ext::pkix::SubjectKeyIdentifier(
94            OctetString::new(Vec::new()).map_err(|e| RecipientError::Der(e.to_string()))?,
95        )),
96        key_enc_alg: AlgorithmIdentifierOwned {
97            oid: RSAES_OAEP,
98            // OAEP params are omitted; both ends fix SHA-256 (the only
99            // algorithm Nitro's RSAES_OAEP_SHA_256 uses).
100            parameters: None,
101        },
102        enc_key: OctetString::new(encrypted_key).map_err(|e| RecipientError::Der(e.to_string()))?,
103    };
104
105    let mut recip_infos = SetOfVec::new();
106    recip_infos
107        .insert(RecipientInfo::Ktri(ktri))
108        .map_err(|e| RecipientError::Der(e.to_string()))?;
109
110    let enc_content_info = EncryptedContentInfo {
111        content_type: ID_DATA,
112        content_enc_alg: AlgorithmIdentifierOwned {
113            oid: AES_256_CBC,
114            parameters: Some(
115                Any::new(der::Tag::OctetString, iv.as_slice())
116                    .map_err(|e| RecipientError::Der(e.to_string()))?,
117            ),
118        },
119        encrypted_content: Some(
120            OctetString::new(encrypted_content).map_err(|e| RecipientError::Der(e.to_string()))?,
121        ),
122    };
123
124    let enveloped = EnvelopedData {
125        version: cms::content_info::CmsVersion::V0,
126        originator_info: None,
127        recip_infos: RecipientInfos(recip_infos),
128        encrypted_content: enc_content_info,
129        unprotected_attrs: None,
130    };
131
132    let content_info = ContentInfo {
133        content_type: ID_ENVELOPED_DATA,
134        content: Any::encode_from(&enveloped).map_err(|e| RecipientError::Der(e.to_string()))?,
135    };
136    content_info
137        .to_der()
138        .map_err(|e| RecipientError::Der(e.to_string()))
139}
140
141fn ber_err(m: impl std::fmt::Display) -> RecipientError {
142    RecipientError::Malformed(format!("BER->DER transcode: {m}"))
143}
144
145/// Transcode a (possibly BER, indefinite-length) ASN.1 blob to strict DER.
146///
147/// AWS KMS encodes the `CiphertextForRecipient` CMS with indefinite-length
148/// constructed encodings (BER), and may chunk OCTET STRINGs into constructed
149/// form. RustCrypto's `der` only accepts definite-length DER. This rewrites
150/// every TLV with a definite length and collapses constructed OCTET STRINGs
151/// into primitive ones, yielding bytes `ContentInfo::from_der` accepts. It is
152/// idempotent on input that is already strict DER. Fail-closed: any structural
153/// surprise returns an error (worst case recovery just fails, as it does now).
154fn ber_to_der(input: &[u8]) -> Result<Vec<u8>, RecipientError> {
155    let (out, rest) = transcode_tlv(input)?;
156    if !rest.is_empty() {
157        return Err(ber_err(format!("{} trailing bytes", rest.len())));
158    }
159    Ok(out)
160}
161
162/// Transcode one TLV; returns (definite-length DER bytes, remaining input).
163fn transcode_tlv(input: &[u8]) -> Result<(Vec<u8>, &[u8]), RecipientError> {
164    let id = *input.first().ok_or_else(|| ber_err("unexpected end of input"))?;
165    if id & 0x1f == 0x1f {
166        return Err(ber_err("high-tag-number form unsupported"));
167    }
168    let constructed = id & 0x20 != 0;
169    let (len, after_len) = read_len(&input[1..])?;
170
171    if !constructed {
172        let len = len.ok_or_else(|| ber_err("indefinite length on primitive"))?;
173        if after_len.len() < len {
174            return Err(ber_err("truncated primitive content"));
175        }
176        return Ok((emit_der(id, &after_len[..len]), &after_len[len..]));
177    }
178
179    // Constructed: transcode every child first, then decide how to re-emit
180    // this node. Two cases collapse a chunked string into a single PRIMITIVE
181    // OCTET STRING (strict DER rejects the constructed form with "not
182    // canonically encoded as DER"):
183    //   * a universal constructed OCTET STRING (tag 0x24), and
184    //   * an IMPLICIT-tagged OCTET STRING carried under a context-specific
185    //     constructed tag that KMS chunked, e.g. EncryptedContentInfo's
186    //     `encryptedContent [0] IMPLICIT OCTET STRING` -> tag 0xA0 holding 0x04
187    //     segments. We detect this by the children all being OCTET STRINGs and
188    //     re-emit with the constructed bit cleared (0xA0 -> 0x80), preserving
189    //     the tag number/class.
190    // Any other constructed tag (SEQUENCE, SET, an EXPLICIT [0] wrapper, ...)
191    // keeps its children's full TLVs unchanged.
192    let mut children: Vec<Vec<u8>> = Vec::new();
193    let after: &[u8];
194    match len {
195        Some(len) => {
196            if after_len.len() < len {
197                return Err(ber_err("truncated constructed content"));
198            }
199            let mut region = &after_len[..len];
200            after = &after_len[len..];
201            while !region.is_empty() {
202                let (child, rest) = transcode_tlv(region)?;
203                children.push(child);
204                region = rest;
205            }
206        }
207        None => {
208            // Indefinite length: children run until the end-of-contents (00 00).
209            let mut region = after_len;
210            loop {
211                if region.len() >= 2 && region[0] == 0x00 && region[1] == 0x00 {
212                    region = &region[2..];
213                    break;
214                }
215                let (child, rest) = transcode_tlv(region)?;
216                children.push(child);
217                region = rest;
218            }
219            after = region;
220        }
221    }
222
223    let is_universal_octet = id == 0x24;
224    let is_context = id & 0xc0 == 0x80; // context-specific class
225    let context_octet = is_context
226        && !children.is_empty()
227        && children.iter().all(|c| c.first() == Some(&0x04));
228    if is_universal_octet || context_octet {
229        let mut body = Vec::new();
230        for child in &children {
231            body.extend_from_slice(&octet_value(child)?);
232        }
233        let out_id = if is_universal_octet { 0x04 } else { id & !0x20 };
234        return Ok((emit_der(out_id, &body), after));
235    }
236
237    let mut body = Vec::new();
238    for child in &children {
239        body.extend_from_slice(child);
240    }
241    Ok((emit_der(id, &body), after))
242}
243
244/// Read an ASN.1 length. `None` = indefinite (0x80). Returns the content slice.
245fn read_len(input: &[u8]) -> Result<(Option<usize>, &[u8]), RecipientError> {
246    let b = *input.first().ok_or_else(|| ber_err("truncated length"))?;
247    let rest = &input[1..];
248    if b == 0x80 {
249        return Ok((None, rest));
250    }
251    if b & 0x80 == 0 {
252        return Ok((Some(b as usize), rest));
253    }
254    let n = (b & 0x7f) as usize;
255    if n == 0 || n > 4 || rest.len() < n {
256        return Err(ber_err("bad long-form length"));
257    }
258    let mut len = 0usize;
259    for &x in &rest[..n] {
260        len = (len << 8) | x as usize;
261    }
262    Ok((Some(len), &rest[n..]))
263}
264
265/// Emit a DER TLV with the definite-length encoding of `content`.
266fn emit_der(id: u8, content: &[u8]) -> Vec<u8> {
267    let mut out = vec![id];
268    let len = content.len();
269    if len < 0x80 {
270        out.push(len as u8);
271    } else {
272        let be = len.to_be_bytes();
273        let start = be.iter().position(|&b| b != 0).unwrap_or(be.len() - 1);
274        let lb = &be[start..];
275        out.push(0x80 | lb.len() as u8);
276        out.extend_from_slice(lb);
277    }
278    out.extend_from_slice(content);
279    out
280}
281
282/// Extract the value bytes of a transcoded primitive OCTET STRING TLV.
283fn octet_value(der: &[u8]) -> Result<Vec<u8>, RecipientError> {
284    if der.first() != Some(&0x04) {
285        return Err(ber_err("constructed OCTET STRING child is not an OCTET STRING"));
286    }
287    let (len, content) = read_len(&der[1..])?;
288    let len = len.ok_or_else(|| ber_err("indefinite OCTET STRING child"))?;
289    content
290        .get(..len)
291        .map(|s| s.to_vec())
292        .ok_or_else(|| ber_err("truncated OCTET STRING child"))
293}
294
295/// Open a `CiphertextForRecipient` CMS `EnvelopedData` with the enclave's
296/// ephemeral private key and recover the plaintext. The ENCLAVE side.
297pub fn decode(ephemeral: &RsaPrivateKey, cms_der: &[u8]) -> Result<Vec<u8>, RecipientError> {
298    // AWS KMS emits the CiphertextForRecipient CMS using indefinite-length
299    // (BER) constructed encodings, which RustCrypto's `der` (strict DER) rejects
300    // with "indefinite length disallowed". Transcode to definite-length DER
301    // first. Idempotent for input that is already strict DER (e.g. our own
302    // `encode` / mock-kms), so it is safe to apply unconditionally.
303    let der = ber_to_der(cms_der)?;
304    let content_info =
305        ContentInfo::from_der(&der).map_err(|e| RecipientError::Der(e.to_string()))?;
306    if content_info.content_type != ID_ENVELOPED_DATA {
307        return Err(RecipientError::Malformed(format!(
308            "content type {} is not id-envelopedData",
309            content_info.content_type
310        )));
311    }
312    let enveloped: EnvelopedData = content_info
313        .content
314        .decode_as()
315        .map_err(|e| RecipientError::Der(e.to_string()))?;
316
317    // Single recipient (the enclave's ephemeral key).
318    let ktri = enveloped
319        .recip_infos
320        .0
321        .as_slice()
322        .iter()
323        .find_map(|ri| match ri {
324            RecipientInfo::Ktri(k) => Some(k),
325            _ => None,
326        })
327        .ok_or_else(|| RecipientError::Malformed("no KeyTransRecipientInfo".into()))?;
328    if ktri.key_enc_alg.oid != RSAES_OAEP {
329        return Err(RecipientError::Malformed(format!(
330            "key encryption alg {} is not RSAES-OAEP",
331            ktri.key_enc_alg.oid
332        )));
333    }
334
335    // Unwrap the content key with RSA-OAEP-SHA256.
336    let cek = ephemeral
337        .decrypt(Oaep::new::<sha2::Sha256>(), ktri.enc_key.as_bytes())
338        .map_err(|e| RecipientError::Rsa(e.to_string()))?;
339    if cek.len() != 32 {
340        return Err(RecipientError::Malformed(format!(
341            "content key is {} bytes, expected 32",
342            cek.len()
343        )));
344    }
345
346    let eci = &enveloped.encrypted_content;
347    if eci.content_enc_alg.oid != AES_256_CBC {
348        return Err(RecipientError::Malformed(format!(
349            "content encryption alg {} is not AES-256-CBC",
350            eci.content_enc_alg.oid
351        )));
352    }
353    // IV is the algorithm parameter (OCTET STRING).
354    let iv_any = eci
355        .content_enc_alg
356        .parameters
357        .as_ref()
358        .ok_or_else(|| RecipientError::Malformed("AES-CBC algorithm has no IV parameter".into()))?;
359    let iv = iv_any
360        .decode_as::<OctetString>()
361        .map_err(|e| RecipientError::Der(e.to_string()))?;
362    let iv = iv.as_bytes();
363    if iv.len() != 16 {
364        return Err(RecipientError::Malformed(format!(
365            "IV is {} bytes, expected 16",
366            iv.len()
367        )));
368    }
369
370    let ciphertext = eci
371        .encrypted_content
372        .as_ref()
373        .ok_or_else(|| RecipientError::Malformed("no encrypted content".into()))?
374        .as_bytes();
375
376    let plaintext = Aes256CbcDec::new(cek.as_slice().into(), iv.into())
377        .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
378        .map_err(|e| RecipientError::Aes(e.to_string()))?;
379    Ok(plaintext)
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};
386
387    fn ephemeral_key() -> RsaPrivateKey {
388        // A fixed test key (generation is slow); any RSA-2048 key works.
389        RsaPrivateKey::new(&mut rand::rngs::OsRng, 2048).expect("keygen")
390    }
391
392    #[test]
393    fn round_trip_recovers_plaintext() {
394        let priv_key = ephemeral_key();
395        let pub_key = RsaPublicKey::from(&priv_key);
396        let secret = b"a 32-byte LUKS passphrase-here!!"; // 32 bytes
397        assert_eq!(secret.len(), 32);
398
399        let envelope = encode(&pub_key, secret).expect("encode");
400        // It is a real DER CMS structure, not the bare plaintext.
401        assert!(envelope.len() > secret.len() + 200);
402        assert_ne!(&envelope, secret);
403
404        let recovered = decode(&priv_key, &envelope).expect("decode");
405        assert_eq!(recovered.as_slice(), secret.as_slice());
406    }
407
408    #[test]
409    fn ber_to_der_normalises_indefinite_length_and_constructed_octet_strings() {
410        // Strict DER passes through unchanged (idempotent) -- SEQUENCE { INTEGER 1 }.
411        let der = [0x30, 0x03, 0x02, 0x01, 0x01];
412        assert_eq!(ber_to_der(&der).unwrap(), der);
413
414        // Indefinite-length SEQUENCE { INTEGER 1 } -> definite-length.
415        let ber_indef = [0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00];
416        assert_eq!(ber_to_der(&ber_indef).unwrap(), der);
417
418        // Constructed OCTET STRING (0x24), indefinite, chunked "ab"+"cd"
419        // collapses to one primitive OCTET STRING 04 04 61 62 63 64.
420        let want = [0x04, 0x04, 0x61, 0x62, 0x63, 0x64];
421        let ber_octet_indef = [
422            0x24, 0x80, 0x04, 0x02, 0x61, 0x62, 0x04, 0x02, 0x63, 0x64, 0x00, 0x00,
423        ];
424        assert_eq!(ber_to_der(&ber_octet_indef).unwrap(), want);
425
426        // Same in definite-length constructed form.
427        let ber_octet_def = [0x24, 0x08, 0x04, 0x02, 0x61, 0x62, 0x04, 0x02, 0x63, 0x64];
428        assert_eq!(ber_to_der(&ber_octet_def).unwrap(), want);
429
430        // Nested indefinite SEQUENCE { SEQUENCE { INTEGER 1 } } -> definite.
431        let ber_nested = [0x30, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00];
432        let der_nested = [0x30, 0x05, 0x30, 0x03, 0x02, 0x01, 0x01];
433        assert_eq!(ber_to_der(&ber_nested).unwrap(), der_nested);
434
435        // IMPLICIT-tagged, chunked OCTET STRING under a context-specific
436        // constructed tag [0] (0xA0), indefinite, "ab"+"cd" -> a single
437        // PRIMITIVE context [0] (0x80) 80 04 61 62 63 64. This is exactly how
438        // KMS chunks EncryptedContentInfo.encryptedContent.
439        let want_ctx = [0x80, 0x04, 0x61, 0x62, 0x63, 0x64];
440        let ber_ctx_indef = [
441            0xA0, 0x80, 0x04, 0x02, 0x61, 0x62, 0x04, 0x02, 0x63, 0x64, 0x00, 0x00,
442        ];
443        assert_eq!(ber_to_der(&ber_ctx_indef).unwrap(), want_ctx);
444        // Definite-length form of the same.
445        let ber_ctx_def = [0xA0, 0x08, 0x04, 0x02, 0x61, 0x62, 0x04, 0x02, 0x63, 0x64];
446        assert_eq!(ber_to_der(&ber_ctx_def).unwrap(), want_ctx);
447
448        // An EXPLICIT [0] wrapper (single SEQUENCE child, not an OCTET STRING)
449        // must STAY constructed -- this is ContentInfo.content.
450        let ber_explicit = [0xA0, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00];
451        let der_explicit = [0xA0, 0x05, 0x30, 0x03, 0x02, 0x01, 0x01];
452        assert_eq!(ber_to_der(&ber_explicit).unwrap(), der_explicit);
453
454        // And a real CMS envelope (already DER) survives the transcode intact.
455        let priv_key = ephemeral_key();
456        let pub_key = RsaPublicKey::from(&priv_key);
457        let env = encode(&pub_key, b"a 32-byte LUKS passphrase-here!!").expect("encode");
458        assert_eq!(ber_to_der(&env).unwrap(), env);
459    }
460
461    fn hex(s: &str) -> Vec<u8> {
462        let s: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
463        s.chunks(2)
464            .map(|c| u8::from_str_radix(std::str::from_utf8(c).unwrap(), 16).unwrap())
465            .collect()
466    }
467
468    #[test]
469    fn real_kms_envelope_transcodes_and_parses() {
470        // A genuine `CiphertextForRecipient` captured from real AWS KMS
471        // (kms:Decrypt with a Nitro Recipient) on the production storage path.
472        // It is indefinite-length BER throughout (note the leading 30 80 and
473        // the trailing run of 00 00 EOC pairs) AND chunks the encrypted content
474        // into a constructed context-specific [0] OCTET STRING -- the exact
475        // shape that made strict DER fail with first "indefinite length
476        // disallowed" then "CONTEXT-SPECIFIC [0] (constructed) not canonically
477        // encoded as DER". After the transcode it must parse cleanly.
478        let envelope = hex(concat!(
479            "308006092a864886f70d010703a08030800201023182016b30820167020102802000d8032ce4",
480            "b06d81bf38c4fc3ff1200706deb6678591c9b7ad900825840fa7eb303c06092a864886f70d01",
481            "0107302fa00f300d06096086480165030402010500a11c301a06092a864886f70d010108300d",
482            "06096086480165030402010500048201008c0955736cd2c8f2b5e39bd13c1d3d0b8d0b6c3b16",
483            "d29ba3e02bd072d7a54ea43d1b736a609654e3642de874a54f75f78c4abc1f131d87a4e92f36",
484            "c6b54c325371bbbd47ea1b3c141ca7c3c6f68d075469a23ee0e5e780783f858e8db11a019d25",
485            "0c81a3ec03d9171e86e6ecf3189aaa653e22e09cab0869bd08e9d5c007069d38da2a73c3c481",
486            "f19e9b33bcc3dee46fc8ccbbba297fc7a8fc1257875342ba9694173c43ae60602429fe81292b",
487            "d9faf46a54c5cf0ba83a7dcfa71f218c27c088d913b9f709c039348e46ea669e33dc6f0d0224",
488            "dcd52874774db1c0e8a5574f7a397500aaef0ac23395597b372c959f336297eb93734b3f951e",
489            "5b062adba12320308006092a864886f70d010701301d060960864801650304012a04104df038",
490            "d3456a8cba065420e8a0ef0278a0800430df06bc5b3e8ba95b06220d48057835b6bc42e1fa30",
491            "8530422b618a9839f12a0e27f6b1a667f14c7255d6f84d88e2300e00000000000000000000",
492        ));
493
494        let der = ber_to_der(&envelope).expect("transcode real KMS envelope");
495        // Idempotent on its own output (it is now strict DER).
496        assert_eq!(ber_to_der(&der).unwrap(), der);
497
498        let ci = ContentInfo::from_der(&der).expect("parse transcoded ContentInfo");
499        assert_eq!(ci.content_type, ID_ENVELOPED_DATA);
500        let ed: EnvelopedData = ci.content.decode_as().expect("decode EnvelopedData");
501
502        let ktri = ed
503            .recip_infos
504            .0
505            .as_slice()
506            .iter()
507            .find_map(|ri| match ri {
508                RecipientInfo::Ktri(k) => Some(k),
509                _ => None,
510            })
511            .expect("a KeyTransRecipientInfo");
512        assert_eq!(ktri.key_enc_alg.oid, RSAES_OAEP);
513        // RSA-2048 OAEP-wrapped CEK is 256 bytes.
514        assert_eq!(ktri.enc_key.as_bytes().len(), 256);
515
516        let eci = &ed.encrypted_content;
517        assert_eq!(eci.content_enc_alg.oid, AES_256_CBC);
518        let iv = eci
519            .content_enc_alg
520            .parameters
521            .as_ref()
522            .unwrap()
523            .decode_as::<OctetString>()
524            .unwrap();
525        assert_eq!(iv.as_bytes().len(), 16);
526        // The chunked [0] OCTET STRING collapsed to a single primitive blob.
527        assert!(eci.encrypted_content.as_ref().unwrap().as_bytes().len() >= 16);
528    }
529
530    #[test]
531    fn wrong_key_fails_to_open() {
532        let pub_key = RsaPublicKey::from(&ephemeral_key());
533        let envelope = encode(&pub_key, b"0123456789abcdef0123456789abcdef").expect("encode");
534        // A different private key cannot unwrap the content key.
535        let other = ephemeral_key();
536        assert!(decode(&other, &envelope).is_err());
537    }
538
539    #[test]
540    fn key_serialisation_round_trips_through_pkcs8() {
541        // The enclave generates the ephemeral key, ships its public half in
542        // the attestation doc, and keeps the private half; make sure a
543        // PKCS#8 round-trip (how it is carried) preserves decryptability.
544        let priv_key = ephemeral_key();
545        let der = priv_key.to_pkcs8_der().unwrap();
546        let restored = RsaPrivateKey::from_pkcs8_der(der.as_bytes()).unwrap();
547        let pub_key = RsaPublicKey::from(&priv_key);
548        let env = encode(&pub_key, b"0123456789abcdef0123456789abcdef").unwrap();
549        assert_eq!(
550            decode(&restored, &env).unwrap().as_slice(),
551            b"0123456789abcdef0123456789abcdef".as_slice()
552        );
553    }
554}