Skip to main content

tzap_plugin_keywrap/
lib.rs

1#![forbid(unsafe_code)]
2
3use hpke::{
4    aead::{AesGcm256, ChaCha20Poly1305},
5    kdf::HkdfSha256,
6    kem::{DhP256HkdfSha256, X25519HkdfSha256},
7    Deserializable, Kem as HpkeKem, OpModeR, OpModeS, Serializable,
8};
9use openssl::{bn::BigNumContext, ec::PointConversionForm, nid::Nid, pkey::PKey, x509::X509};
10use rand_core::{OsRng, UnwrapErr};
11use sha2::{Digest, Sha256};
12use tzap_core::format::{FORMAT_VERSION, VOLUME_FORMAT_REV_44};
13use tzap_core::wire::RecipientRecordV1;
14use x509_parser::parse_x509_certificate;
15
16/// Profile identifier for key-wrap v1 recipient records.
17pub const KEYWRAP_PROFILE_ID: u16 = 0x0001;
18
19/// Recipient identity type for a DER-encoded x509 leaf certificate.
20pub const KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES: u16 = 2;
21
22const KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN: usize = 64;
23pub const KEYWRAP_PAYLOAD_VERSION: u16 = 1;
24const KEY_WRAP_CONTEXT_DOMAIN: &[u8] = b"tzap-keywrap-x509-hpke-v1-context\0";
25const HPKE_INFO_DOMAIN: &[u8] = b"tzap-x509-hpke-recipient-v1\0";
26const HPKE_AAD_DOMAIN: &[u8] = b"tzap-keywrap-master-key-v44\0";
27
28const X25519_KEM_ID: u16 = 0x0020;
29const P256_KEM_ID: u16 = 0x0010;
30const HKDF_SHA256_KDF_ID: u16 = 0x0001;
31const CHACHA20POLY1305_AEAD_ID: u16 = 0x0003;
32const AES256GCM_AEAD_ID: u16 = 0x0002;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35struct HpkeSuite {
36    kem_id: u16,
37    kdf_id: u16,
38    aead_id: u16,
39    enc_length: usize,
40    ciphertext_length: usize,
41}
42
43impl HpkeSuite {
44    fn for_profile(hpke_kem_id: u16, hpke_kdf_id: u16, hpke_aead_id: u16) -> Option<Self> {
45        let suites = [
46            Self {
47                kem_id: X25519_KEM_ID,
48                kdf_id: HKDF_SHA256_KDF_ID,
49                aead_id: CHACHA20POLY1305_AEAD_ID,
50                enc_length: 32,
51                ciphertext_length: 48,
52            },
53            Self {
54                kem_id: P256_KEM_ID,
55                kdf_id: HKDF_SHA256_KDF_ID,
56                aead_id: AES256GCM_AEAD_ID,
57                enc_length: 65,
58                ciphertext_length: 48,
59            },
60        ];
61
62        suites.into_iter().find(|suite| {
63            suite.kem_id == hpke_kem_id
64                && suite.kdf_id == hpke_kdf_id
65                && suite.aead_id == hpke_aead_id
66        })
67    }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum KeyWrapSuite {
72    X25519HkdfSha256ChaCha20Poly1305,
73    P256HkdfSha256Aes256Gcm,
74}
75
76impl KeyWrapSuite {
77    fn hpke_suite(self) -> HpkeSuite {
78        match self {
79            Self::X25519HkdfSha256ChaCha20Poly1305 => HpkeSuite {
80                kem_id: X25519_KEM_ID,
81                kdf_id: HKDF_SHA256_KDF_ID,
82                aead_id: CHACHA20POLY1305_AEAD_ID,
83                enc_length: 32,
84                ciphertext_length: 48,
85            },
86            Self::P256HkdfSha256Aes256Gcm => HpkeSuite {
87                kem_id: P256_KEM_ID,
88                kdf_id: HKDF_SHA256_KDF_ID,
89                aead_id: AES256GCM_AEAD_ID,
90                enc_length: 65,
91                ciphertext_length: 48,
92            },
93        }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ArchiveIdentity {
99    pub archive_uuid: [u8; 16],
100    pub session_id: [u8; 16],
101    pub format_version: u16,
102    pub volume_format_rev: u16,
103}
104
105impl Default for ArchiveIdentity {
106    fn default() -> Self {
107        Self {
108            archive_uuid: [0u8; 16],
109            session_id: [0u8; 16],
110            format_version: FORMAT_VERSION,
111            volume_format_rev: VOLUME_FORMAT_REV_44,
112        }
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct RecipientRecordMetadata {
118    pub profile_id: u16,
119    pub recipient_identity_type: u16,
120    pub recipient_identity_digest: [u8; 32],
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct RecipientRecordInput {
125    pub archive_identity: ArchiveIdentity,
126    pub metadata: RecipientRecordMetadata,
127    pub recipient_identity_bytes: Vec<u8>,
128    pub profile_payload_bytes: Vec<u8>,
129}
130
131/// Abstraction for locating a private key for profile-specific decryption.
132pub trait PrivateKeyLookup {
133    fn lookup_private_key(
134        &self,
135        archive_identity: &ArchiveIdentity,
136        metadata: &RecipientRecordMetadata,
137        recipient_identity_bytes: &[u8],
138    ) -> Option<Vec<u8>>;
139
140    fn is_recipient_certificate_accepted(
141        &self,
142        _archive_identity: &ArchiveIdentity,
143        _metadata: &RecipientRecordMetadata,
144        _recipient_identity_bytes: &[u8],
145        _recipient_spki_digest: &[u8; 32],
146    ) -> bool {
147        true
148    }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum KeyWrapOutcome {
153    UnsupportedProfileId,
154    UnsupportedArchiveIdentity,
155    UnsupportedRecipientIdentity,
156    UnsupportedSuite,
157    CertificatePolicyRejected,
158    InvalidRecord,
159    NoMatchingPrivateKey,
160    UnwrappedCandidateMasterKey {
161        master_key: [u8; 32],
162        recipient_spki_digest: [u8; 32],
163    },
164}
165
166#[derive(Debug, Clone)]
167struct ParsedRecipientIdentity {
168    recipient_identity_digest: [u8; 32],
169    recipient_spki_digest: [u8; 32],
170}
171
172#[derive(Debug, Clone)]
173struct ParsedProfilePayload {
174    suite: HpkeSuite,
175    enc: Vec<u8>,
176    ciphertext: Vec<u8>,
177    key_wrap_context_digest: [u8; 32],
178}
179
180/// Dispatch a single key-wrap recipient record to the profile parser.
181///
182/// For `profile_id == KEYWRAP_PROFILE_ID`, this validates:
183/// - recipient identity parsing for X.509 type=2 records
184/// - profile payload framing
185/// - context digest computation
186/// - supported HPKE suites and fixed lengths
187///
188/// It then asks the caller for a matching private key and attempts HPKE Open.
189pub fn dispatch_key_wrap_record<L>(
190    input: RecipientRecordInput,
191    private_key_lookup: &L,
192) -> KeyWrapOutcome
193where
194    L: PrivateKeyLookup + ?Sized,
195{
196    if input.archive_identity.format_version != FORMAT_VERSION
197        || input.archive_identity.volume_format_rev != VOLUME_FORMAT_REV_44
198    {
199        return KeyWrapOutcome::UnsupportedArchiveIdentity;
200    }
201
202    if input.metadata.profile_id != KEYWRAP_PROFILE_ID {
203        return KeyWrapOutcome::UnsupportedProfileId;
204    }
205
206    if input.metadata.recipient_identity_type != KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES {
207        return KeyWrapOutcome::UnsupportedRecipientIdentity;
208    }
209
210    let parsed_identity = match parse_x509_recipient_identity(&input.recipient_identity_bytes) {
211        Ok(identity) => identity,
212        Err(outcome) => return outcome,
213    };
214
215    if parsed_identity.recipient_identity_digest != input.metadata.recipient_identity_digest {
216        return KeyWrapOutcome::InvalidRecord;
217    }
218
219    let payload = match parse_profile_payload(&input.profile_payload_bytes) {
220        Ok(payload) => payload,
221        Err(outcome) => return outcome,
222    };
223
224    if let Err(outcome) =
225        validate_recipient_public_key_matches_suite(&input.recipient_identity_bytes, payload.suite)
226    {
227        return outcome;
228    }
229
230    let private_key = match private_key_lookup.lookup_private_key(
231        &input.archive_identity,
232        &input.metadata,
233        &input.recipient_identity_bytes,
234    ) {
235        Some(private_key) => private_key,
236        None => return KeyWrapOutcome::NoMatchingPrivateKey,
237    };
238
239    if !private_key_lookup.is_recipient_certificate_accepted(
240        &input.archive_identity,
241        &input.metadata,
242        &input.recipient_identity_bytes,
243        &parsed_identity.recipient_spki_digest,
244    ) {
245        return KeyWrapOutcome::CertificatePolicyRejected;
246    }
247
248    let expected_context_digest = compute_key_wrap_context_digest(
249        &input.archive_identity,
250        &input.metadata,
251        &parsed_identity,
252        payload.suite,
253    );
254    if payload.key_wrap_context_digest != expected_context_digest {
255        return KeyWrapOutcome::InvalidRecord;
256    }
257
258    match hpke_open_candidate_master_key(&payload, &private_key) {
259        Ok(master_key) => KeyWrapOutcome::UnwrappedCandidateMasterKey {
260            master_key,
261            recipient_spki_digest: parsed_identity.recipient_spki_digest,
262        },
263        Err(outcome) => outcome,
264    }
265}
266
267pub fn wrap_master_key_for_recipient(
268    archive_identity: ArchiveIdentity,
269    recipient_certificate_der: &[u8],
270    master_key: &[u8; 32],
271    suite: KeyWrapSuite,
272) -> Result<RecipientRecordV1, KeyWrapOutcome> {
273    if archive_identity.format_version != FORMAT_VERSION
274        || archive_identity.volume_format_rev != VOLUME_FORMAT_REV_44
275    {
276        return Err(KeyWrapOutcome::UnsupportedArchiveIdentity);
277    }
278
279    let identity = parse_x509_recipient_identity(recipient_certificate_der)?;
280    let metadata = RecipientRecordMetadata {
281        profile_id: KEYWRAP_PROFILE_ID,
282        recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
283        recipient_identity_digest: identity.recipient_identity_digest,
284    };
285    let profile_payload_bytes = hpke_seal_master_key(
286        recipient_certificate_der,
287        &archive_identity,
288        &metadata,
289        &identity,
290        master_key,
291        suite.hpke_suite(),
292    )?;
293
294    Ok(RecipientRecordV1 {
295        record_length: 0,
296        profile_id: KEYWRAP_PROFILE_ID,
297        recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
298        flags: 0,
299        recipient_identity_length: recipient_certificate_der.len() as u32,
300        profile_payload_length: profile_payload_bytes.len() as u32,
301        recipient_identity_digest: identity.recipient_identity_digest,
302        recipient_identity_bytes: recipient_certificate_der.to_vec(),
303        profile_payload_bytes,
304    })
305}
306
307fn hpke_seal_master_key(
308    recipient_certificate_der: &[u8],
309    archive_identity: &ArchiveIdentity,
310    metadata: &RecipientRecordMetadata,
311    identity: &ParsedRecipientIdentity,
312    master_key: &[u8; 32],
313    suite: HpkeSuite,
314) -> Result<Vec<u8>, KeyWrapOutcome> {
315    let key_wrap_context_digest =
316        compute_key_wrap_context_digest(archive_identity, metadata, identity, suite);
317    let info = hpke_info(&key_wrap_context_digest);
318    let aad = hpke_aad(&key_wrap_context_digest);
319    let (enc, ciphertext) = match (suite.kem_id, suite.aead_id) {
320        (X25519_KEM_ID, CHACHA20POLY1305_AEAD_ID) => {
321            let public_key = x25519_public_key_from_certificate(recipient_certificate_der)
322                .map_err(|_| KeyWrapOutcome::UnsupportedSuite)?;
323            let mut rng = UnwrapErr(OsRng);
324            let (enc, ciphertext) =
325                hpke::single_shot_seal::<ChaCha20Poly1305, HkdfSha256, X25519HkdfSha256, _>(
326                    &OpModeS::Base,
327                    &public_key,
328                    &info,
329                    master_key,
330                    &aad,
331                    &mut rng,
332                )
333                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
334            (enc.to_bytes().to_vec(), ciphertext)
335        }
336        (P256_KEM_ID, AES256GCM_AEAD_ID) => {
337            let public_key = p256_public_key_from_certificate(recipient_certificate_der)
338                .map_err(|_| KeyWrapOutcome::UnsupportedSuite)?;
339            let mut rng = UnwrapErr(OsRng);
340            let (enc, ciphertext) =
341                hpke::single_shot_seal::<AesGcm256, HkdfSha256, DhP256HkdfSha256, _>(
342                    &OpModeS::Base,
343                    &public_key,
344                    &info,
345                    master_key,
346                    &aad,
347                    &mut rng,
348                )
349                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
350            (enc.to_bytes().to_vec(), ciphertext)
351        }
352        _ => return Err(KeyWrapOutcome::UnsupportedSuite),
353    };
354
355    if enc.len() != suite.enc_length || ciphertext.len() != suite.ciphertext_length {
356        return Err(KeyWrapOutcome::InvalidRecord);
357    }
358
359    Ok(build_profile_payload(
360        suite,
361        &key_wrap_context_digest,
362        &enc,
363        &ciphertext,
364    ))
365}
366
367fn hpke_open_candidate_master_key(
368    payload: &ParsedProfilePayload,
369    private_key_bytes: &[u8],
370) -> Result<[u8; 32], KeyWrapOutcome> {
371    let info = hpke_info(&payload.key_wrap_context_digest);
372    let aad = hpke_aad(&payload.key_wrap_context_digest);
373    let plaintext = match (payload.suite.kem_id, payload.suite.aead_id) {
374        (X25519_KEM_ID, CHACHA20POLY1305_AEAD_ID) => {
375            let private_key = x25519_private_key_from_bytes(private_key_bytes)?;
376            let encapped_key = <X25519HkdfSha256 as HpkeKem>::EncappedKey::from_bytes(&payload.enc)
377                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
378            hpke::single_shot_open::<ChaCha20Poly1305, HkdfSha256, X25519HkdfSha256>(
379                &OpModeR::Base,
380                &private_key,
381                &encapped_key,
382                &info,
383                &payload.ciphertext,
384                &aad,
385            )
386            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
387        }
388        (P256_KEM_ID, AES256GCM_AEAD_ID) => {
389            let private_key = p256_private_key_from_bytes(private_key_bytes)?;
390            let encapped_key = <DhP256HkdfSha256 as HpkeKem>::EncappedKey::from_bytes(&payload.enc)
391                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
392            hpke::single_shot_open::<AesGcm256, HkdfSha256, DhP256HkdfSha256>(
393                &OpModeR::Base,
394                &private_key,
395                &encapped_key,
396                &info,
397                &payload.ciphertext,
398                &aad,
399            )
400            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
401        }
402        _ => return Err(KeyWrapOutcome::UnsupportedSuite),
403    };
404
405    if plaintext.len() != 32 {
406        return Err(KeyWrapOutcome::InvalidRecord);
407    }
408    let mut master_key = [0u8; 32];
409    master_key.copy_from_slice(&plaintext);
410    Ok(master_key)
411}
412
413fn build_profile_payload(
414    suite: HpkeSuite,
415    key_wrap_context_digest: &[u8; 32],
416    enc: &[u8],
417    ciphertext: &[u8],
418) -> Vec<u8> {
419    let mut payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len() + ciphertext.len()];
420    payload[0..2].copy_from_slice(&KEYWRAP_PAYLOAD_VERSION.to_le_bytes());
421    payload[2..4].copy_from_slice(&suite.kem_id.to_le_bytes());
422    payload[4..6].copy_from_slice(&suite.kdf_id.to_le_bytes());
423    payload[6..8].copy_from_slice(&suite.aead_id.to_le_bytes());
424    payload[8..10].copy_from_slice(&(enc.len() as u16).to_le_bytes());
425    payload[10..12].copy_from_slice(&(ciphertext.len() as u16).to_le_bytes());
426    payload[16..48].copy_from_slice(key_wrap_context_digest);
427    payload[KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN..KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len()]
428        .copy_from_slice(enc);
429    payload[KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len()..].copy_from_slice(ciphertext);
430    payload
431}
432
433fn parse_x509_recipient_identity(
434    recipient_identity_bytes: &[u8],
435) -> Result<ParsedRecipientIdentity, KeyWrapOutcome> {
436    let (remaining, parsed_cert) = parse_x509_certificate(recipient_identity_bytes)
437        .map_err(|_error| KeyWrapOutcome::InvalidRecord)?;
438
439    if !remaining.is_empty() {
440        return Err(KeyWrapOutcome::InvalidRecord);
441    }
442
443    match parsed_cert.key_usage() {
444        Ok(Some(key_usage)) => {
445            if !key_usage.value.key_agreement() {
446                return Err(KeyWrapOutcome::InvalidRecord);
447            }
448        }
449        Ok(None) => {}
450        Err(_) => {
451            return Err(KeyWrapOutcome::InvalidRecord);
452        }
453    }
454
455    let openssl_cert =
456        X509::from_der(recipient_identity_bytes).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
457    let openssl_public_key: PKey<_> = openssl_cert
458        .public_key()
459        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
460    let spki_der = openssl_public_key
461        .public_key_to_der()
462        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
463    let recipient_identity_digest = sha256_digest(recipient_identity_bytes);
464    let recipient_spki_digest = sha256_digest(&spki_der);
465
466    Ok(ParsedRecipientIdentity {
467        recipient_identity_digest,
468        recipient_spki_digest,
469    })
470}
471
472fn parse_profile_payload(
473    profile_payload_bytes: &[u8],
474) -> Result<ParsedProfilePayload, KeyWrapOutcome> {
475    if profile_payload_bytes.len() < KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN {
476        return Err(KeyWrapOutcome::InvalidRecord);
477    }
478
479    let payload_version = read_u16(profile_payload_bytes, 0)?;
480    let hpke_kem_id = read_u16(profile_payload_bytes, 2)?;
481    let hpke_kdf_id = read_u16(profile_payload_bytes, 4)?;
482    let hpke_aead_id = read_u16(profile_payload_bytes, 6)?;
483    let enc_length = usize::from(read_u16(profile_payload_bytes, 8)?);
484    let ciphertext_length = usize::from(read_u16(profile_payload_bytes, 10)?);
485    let flags = read_u32(profile_payload_bytes, 12)?;
486    let key_wrap_context_digest = read_array_32(profile_payload_bytes, 16)?;
487
488    if flags != 0 {
489        return Err(KeyWrapOutcome::InvalidRecord);
490    }
491
492    if !profile_payload_bytes[48..KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN]
493        .iter()
494        .all(|value| *value == 0)
495    {
496        return Err(KeyWrapOutcome::InvalidRecord);
497    }
498
499    if payload_version != KEYWRAP_PAYLOAD_VERSION {
500        return Err(KeyWrapOutcome::InvalidRecord);
501    }
502
503    let suite = HpkeSuite::for_profile(hpke_kem_id, hpke_kdf_id, hpke_aead_id)
504        .ok_or(KeyWrapOutcome::UnsupportedSuite)?;
505
506    if enc_length != suite.enc_length || ciphertext_length != suite.ciphertext_length {
507        return Err(KeyWrapOutcome::InvalidRecord);
508    }
509
510    let expected_length = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN
511        .checked_add(enc_length)
512        .and_then(|value| value.checked_add(ciphertext_length))
513        .ok_or(KeyWrapOutcome::InvalidRecord)?;
514
515    if profile_payload_bytes.len() != expected_length {
516        return Err(KeyWrapOutcome::InvalidRecord);
517    }
518
519    let enc_start = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN;
520    let ciphertext_start = enc_start + enc_length;
521    let enc = profile_payload_bytes[enc_start..ciphertext_start].to_vec();
522    let ciphertext =
523        profile_payload_bytes[ciphertext_start..ciphertext_start + ciphertext_length].to_vec();
524
525    Ok(ParsedProfilePayload {
526        suite,
527        enc,
528        ciphertext,
529        key_wrap_context_digest,
530    })
531}
532
533fn validate_recipient_public_key_matches_suite(
534    recipient_certificate_der: &[u8],
535    suite: HpkeSuite,
536) -> Result<(), KeyWrapOutcome> {
537    match suite.kem_id {
538        X25519_KEM_ID => x25519_public_key_from_certificate(recipient_certificate_der)
539            .map(|_| ())
540            .map_err(|_| KeyWrapOutcome::UnsupportedSuite),
541        P256_KEM_ID => p256_public_key_from_certificate(recipient_certificate_der)
542            .map(|_| ())
543            .map_err(|_| KeyWrapOutcome::UnsupportedSuite),
544        _ => Err(KeyWrapOutcome::UnsupportedSuite),
545    }
546}
547
548fn compute_key_wrap_context_digest(
549    archive_identity: &ArchiveIdentity,
550    metadata: &RecipientRecordMetadata,
551    identity: &ParsedRecipientIdentity,
552    suite: HpkeSuite,
553) -> [u8; 32] {
554    let mut hasher = Sha256::new();
555    hasher.update(KEY_WRAP_CONTEXT_DOMAIN);
556    hasher.update(archive_identity.archive_uuid);
557    hasher.update(archive_identity.session_id);
558    hasher.update(archive_identity.format_version.to_le_bytes());
559    hasher.update(archive_identity.volume_format_rev.to_le_bytes());
560    hasher.update(metadata.profile_id.to_le_bytes());
561    hasher.update(metadata.recipient_identity_type.to_le_bytes());
562    hasher.update(identity.recipient_identity_digest);
563    hasher.update(identity.recipient_spki_digest);
564    hasher.update(suite.kem_id.to_le_bytes());
565    hasher.update(suite.kdf_id.to_le_bytes());
566    hasher.update(suite.aead_id.to_le_bytes());
567
568    let digest = hasher.finalize();
569    let mut out = [0u8; 32];
570    out.copy_from_slice(&digest);
571    out
572}
573
574fn hpke_info(key_wrap_context_digest: &[u8; 32]) -> Vec<u8> {
575    let mut info = Vec::with_capacity(HPKE_INFO_DOMAIN.len() + key_wrap_context_digest.len());
576    info.extend_from_slice(HPKE_INFO_DOMAIN);
577    info.extend_from_slice(key_wrap_context_digest);
578    info
579}
580
581fn hpke_aad(key_wrap_context_digest: &[u8; 32]) -> Vec<u8> {
582    let mut aad = Vec::with_capacity(HPKE_AAD_DOMAIN.len() + key_wrap_context_digest.len());
583    aad.extend_from_slice(HPKE_AAD_DOMAIN);
584    aad.extend_from_slice(key_wrap_context_digest);
585    aad
586}
587
588fn x25519_public_key_from_certificate(
589    recipient_certificate_der: &[u8],
590) -> Result<<X25519HkdfSha256 as HpkeKem>::PublicKey, KeyWrapOutcome> {
591    let certificate =
592        X509::from_der(recipient_certificate_der).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
593    let public_key = certificate
594        .public_key()
595        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
596    let raw = public_key
597        .raw_public_key()
598        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
599    if raw.len() != 32 {
600        return Err(KeyWrapOutcome::InvalidRecord);
601    }
602    <X25519HkdfSha256 as HpkeKem>::PublicKey::from_bytes(&raw)
603        .map_err(|_| KeyWrapOutcome::InvalidRecord)
604}
605
606fn p256_public_key_from_certificate(
607    recipient_certificate_der: &[u8],
608) -> Result<<DhP256HkdfSha256 as HpkeKem>::PublicKey, KeyWrapOutcome> {
609    let certificate =
610        X509::from_der(recipient_certificate_der).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
611    let public_key = certificate
612        .public_key()
613        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
614    let ec_key = public_key
615        .ec_key()
616        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
617    if ec_key.group().curve_name() != Some(Nid::X9_62_PRIME256V1) {
618        return Err(KeyWrapOutcome::InvalidRecord);
619    }
620    let mut context = BigNumContext::new().map_err(|_| KeyWrapOutcome::InvalidRecord)?;
621    let encoded = ec_key
622        .public_key()
623        .to_bytes(
624            ec_key.group(),
625            PointConversionForm::UNCOMPRESSED,
626            &mut context,
627        )
628        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
629    if encoded.len() != 65 {
630        return Err(KeyWrapOutcome::InvalidRecord);
631    }
632    <DhP256HkdfSha256 as HpkeKem>::PublicKey::from_bytes(&encoded)
633        .map_err(|_| KeyWrapOutcome::InvalidRecord)
634}
635
636fn x25519_private_key_from_bytes(
637    private_key_bytes: &[u8],
638) -> Result<<X25519HkdfSha256 as HpkeKem>::PrivateKey, KeyWrapOutcome> {
639    let raw = if private_key_bytes.len() == 32 {
640        private_key_bytes.to_vec()
641    } else {
642        let private_key = PKey::private_key_from_der(private_key_bytes)
643            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
644        private_key
645            .raw_private_key()
646            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
647    };
648    if raw.len() != 32 {
649        return Err(KeyWrapOutcome::InvalidRecord);
650    }
651    <X25519HkdfSha256 as HpkeKem>::PrivateKey::from_bytes(&raw)
652        .map_err(|_| KeyWrapOutcome::InvalidRecord)
653}
654
655fn p256_private_key_from_bytes(
656    private_key_bytes: &[u8],
657) -> Result<<DhP256HkdfSha256 as HpkeKem>::PrivateKey, KeyWrapOutcome> {
658    let raw = if private_key_bytes.len() == 32 {
659        private_key_bytes.to_vec()
660    } else {
661        let private_key = PKey::private_key_from_der(private_key_bytes)
662            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
663        let ec_key = private_key
664            .ec_key()
665            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
666        ec_key
667            .private_key()
668            .to_vec_padded(32)
669            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
670    };
671    if raw.len() != 32 {
672        return Err(KeyWrapOutcome::InvalidRecord);
673    }
674    <DhP256HkdfSha256 as HpkeKem>::PrivateKey::from_bytes(&raw)
675        .map_err(|_| KeyWrapOutcome::InvalidRecord)
676}
677
678fn sha256_digest(bytes: &[u8]) -> [u8; 32] {
679    let digest = Sha256::digest(bytes);
680    let mut out = [0u8; 32];
681    out.copy_from_slice(&digest);
682    out
683}
684
685fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, KeyWrapOutcome> {
686    let value = bytes
687        .get(offset..offset + 2)
688        .ok_or(KeyWrapOutcome::InvalidRecord)?;
689    Ok(u16::from_le_bytes([value[0], value[1]]))
690}
691
692fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, KeyWrapOutcome> {
693    let value = bytes
694        .get(offset..offset + 4)
695        .ok_or(KeyWrapOutcome::InvalidRecord)?;
696    Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
697}
698
699fn read_array_32(bytes: &[u8], offset: usize) -> Result<[u8; 32], KeyWrapOutcome> {
700    let value = bytes
701        .get(offset..offset + 32)
702        .ok_or(KeyWrapOutcome::InvalidRecord)?;
703    let mut out = [0u8; 32];
704    out.copy_from_slice(value);
705    Ok(out)
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    use openssl::{
713        asn1::{Asn1Object, Asn1OctetString, Asn1Time},
714        bn::{BigNum, MsbOption},
715        ec::{EcGroup, EcKey},
716        hash::MessageDigest,
717        nid::Nid,
718        pkey::{PKey, Private},
719        rand::rand_bytes,
720        rsa::Rsa,
721        x509::{
722            extension::BasicConstraints, extension::KeyUsage, X509Extension, X509NameBuilder, X509,
723        },
724    };
725
726    #[derive(Debug)]
727    struct NoMatchLookup;
728
729    impl PrivateKeyLookup for NoMatchLookup {
730        fn lookup_private_key(
731            &self,
732            _: &ArchiveIdentity,
733            _: &RecipientRecordMetadata,
734            _: &[u8],
735        ) -> Option<Vec<u8>> {
736            None
737        }
738    }
739
740    #[derive(Debug)]
741    struct StaticLookup {
742        private_key: Vec<u8>,
743    }
744
745    impl PrivateKeyLookup for StaticLookup {
746        fn lookup_private_key(
747            &self,
748            _: &ArchiveIdentity,
749            _: &RecipientRecordMetadata,
750            _: &[u8],
751        ) -> Option<Vec<u8>> {
752            Some(self.private_key.clone())
753        }
754    }
755
756    #[derive(Debug)]
757    struct RejectLookup;
758
759    impl PrivateKeyLookup for RejectLookup {
760        fn lookup_private_key(
761            &self,
762            _: &ArchiveIdentity,
763            _: &RecipientRecordMetadata,
764            _: &[u8],
765        ) -> Option<Vec<u8>> {
766            Some(vec![0xAA; 32])
767        }
768
769        fn is_recipient_certificate_accepted(
770            &self,
771            _: &ArchiveIdentity,
772            _: &RecipientRecordMetadata,
773            _: &[u8],
774            _: &[u8; 32],
775        ) -> bool {
776            false
777        }
778    }
779
780    #[derive(Debug)]
781    struct MatchingLookup {
782        matches: Vec<(Vec<u8>, Vec<u8>)>,
783    }
784
785    impl PrivateKeyLookup for MatchingLookup {
786        fn lookup_private_key(
787            &self,
788            _: &ArchiveIdentity,
789            _: &RecipientRecordMetadata,
790            recipient_identity_bytes: &[u8],
791        ) -> Option<Vec<u8>> {
792            self.matches
793                .iter()
794                .find(|(identity, _)| identity.as_slice() == recipient_identity_bytes)
795                .map(|(_, private_key)| private_key.clone())
796        }
797    }
798
799    fn archive_identity() -> ArchiveIdentity {
800        let mut archive_uuid = [0u8; 16];
801        let mut session_id = [0u8; 16];
802        rand_bytes(&mut archive_uuid).unwrap();
803        rand_bytes(&mut session_id).unwrap();
804
805        ArchiveIdentity {
806            archive_uuid,
807            session_id,
808            format_version: FORMAT_VERSION,
809            volume_format_rev: VOLUME_FORMAT_REV_44,
810        }
811    }
812
813    fn test_certificate_der() -> Vec<u8> {
814        make_self_signed_certificate(None)
815    }
816
817    fn test_certificate_der_with_bad_key_usage() -> Vec<u8> {
818        let key_usage = KeyUsage::new()
819            .critical()
820            .digital_signature()
821            .build()
822            .unwrap();
823        make_self_signed_certificate(Some(key_usage)).to_vec()
824    }
825
826    fn test_certificate_der_with_malformed_key_usage() -> Vec<u8> {
827        let recipient_key = PKey::generate_x25519().unwrap();
828        let key_usage = X509Extension::new_from_der(
829            &Asn1Object::from_str("2.5.29.15").unwrap(),
830            true,
831            &Asn1OctetString::new_from_bytes(b"\x05\x00").unwrap(),
832        )
833        .unwrap();
834        make_certificate_for_subject(&recipient_key, Some(key_usage))
835    }
836
837    fn x25519_recipient_material() -> (Vec<u8>, Vec<u8>) {
838        let recipient_key = PKey::generate_x25519().unwrap();
839        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
840        (
841            make_certificate_for_subject(&recipient_key, Some(key_usage)),
842            recipient_key.raw_private_key().unwrap(),
843        )
844    }
845
846    fn p256_recipient_material() -> (Vec<u8>, Vec<u8>) {
847        let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
848        let recipient_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
849        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
850        (
851            make_certificate_for_subject(&recipient_key, Some(key_usage)),
852            recipient_key.private_key_to_der().unwrap(),
853        )
854    }
855
856    fn p384_recipient_certificate() -> Vec<u8> {
857        let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
858        let recipient_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
859        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
860        make_certificate_for_subject(&recipient_key, Some(key_usage))
861    }
862
863    fn make_self_signed_certificate(
864        key_usage_ext: Option<openssl::x509::X509Extension>,
865    ) -> Vec<u8> {
866        let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
867        let mut name = X509NameBuilder::new().unwrap();
868        name.append_entry_by_text("CN", "tzap-keywrap-test")
869            .unwrap();
870        let name = name.build();
871
872        let mut certificate = X509::builder().unwrap();
873        certificate.set_version(2).unwrap();
874        certificate
875            .set_serial_number(&random_serial_number())
876            .unwrap();
877        certificate.set_subject_name(&name).unwrap();
878        certificate.set_issuer_name(&name).unwrap();
879        certificate.set_pubkey(&key).unwrap();
880        certificate
881            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
882            .unwrap();
883        certificate
884            .set_not_after(&Asn1Time::days_from_now(365).unwrap())
885            .unwrap();
886        certificate
887            .append_extension(BasicConstraints::new().build().unwrap())
888            .unwrap();
889        if let Some(key_usage_ext) = key_usage_ext {
890            certificate.append_extension(key_usage_ext).unwrap();
891        }
892        certificate.sign(&key, MessageDigest::sha256()).unwrap();
893
894        certificate.build().to_der().unwrap()
895    }
896
897    fn make_certificate_for_subject(
898        subject_key: &PKey<Private>,
899        key_usage_ext: Option<openssl::x509::X509Extension>,
900    ) -> Vec<u8> {
901        let signer_key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
902        let mut name = X509NameBuilder::new().unwrap();
903        name.append_entry_by_text("CN", "tzap-keywrap-recipient")
904            .unwrap();
905        let name = name.build();
906
907        let mut certificate = X509::builder().unwrap();
908        certificate.set_version(2).unwrap();
909        certificate
910            .set_serial_number(&random_serial_number())
911            .unwrap();
912        certificate.set_subject_name(&name).unwrap();
913        certificate.set_issuer_name(&name).unwrap();
914        certificate.set_pubkey(subject_key).unwrap();
915        certificate
916            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
917            .unwrap();
918        certificate
919            .set_not_after(&Asn1Time::days_from_now(365).unwrap())
920            .unwrap();
921        certificate
922            .append_extension(BasicConstraints::new().build().unwrap())
923            .unwrap();
924        if let Some(key_usage_ext) = key_usage_ext {
925            certificate.append_extension(key_usage_ext).unwrap();
926        }
927        certificate
928            .sign(&signer_key, MessageDigest::sha256())
929            .unwrap();
930
931        certificate.build().to_der().unwrap()
932    }
933
934    fn random_serial_number() -> openssl::asn1::Asn1Integer {
935        let mut serial = BigNum::new().unwrap();
936        serial.rand(159, MsbOption::MAYBE_ZERO, false).unwrap();
937        serial.to_asn1_integer().unwrap()
938    }
939
940    fn make_payload(
941        archive_identity: &ArchiveIdentity,
942        metadata: &RecipientRecordMetadata,
943        identity: &ParsedRecipientIdentity,
944        suite: HpkeSuite,
945    ) -> Vec<u8> {
946        make_payload_with_lengths(
947            archive_identity,
948            metadata,
949            identity,
950            suite,
951            suite.enc_length,
952            suite.ciphertext_length,
953        )
954    }
955
956    fn make_payload_with_lengths(
957        archive_identity: &ArchiveIdentity,
958        metadata: &RecipientRecordMetadata,
959        identity: &ParsedRecipientIdentity,
960        suite: HpkeSuite,
961        enc_len: usize,
962        ciphertext_len: usize,
963    ) -> Vec<u8> {
964        let mut payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc_len + ciphertext_len];
965        payload[0..2].copy_from_slice(&KEYWRAP_PAYLOAD_VERSION.to_le_bytes());
966        payload[2..4].copy_from_slice(&suite.kem_id.to_le_bytes());
967        payload[4..6].copy_from_slice(&suite.kdf_id.to_le_bytes());
968        payload[6..8].copy_from_slice(&suite.aead_id.to_le_bytes());
969        payload[8..10].copy_from_slice(&u16::try_from(enc_len).unwrap().to_le_bytes());
970        payload[10..12].copy_from_slice(&u16::try_from(ciphertext_len).unwrap().to_le_bytes());
971        payload[16..48].copy_from_slice(&compute_key_wrap_context_digest(
972            archive_identity,
973            metadata,
974            identity,
975            suite,
976        ));
977
978        let enc_start = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN;
979        let enc_end = enc_start + enc_len;
980        let ct_end = enc_end + ciphertext_len;
981        for value in payload[enc_start..enc_end].iter_mut() {
982            *value = 0xAA;
983        }
984        for value in payload[enc_end..ct_end].iter_mut() {
985            *value = 0x55;
986        }
987
988        payload
989    }
990
991    fn recipient_record_input_with_payload(
992        profile_payload: Vec<u8>,
993        cert_der: &[u8],
994    ) -> RecipientRecordInput {
995        let identity_digest = sha256_digest(cert_der);
996        RecipientRecordInput {
997            archive_identity: archive_identity(),
998            metadata: RecipientRecordMetadata {
999                profile_id: KEYWRAP_PROFILE_ID,
1000                recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1001                recipient_identity_digest: identity_digest,
1002            },
1003            recipient_identity_bytes: cert_der.to_vec(),
1004            profile_payload_bytes: profile_payload,
1005        }
1006    }
1007
1008    fn recipient_record_input_for_suite(cert_der: &[u8], suite: HpkeSuite) -> RecipientRecordInput {
1009        let identity = parse_x509_recipient_identity(cert_der).unwrap();
1010        let archive_identity = archive_identity();
1011        let metadata = RecipientRecordMetadata {
1012            profile_id: KEYWRAP_PROFILE_ID,
1013            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1014            recipient_identity_digest: identity.recipient_identity_digest,
1015        };
1016        let payload = make_payload(&archive_identity, &metadata, &identity, suite);
1017        RecipientRecordInput {
1018            archive_identity,
1019            metadata,
1020            recipient_identity_bytes: cert_der.to_vec(),
1021            profile_payload_bytes: payload,
1022        }
1023    }
1024
1025    fn recipient_record_input_from_record(
1026        archive_identity: ArchiveIdentity,
1027        record: RecipientRecordV1,
1028    ) -> RecipientRecordInput {
1029        RecipientRecordInput {
1030            archive_identity,
1031            metadata: RecipientRecordMetadata {
1032                profile_id: record.profile_id,
1033                recipient_identity_type: record.recipient_identity_type,
1034                recipient_identity_digest: record.recipient_identity_digest,
1035            },
1036            recipient_identity_bytes: record.recipient_identity_bytes,
1037            profile_payload_bytes: record.profile_payload_bytes,
1038        }
1039    }
1040
1041    #[test]
1042    fn malformed_recipient_identity_is_invalid() {
1043        let cert_der = b"not-a-certificate".to_vec();
1044        let profile_payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1045
1046        let input = recipient_record_input_with_payload(profile_payload, &cert_der);
1047        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1048
1049        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1050    }
1051
1052    #[test]
1053    fn unsupported_identity_type_is_returned() {
1054        let cert_der = test_certificate_der();
1055        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1056
1057        let archive_identity = archive_identity();
1058        let suite =
1059            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1060                .unwrap();
1061        let payload = make_payload(
1062            &archive_identity,
1063            &RecipientRecordMetadata {
1064                profile_id: KEYWRAP_PROFILE_ID,
1065                recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1066                recipient_identity_digest: identity.recipient_identity_digest,
1067            },
1068            &identity,
1069            suite,
1070        );
1071        let mut input = recipient_record_input_with_payload(payload, &cert_der);
1072        input.metadata.recipient_identity_type = 0;
1073
1074        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1075
1076        assert!(matches!(
1077            result,
1078            KeyWrapOutcome::UnsupportedRecipientIdentity
1079        ));
1080    }
1081
1082    #[test]
1083    fn unsupported_suite_is_invalid() {
1084        let cert_der = test_certificate_der();
1085        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1086        let archive_identity = archive_identity();
1087        let metadata = RecipientRecordMetadata {
1088            profile_id: KEYWRAP_PROFILE_ID,
1089            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1090            recipient_identity_digest: identity.recipient_identity_digest,
1091        };
1092
1093        let unsupported_suite = HpkeSuite {
1094            kem_id: 0x00ff,
1095            kdf_id: 0x00ff,
1096            aead_id: 0x00ff,
1097            enc_length: 0,
1098            ciphertext_length: 0,
1099        };
1100
1101        let payload = make_payload_with_lengths(
1102            &archive_identity,
1103            &metadata,
1104            &identity,
1105            unsupported_suite,
1106            0,
1107            0,
1108        );
1109
1110        let input = recipient_record_input_with_payload(payload, &cert_der);
1111        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1112
1113        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1114    }
1115
1116    #[test]
1117    fn wrong_lengths_rejected_as_invalid_record() {
1118        let cert_der = test_certificate_der();
1119        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1120        let archive_identity = archive_identity();
1121        let metadata = RecipientRecordMetadata {
1122            profile_id: KEYWRAP_PROFILE_ID,
1123            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1124            recipient_identity_digest: identity.recipient_identity_digest,
1125        };
1126        let suite =
1127            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1128                .unwrap();
1129
1130        let payload = make_payload_with_lengths(
1131            &archive_identity,
1132            &metadata,
1133            &identity,
1134            suite,
1135            suite.enc_length - 1,
1136            suite.ciphertext_length,
1137        );
1138        let input = recipient_record_input_with_payload(payload, &cert_der);
1139
1140        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1141
1142        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1143    }
1144
1145    #[test]
1146    fn wrong_context_digest_is_invalid_record() {
1147        let (cert_der, private_key) = x25519_recipient_material();
1148        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1149        let archive_identity = archive_identity();
1150        let metadata = RecipientRecordMetadata {
1151            profile_id: KEYWRAP_PROFILE_ID,
1152            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1153            recipient_identity_digest: identity.recipient_identity_digest,
1154        };
1155        let suite =
1156            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1157                .unwrap();
1158        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1159        payload[16] ^= 0xFF;
1160
1161        let input = recipient_record_input_with_payload(payload, &cert_der);
1162        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1163
1164        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1165    }
1166
1167    #[test]
1168    fn no_matching_private_key_precedes_context_digest_check() {
1169        let (cert_der, _) = x25519_recipient_material();
1170        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1171        let archive_identity = archive_identity();
1172        let metadata = RecipientRecordMetadata {
1173            profile_id: KEYWRAP_PROFILE_ID,
1174            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1175            recipient_identity_digest: identity.recipient_identity_digest,
1176        };
1177        let suite =
1178            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1179                .unwrap();
1180        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1181        payload[16] ^= 0xFF;
1182
1183        let input = recipient_record_input_with_payload(payload, &cert_der);
1184        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1185
1186        assert!(matches!(result, KeyWrapOutcome::NoMatchingPrivateKey));
1187    }
1188
1189    #[test]
1190    fn certificate_policy_rejection_precedes_context_digest_check() {
1191        let (cert_der, _) = x25519_recipient_material();
1192        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1193        let archive_identity = archive_identity();
1194        let metadata = RecipientRecordMetadata {
1195            profile_id: KEYWRAP_PROFILE_ID,
1196            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1197            recipient_identity_digest: identity.recipient_identity_digest,
1198        };
1199        let suite =
1200            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1201                .unwrap();
1202        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1203        payload[16] ^= 0xFF;
1204
1205        let input = recipient_record_input_with_payload(payload, &cert_der);
1206        let result = dispatch_key_wrap_record(input, &RejectLookup);
1207
1208        assert!(matches!(result, KeyWrapOutcome::CertificatePolicyRejected));
1209    }
1210
1211    #[test]
1212    fn valid_structure_with_no_matching_private_key_is_no_matching() {
1213        let archive_identity = archive_identity();
1214        let (cert_der, _) = x25519_recipient_material();
1215        let master_key = [0x42u8; 32];
1216        let record = wrap_master_key_for_recipient(
1217            archive_identity.clone(),
1218            &cert_der,
1219            &master_key,
1220            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1221        )
1222        .unwrap();
1223        let input = recipient_record_input_from_record(archive_identity, record);
1224
1225        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1226
1227        assert!(matches!(result, KeyWrapOutcome::NoMatchingPrivateKey));
1228    }
1229
1230    #[test]
1231    fn key_usage_without_key_agreement_is_invalid_record() {
1232        let cert_der = test_certificate_der_with_bad_key_usage();
1233        let payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1234        let input = recipient_record_input_with_payload(payload, &cert_der);
1235
1236        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1237
1238        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1239    }
1240
1241    #[test]
1242    fn malformed_key_usage_extension_is_invalid_record() {
1243        let cert_der = test_certificate_der_with_malformed_key_usage();
1244        let payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1245        let input = recipient_record_input_with_payload(payload, &cert_der);
1246
1247        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1248
1249        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1250    }
1251
1252    #[test]
1253    fn certificate_policy_rejection_is_reported() {
1254        let archive_identity = archive_identity();
1255        let (cert_der, _) = x25519_recipient_material();
1256        let master_key = [0x42u8; 32];
1257        let record = wrap_master_key_for_recipient(
1258            archive_identity.clone(),
1259            &cert_der,
1260            &master_key,
1261            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1262        )
1263        .unwrap();
1264        let input = recipient_record_input_from_record(archive_identity, record);
1265
1266        let result = dispatch_key_wrap_record(input, &RejectLookup);
1267
1268        assert!(matches!(result, KeyWrapOutcome::CertificatePolicyRejected));
1269    }
1270
1271    #[test]
1272    fn suite_certificate_mismatch_is_unsupported_before_private_key_lookup() {
1273        let (p256_cert_der, _) = p256_recipient_material();
1274        let x25519_suite =
1275            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1276                .unwrap();
1277        let result = dispatch_key_wrap_record(
1278            recipient_record_input_for_suite(&p256_cert_der, x25519_suite),
1279            &NoMatchLookup,
1280        );
1281        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1282
1283        let (x25519_cert_der, _) = x25519_recipient_material();
1284        let p256_suite =
1285            HpkeSuite::for_profile(P256_KEM_ID, HKDF_SHA256_KDF_ID, AES256GCM_AEAD_ID).unwrap();
1286        let result = dispatch_key_wrap_record(
1287            recipient_record_input_for_suite(&x25519_cert_der, p256_suite),
1288            &NoMatchLookup,
1289        );
1290        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1291
1292        let p384_cert_der = p384_recipient_certificate();
1293        let result = dispatch_key_wrap_record(
1294            recipient_record_input_for_suite(&p384_cert_der, p256_suite),
1295            &NoMatchLookup,
1296        );
1297        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1298    }
1299
1300    #[test]
1301    fn writer_reports_unsupported_suite_for_certificate_mismatch() {
1302        let master_key = [0x42u8; 32];
1303        let (p256_cert_der, _) = p256_recipient_material();
1304        let result = wrap_master_key_for_recipient(
1305            archive_identity(),
1306            &p256_cert_der,
1307            &master_key,
1308            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1309        );
1310        assert!(matches!(result, Err(KeyWrapOutcome::UnsupportedSuite)));
1311
1312        let (x25519_cert_der, _) = x25519_recipient_material();
1313        let result = wrap_master_key_for_recipient(
1314            archive_identity(),
1315            &x25519_cert_der,
1316            &master_key,
1317            KeyWrapSuite::P256HkdfSha256Aes256Gcm,
1318        );
1319        assert!(matches!(result, Err(KeyWrapOutcome::UnsupportedSuite)));
1320    }
1321
1322    #[test]
1323    fn x25519_wrap_unwrap_returns_candidate_master_key() {
1324        let (cert_der, private_key) = x25519_recipient_material();
1325        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1326        let archive_identity = archive_identity();
1327        let master_key = [0x42u8; 32];
1328        let record = wrap_master_key_for_recipient(
1329            archive_identity.clone(),
1330            &cert_der,
1331            &master_key,
1332            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1333        )
1334        .unwrap();
1335        let input = recipient_record_input_from_record(archive_identity, record);
1336
1337        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1338
1339        assert!(matches!(
1340            result,
1341            KeyWrapOutcome::UnwrappedCandidateMasterKey {
1342                master_key: actual_master_key,
1343                recipient_spki_digest: actual_digest
1344            } if actual_master_key == master_key && actual_digest == identity.recipient_spki_digest
1345        ));
1346    }
1347
1348    #[test]
1349    fn p256_wrap_unwrap_returns_candidate_master_key() {
1350        let (cert_der, private_key) = p256_recipient_material();
1351        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1352        let archive_identity = archive_identity();
1353        let master_key = [0x24u8; 32];
1354        let record = wrap_master_key_for_recipient(
1355            archive_identity.clone(),
1356            &cert_der,
1357            &master_key,
1358            KeyWrapSuite::P256HkdfSha256Aes256Gcm,
1359        )
1360        .unwrap();
1361        let input = recipient_record_input_from_record(archive_identity, record);
1362
1363        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1364
1365        assert!(matches!(
1366            result,
1367            KeyWrapOutcome::UnwrappedCandidateMasterKey {
1368                master_key: actual_master_key,
1369                recipient_spki_digest: actual_digest
1370            } if actual_master_key == master_key && actual_digest == identity.recipient_spki_digest
1371        ));
1372    }
1373
1374    #[test]
1375    fn multi_recipient_records_open_independently_with_matching_private_key() {
1376        let archive_identity = archive_identity();
1377        let master_key = [0x33u8; 32];
1378        let (bob_cert, bob_key) = x25519_recipient_material();
1379        let (carol_cert, carol_key) = p256_recipient_material();
1380        let (ops_cert, ops_key) = x25519_recipient_material();
1381        let recipients = [
1382            (
1383                bob_cert,
1384                bob_key,
1385                KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1386            ),
1387            (carol_cert, carol_key, KeyWrapSuite::P256HkdfSha256Aes256Gcm),
1388            (
1389                ops_cert,
1390                ops_key,
1391                KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1392            ),
1393        ];
1394        let records = recipients
1395            .iter()
1396            .map(|(cert_der, _, suite)| {
1397                wrap_master_key_for_recipient(
1398                    archive_identity.clone(),
1399                    cert_der,
1400                    &master_key,
1401                    *suite,
1402                )
1403                .unwrap()
1404            })
1405            .collect::<Vec<_>>();
1406
1407        for (cert_der, private_key, _) in recipients {
1408            let lookup = MatchingLookup {
1409                matches: vec![(cert_der.clone(), private_key)],
1410            };
1411            let unwrapped = records
1412                .clone()
1413                .into_iter()
1414                .filter_map(|record| {
1415                    let input =
1416                        recipient_record_input_from_record(archive_identity.clone(), record);
1417                    match dispatch_key_wrap_record(input, &lookup) {
1418                        KeyWrapOutcome::UnwrappedCandidateMasterKey {
1419                            master_key: candidate,
1420                            ..
1421                        } => Some(candidate),
1422                        KeyWrapOutcome::NoMatchingPrivateKey => None,
1423                        other => panic!("unexpected key-wrap outcome: {other:?}"),
1424                    }
1425                })
1426                .collect::<Vec<_>>();
1427
1428            assert_eq!(unwrapped, vec![master_key]);
1429        }
1430    }
1431
1432    #[test]
1433    fn wrong_private_key_is_invalid_record() {
1434        let (cert_der, _private_key) = x25519_recipient_material();
1435        let (_wrong_cert_der, wrong_private_key) = x25519_recipient_material();
1436        let archive_identity = archive_identity();
1437        let master_key = [0x5au8; 32];
1438        let record = wrap_master_key_for_recipient(
1439            archive_identity.clone(),
1440            &cert_der,
1441            &master_key,
1442            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1443        )
1444        .unwrap();
1445        let input = recipient_record_input_from_record(archive_identity, record);
1446
1447        let result = dispatch_key_wrap_record(
1448            input,
1449            &StaticLookup {
1450                private_key: wrong_private_key,
1451            },
1452        );
1453
1454        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1455    }
1456
1457    #[test]
1458    fn signing_certificate_key_usage_does_not_satisfy_recipient_wrap() {
1459        let cert_der = test_certificate_der_with_bad_key_usage();
1460        let master_key = [0x42u8; 32];
1461
1462        let result = wrap_master_key_for_recipient(
1463            archive_identity(),
1464            &cert_der,
1465            &master_key,
1466            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1467        );
1468
1469        assert!(matches!(result, Err(KeyWrapOutcome::InvalidRecord)));
1470    }
1471}