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_45};
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-v45\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_45,
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_45
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_45
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    hpke_seal_master_key_with_aad_domain(
316        recipient_certificate_der,
317        archive_identity,
318        metadata,
319        identity,
320        master_key,
321        suite,
322        HPKE_AAD_DOMAIN,
323    )
324}
325
326fn hpke_seal_master_key_with_aad_domain(
327    recipient_certificate_der: &[u8],
328    archive_identity: &ArchiveIdentity,
329    metadata: &RecipientRecordMetadata,
330    identity: &ParsedRecipientIdentity,
331    master_key: &[u8; 32],
332    suite: HpkeSuite,
333    aad_domain: &[u8],
334) -> Result<Vec<u8>, KeyWrapOutcome> {
335    let key_wrap_context_digest =
336        compute_key_wrap_context_digest(archive_identity, metadata, identity, suite);
337    let info = hpke_info(&key_wrap_context_digest);
338    let aad = hpke_aad_with_domain(aad_domain, &key_wrap_context_digest);
339    let (enc, ciphertext) = match (suite.kem_id, suite.aead_id) {
340        (X25519_KEM_ID, CHACHA20POLY1305_AEAD_ID) => {
341            let public_key = x25519_public_key_from_certificate(recipient_certificate_der)
342                .map_err(|_| KeyWrapOutcome::UnsupportedSuite)?;
343            let mut rng = UnwrapErr(OsRng);
344            let (enc, ciphertext) =
345                hpke::single_shot_seal::<ChaCha20Poly1305, HkdfSha256, X25519HkdfSha256, _>(
346                    &OpModeS::Base,
347                    &public_key,
348                    &info,
349                    master_key,
350                    &aad,
351                    &mut rng,
352                )
353                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
354            (enc.to_bytes().to_vec(), ciphertext)
355        }
356        (P256_KEM_ID, AES256GCM_AEAD_ID) => {
357            let public_key = p256_public_key_from_certificate(recipient_certificate_der)
358                .map_err(|_| KeyWrapOutcome::UnsupportedSuite)?;
359            let mut rng = UnwrapErr(OsRng);
360            let (enc, ciphertext) =
361                hpke::single_shot_seal::<AesGcm256, HkdfSha256, DhP256HkdfSha256, _>(
362                    &OpModeS::Base,
363                    &public_key,
364                    &info,
365                    master_key,
366                    &aad,
367                    &mut rng,
368                )
369                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
370            (enc.to_bytes().to_vec(), ciphertext)
371        }
372        _ => return Err(KeyWrapOutcome::UnsupportedSuite),
373    };
374
375    if enc.len() != suite.enc_length || ciphertext.len() != suite.ciphertext_length {
376        return Err(KeyWrapOutcome::InvalidRecord);
377    }
378
379    Ok(build_profile_payload(
380        suite,
381        &key_wrap_context_digest,
382        &enc,
383        &ciphertext,
384    ))
385}
386
387fn hpke_open_candidate_master_key(
388    payload: &ParsedProfilePayload,
389    private_key_bytes: &[u8],
390) -> Result<[u8; 32], KeyWrapOutcome> {
391    let info = hpke_info(&payload.key_wrap_context_digest);
392    let aad = hpke_aad(&payload.key_wrap_context_digest);
393    let plaintext = match (payload.suite.kem_id, payload.suite.aead_id) {
394        (X25519_KEM_ID, CHACHA20POLY1305_AEAD_ID) => {
395            let private_key = x25519_private_key_from_bytes(private_key_bytes)?;
396            let encapped_key = <X25519HkdfSha256 as HpkeKem>::EncappedKey::from_bytes(&payload.enc)
397                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
398            hpke::single_shot_open::<ChaCha20Poly1305, HkdfSha256, X25519HkdfSha256>(
399                &OpModeR::Base,
400                &private_key,
401                &encapped_key,
402                &info,
403                &payload.ciphertext,
404                &aad,
405            )
406            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
407        }
408        (P256_KEM_ID, AES256GCM_AEAD_ID) => {
409            let private_key = p256_private_key_from_bytes(private_key_bytes)?;
410            let encapped_key = <DhP256HkdfSha256 as HpkeKem>::EncappedKey::from_bytes(&payload.enc)
411                .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
412            hpke::single_shot_open::<AesGcm256, HkdfSha256, DhP256HkdfSha256>(
413                &OpModeR::Base,
414                &private_key,
415                &encapped_key,
416                &info,
417                &payload.ciphertext,
418                &aad,
419            )
420            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
421        }
422        _ => return Err(KeyWrapOutcome::UnsupportedSuite),
423    };
424
425    if plaintext.len() != 32 {
426        return Err(KeyWrapOutcome::InvalidRecord);
427    }
428    let mut master_key = [0u8; 32];
429    master_key.copy_from_slice(&plaintext);
430    Ok(master_key)
431}
432
433fn build_profile_payload(
434    suite: HpkeSuite,
435    key_wrap_context_digest: &[u8; 32],
436    enc: &[u8],
437    ciphertext: &[u8],
438) -> Vec<u8> {
439    let mut payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len() + ciphertext.len()];
440    payload[0..2].copy_from_slice(&KEYWRAP_PAYLOAD_VERSION.to_le_bytes());
441    payload[2..4].copy_from_slice(&suite.kem_id.to_le_bytes());
442    payload[4..6].copy_from_slice(&suite.kdf_id.to_le_bytes());
443    payload[6..8].copy_from_slice(&suite.aead_id.to_le_bytes());
444    payload[8..10].copy_from_slice(&(enc.len() as u16).to_le_bytes());
445    payload[10..12].copy_from_slice(&(ciphertext.len() as u16).to_le_bytes());
446    payload[16..48].copy_from_slice(key_wrap_context_digest);
447    payload[KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN..KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len()]
448        .copy_from_slice(enc);
449    payload[KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc.len()..].copy_from_slice(ciphertext);
450    payload
451}
452
453fn parse_x509_recipient_identity(
454    recipient_identity_bytes: &[u8],
455) -> Result<ParsedRecipientIdentity, KeyWrapOutcome> {
456    let (remaining, parsed_cert) = parse_x509_certificate(recipient_identity_bytes)
457        .map_err(|_error| KeyWrapOutcome::InvalidRecord)?;
458
459    if !remaining.is_empty() {
460        return Err(KeyWrapOutcome::InvalidRecord);
461    }
462
463    match parsed_cert.key_usage() {
464        Ok(Some(key_usage)) => {
465            if !key_usage.value.key_agreement() {
466                return Err(KeyWrapOutcome::InvalidRecord);
467            }
468        }
469        Ok(None) => {}
470        Err(_) => {
471            return Err(KeyWrapOutcome::InvalidRecord);
472        }
473    }
474
475    let openssl_cert =
476        X509::from_der(recipient_identity_bytes).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
477    let openssl_public_key: PKey<_> = openssl_cert
478        .public_key()
479        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
480    let spki_der = openssl_public_key
481        .public_key_to_der()
482        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
483    let recipient_identity_digest = sha256_digest(recipient_identity_bytes);
484    let recipient_spki_digest = sha256_digest(&spki_der);
485
486    Ok(ParsedRecipientIdentity {
487        recipient_identity_digest,
488        recipient_spki_digest,
489    })
490}
491
492fn parse_profile_payload(
493    profile_payload_bytes: &[u8],
494) -> Result<ParsedProfilePayload, KeyWrapOutcome> {
495    if profile_payload_bytes.len() < KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN {
496        return Err(KeyWrapOutcome::InvalidRecord);
497    }
498
499    let payload_version = read_u16(profile_payload_bytes, 0)?;
500    let hpke_kem_id = read_u16(profile_payload_bytes, 2)?;
501    let hpke_kdf_id = read_u16(profile_payload_bytes, 4)?;
502    let hpke_aead_id = read_u16(profile_payload_bytes, 6)?;
503    let enc_length = usize::from(read_u16(profile_payload_bytes, 8)?);
504    let ciphertext_length = usize::from(read_u16(profile_payload_bytes, 10)?);
505    let flags = read_u32(profile_payload_bytes, 12)?;
506    let key_wrap_context_digest = read_array_32(profile_payload_bytes, 16)?;
507
508    if flags != 0 {
509        return Err(KeyWrapOutcome::InvalidRecord);
510    }
511
512    if !profile_payload_bytes[48..KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN]
513        .iter()
514        .all(|value| *value == 0)
515    {
516        return Err(KeyWrapOutcome::InvalidRecord);
517    }
518
519    if payload_version != KEYWRAP_PAYLOAD_VERSION {
520        return Err(KeyWrapOutcome::InvalidRecord);
521    }
522
523    let suite = HpkeSuite::for_profile(hpke_kem_id, hpke_kdf_id, hpke_aead_id)
524        .ok_or(KeyWrapOutcome::UnsupportedSuite)?;
525
526    if enc_length != suite.enc_length || ciphertext_length != suite.ciphertext_length {
527        return Err(KeyWrapOutcome::InvalidRecord);
528    }
529
530    let expected_length = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN
531        .checked_add(enc_length)
532        .and_then(|value| value.checked_add(ciphertext_length))
533        .ok_or(KeyWrapOutcome::InvalidRecord)?;
534
535    if profile_payload_bytes.len() != expected_length {
536        return Err(KeyWrapOutcome::InvalidRecord);
537    }
538
539    let enc_start = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN;
540    let ciphertext_start = enc_start + enc_length;
541    let enc = profile_payload_bytes[enc_start..ciphertext_start].to_vec();
542    let ciphertext =
543        profile_payload_bytes[ciphertext_start..ciphertext_start + ciphertext_length].to_vec();
544
545    Ok(ParsedProfilePayload {
546        suite,
547        enc,
548        ciphertext,
549        key_wrap_context_digest,
550    })
551}
552
553fn validate_recipient_public_key_matches_suite(
554    recipient_certificate_der: &[u8],
555    suite: HpkeSuite,
556) -> Result<(), KeyWrapOutcome> {
557    match suite.kem_id {
558        X25519_KEM_ID => x25519_public_key_from_certificate(recipient_certificate_der)
559            .map(|_| ())
560            .map_err(|_| KeyWrapOutcome::UnsupportedSuite),
561        P256_KEM_ID => p256_public_key_from_certificate(recipient_certificate_der)
562            .map(|_| ())
563            .map_err(|_| KeyWrapOutcome::UnsupportedSuite),
564        _ => Err(KeyWrapOutcome::UnsupportedSuite),
565    }
566}
567
568fn compute_key_wrap_context_digest(
569    archive_identity: &ArchiveIdentity,
570    metadata: &RecipientRecordMetadata,
571    identity: &ParsedRecipientIdentity,
572    suite: HpkeSuite,
573) -> [u8; 32] {
574    let mut hasher = Sha256::new();
575    hasher.update(KEY_WRAP_CONTEXT_DOMAIN);
576    hasher.update(archive_identity.archive_uuid);
577    hasher.update(archive_identity.session_id);
578    hasher.update(archive_identity.format_version.to_le_bytes());
579    hasher.update(archive_identity.volume_format_rev.to_le_bytes());
580    hasher.update(metadata.profile_id.to_le_bytes());
581    hasher.update(metadata.recipient_identity_type.to_le_bytes());
582    hasher.update(identity.recipient_identity_digest);
583    hasher.update(identity.recipient_spki_digest);
584    hasher.update(suite.kem_id.to_le_bytes());
585    hasher.update(suite.kdf_id.to_le_bytes());
586    hasher.update(suite.aead_id.to_le_bytes());
587
588    let digest = hasher.finalize();
589    let mut out = [0u8; 32];
590    out.copy_from_slice(&digest);
591    out
592}
593
594fn hpke_info(key_wrap_context_digest: &[u8; 32]) -> Vec<u8> {
595    let mut info = Vec::with_capacity(HPKE_INFO_DOMAIN.len() + key_wrap_context_digest.len());
596    info.extend_from_slice(HPKE_INFO_DOMAIN);
597    info.extend_from_slice(key_wrap_context_digest);
598    info
599}
600
601fn hpke_aad(key_wrap_context_digest: &[u8; 32]) -> Vec<u8> {
602    hpke_aad_with_domain(HPKE_AAD_DOMAIN, key_wrap_context_digest)
603}
604
605fn hpke_aad_with_domain(domain: &[u8], key_wrap_context_digest: &[u8; 32]) -> Vec<u8> {
606    let mut aad = Vec::with_capacity(domain.len() + key_wrap_context_digest.len());
607    aad.extend_from_slice(domain);
608    aad.extend_from_slice(key_wrap_context_digest);
609    aad
610}
611
612fn x25519_public_key_from_certificate(
613    recipient_certificate_der: &[u8],
614) -> Result<<X25519HkdfSha256 as HpkeKem>::PublicKey, KeyWrapOutcome> {
615    let certificate =
616        X509::from_der(recipient_certificate_der).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
617    let public_key = certificate
618        .public_key()
619        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
620    let raw = public_key
621        .raw_public_key()
622        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
623    if raw.len() != 32 {
624        return Err(KeyWrapOutcome::InvalidRecord);
625    }
626    <X25519HkdfSha256 as HpkeKem>::PublicKey::from_bytes(&raw)
627        .map_err(|_| KeyWrapOutcome::InvalidRecord)
628}
629
630fn p256_public_key_from_certificate(
631    recipient_certificate_der: &[u8],
632) -> Result<<DhP256HkdfSha256 as HpkeKem>::PublicKey, KeyWrapOutcome> {
633    let certificate =
634        X509::from_der(recipient_certificate_der).map_err(|_| KeyWrapOutcome::InvalidRecord)?;
635    let public_key = certificate
636        .public_key()
637        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
638    let ec_key = public_key
639        .ec_key()
640        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
641    if ec_key.group().curve_name() != Some(Nid::X9_62_PRIME256V1) {
642        return Err(KeyWrapOutcome::InvalidRecord);
643    }
644    let mut context = BigNumContext::new().map_err(|_| KeyWrapOutcome::InvalidRecord)?;
645    let encoded = ec_key
646        .public_key()
647        .to_bytes(
648            ec_key.group(),
649            PointConversionForm::UNCOMPRESSED,
650            &mut context,
651        )
652        .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
653    if encoded.len() != 65 {
654        return Err(KeyWrapOutcome::InvalidRecord);
655    }
656    <DhP256HkdfSha256 as HpkeKem>::PublicKey::from_bytes(&encoded)
657        .map_err(|_| KeyWrapOutcome::InvalidRecord)
658}
659
660fn x25519_private_key_from_bytes(
661    private_key_bytes: &[u8],
662) -> Result<<X25519HkdfSha256 as HpkeKem>::PrivateKey, KeyWrapOutcome> {
663    let raw = if private_key_bytes.len() == 32 {
664        private_key_bytes.to_vec()
665    } else {
666        let private_key = PKey::private_key_from_der(private_key_bytes)
667            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
668        private_key
669            .raw_private_key()
670            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
671    };
672    if raw.len() != 32 {
673        return Err(KeyWrapOutcome::InvalidRecord);
674    }
675    <X25519HkdfSha256 as HpkeKem>::PrivateKey::from_bytes(&raw)
676        .map_err(|_| KeyWrapOutcome::InvalidRecord)
677}
678
679fn p256_private_key_from_bytes(
680    private_key_bytes: &[u8],
681) -> Result<<DhP256HkdfSha256 as HpkeKem>::PrivateKey, KeyWrapOutcome> {
682    let raw = if private_key_bytes.len() == 32 {
683        private_key_bytes.to_vec()
684    } else {
685        let private_key = PKey::private_key_from_der(private_key_bytes)
686            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
687        let ec_key = private_key
688            .ec_key()
689            .map_err(|_| KeyWrapOutcome::InvalidRecord)?;
690        ec_key
691            .private_key()
692            .to_vec_padded(32)
693            .map_err(|_| KeyWrapOutcome::InvalidRecord)?
694    };
695    if raw.len() != 32 {
696        return Err(KeyWrapOutcome::InvalidRecord);
697    }
698    <DhP256HkdfSha256 as HpkeKem>::PrivateKey::from_bytes(&raw)
699        .map_err(|_| KeyWrapOutcome::InvalidRecord)
700}
701
702fn sha256_digest(bytes: &[u8]) -> [u8; 32] {
703    let digest = Sha256::digest(bytes);
704    let mut out = [0u8; 32];
705    out.copy_from_slice(&digest);
706    out
707}
708
709fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, KeyWrapOutcome> {
710    let value = bytes
711        .get(offset..offset + 2)
712        .ok_or(KeyWrapOutcome::InvalidRecord)?;
713    Ok(u16::from_le_bytes([value[0], value[1]]))
714}
715
716fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, KeyWrapOutcome> {
717    let value = bytes
718        .get(offset..offset + 4)
719        .ok_or(KeyWrapOutcome::InvalidRecord)?;
720    Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
721}
722
723fn read_array_32(bytes: &[u8], offset: usize) -> Result<[u8; 32], KeyWrapOutcome> {
724    let value = bytes
725        .get(offset..offset + 32)
726        .ok_or(KeyWrapOutcome::InvalidRecord)?;
727    let mut out = [0u8; 32];
728    out.copy_from_slice(value);
729    Ok(out)
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    use openssl::{
737        asn1::{Asn1Object, Asn1OctetString, Asn1Time},
738        bn::{BigNum, MsbOption},
739        ec::{EcGroup, EcKey},
740        hash::MessageDigest,
741        nid::Nid,
742        pkey::{PKey, Private},
743        rand::rand_bytes,
744        rsa::Rsa,
745        x509::{
746            extension::BasicConstraints, extension::KeyUsage, X509Extension, X509NameBuilder, X509,
747        },
748    };
749
750    #[derive(Debug)]
751    struct NoMatchLookup;
752
753    impl PrivateKeyLookup for NoMatchLookup {
754        fn lookup_private_key(
755            &self,
756            _: &ArchiveIdentity,
757            _: &RecipientRecordMetadata,
758            _: &[u8],
759        ) -> Option<Vec<u8>> {
760            None
761        }
762    }
763
764    #[derive(Debug)]
765    struct StaticLookup {
766        private_key: Vec<u8>,
767    }
768
769    impl PrivateKeyLookup for StaticLookup {
770        fn lookup_private_key(
771            &self,
772            _: &ArchiveIdentity,
773            _: &RecipientRecordMetadata,
774            _: &[u8],
775        ) -> Option<Vec<u8>> {
776            Some(self.private_key.clone())
777        }
778    }
779
780    #[derive(Debug)]
781    struct RejectLookup;
782
783    impl PrivateKeyLookup for RejectLookup {
784        fn lookup_private_key(
785            &self,
786            _: &ArchiveIdentity,
787            _: &RecipientRecordMetadata,
788            _: &[u8],
789        ) -> Option<Vec<u8>> {
790            Some(vec![0xAA; 32])
791        }
792
793        fn is_recipient_certificate_accepted(
794            &self,
795            _: &ArchiveIdentity,
796            _: &RecipientRecordMetadata,
797            _: &[u8],
798            _: &[u8; 32],
799        ) -> bool {
800            false
801        }
802    }
803
804    #[derive(Debug)]
805    struct MatchingLookup {
806        matches: Vec<(Vec<u8>, Vec<u8>)>,
807    }
808
809    impl PrivateKeyLookup for MatchingLookup {
810        fn lookup_private_key(
811            &self,
812            _: &ArchiveIdentity,
813            _: &RecipientRecordMetadata,
814            recipient_identity_bytes: &[u8],
815        ) -> Option<Vec<u8>> {
816            self.matches
817                .iter()
818                .find(|(identity, _)| identity.as_slice() == recipient_identity_bytes)
819                .map(|(_, private_key)| private_key.clone())
820        }
821    }
822
823    fn archive_identity() -> ArchiveIdentity {
824        let mut archive_uuid = [0u8; 16];
825        let mut session_id = [0u8; 16];
826        rand_bytes(&mut archive_uuid).unwrap();
827        rand_bytes(&mut session_id).unwrap();
828
829        ArchiveIdentity {
830            archive_uuid,
831            session_id,
832            format_version: FORMAT_VERSION,
833            volume_format_rev: VOLUME_FORMAT_REV_45,
834        }
835    }
836
837    fn test_certificate_der() -> Vec<u8> {
838        make_self_signed_certificate(None)
839    }
840
841    fn test_certificate_der_with_bad_key_usage() -> Vec<u8> {
842        let key_usage = KeyUsage::new()
843            .critical()
844            .digital_signature()
845            .build()
846            .unwrap();
847        make_self_signed_certificate(Some(key_usage)).to_vec()
848    }
849
850    fn test_certificate_der_with_malformed_key_usage() -> Vec<u8> {
851        let recipient_key = PKey::generate_x25519().unwrap();
852        let key_usage = X509Extension::new_from_der(
853            &Asn1Object::from_str("2.5.29.15").unwrap(),
854            true,
855            &Asn1OctetString::new_from_bytes(b"\x05\x00").unwrap(),
856        )
857        .unwrap();
858        make_certificate_for_subject(&recipient_key, Some(key_usage))
859    }
860
861    fn x25519_recipient_material() -> (Vec<u8>, Vec<u8>) {
862        let recipient_key = PKey::generate_x25519().unwrap();
863        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
864        (
865            make_certificate_for_subject(&recipient_key, Some(key_usage)),
866            recipient_key.raw_private_key().unwrap(),
867        )
868    }
869
870    fn p256_recipient_material() -> (Vec<u8>, Vec<u8>) {
871        let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
872        let recipient_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
873        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
874        (
875            make_certificate_for_subject(&recipient_key, Some(key_usage)),
876            recipient_key.private_key_to_der().unwrap(),
877        )
878    }
879
880    fn p384_recipient_certificate() -> Vec<u8> {
881        let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
882        let recipient_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
883        let key_usage = KeyUsage::new().critical().key_agreement().build().unwrap();
884        make_certificate_for_subject(&recipient_key, Some(key_usage))
885    }
886
887    fn make_self_signed_certificate(
888        key_usage_ext: Option<openssl::x509::X509Extension>,
889    ) -> Vec<u8> {
890        let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
891        let mut name = X509NameBuilder::new().unwrap();
892        name.append_entry_by_text("CN", "tzap-keywrap-test")
893            .unwrap();
894        let name = name.build();
895
896        let mut certificate = X509::builder().unwrap();
897        certificate.set_version(2).unwrap();
898        certificate
899            .set_serial_number(&random_serial_number())
900            .unwrap();
901        certificate.set_subject_name(&name).unwrap();
902        certificate.set_issuer_name(&name).unwrap();
903        certificate.set_pubkey(&key).unwrap();
904        certificate
905            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
906            .unwrap();
907        certificate
908            .set_not_after(&Asn1Time::days_from_now(365).unwrap())
909            .unwrap();
910        certificate
911            .append_extension(BasicConstraints::new().build().unwrap())
912            .unwrap();
913        if let Some(key_usage_ext) = key_usage_ext {
914            certificate.append_extension(key_usage_ext).unwrap();
915        }
916        certificate.sign(&key, MessageDigest::sha256()).unwrap();
917
918        certificate.build().to_der().unwrap()
919    }
920
921    fn make_certificate_for_subject(
922        subject_key: &PKey<Private>,
923        key_usage_ext: Option<openssl::x509::X509Extension>,
924    ) -> Vec<u8> {
925        let signer_key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
926        let mut name = X509NameBuilder::new().unwrap();
927        name.append_entry_by_text("CN", "tzap-keywrap-recipient")
928            .unwrap();
929        let name = name.build();
930
931        let mut certificate = X509::builder().unwrap();
932        certificate.set_version(2).unwrap();
933        certificate
934            .set_serial_number(&random_serial_number())
935            .unwrap();
936        certificate.set_subject_name(&name).unwrap();
937        certificate.set_issuer_name(&name).unwrap();
938        certificate.set_pubkey(subject_key).unwrap();
939        certificate
940            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
941            .unwrap();
942        certificate
943            .set_not_after(&Asn1Time::days_from_now(365).unwrap())
944            .unwrap();
945        certificate
946            .append_extension(BasicConstraints::new().build().unwrap())
947            .unwrap();
948        if let Some(key_usage_ext) = key_usage_ext {
949            certificate.append_extension(key_usage_ext).unwrap();
950        }
951        certificate
952            .sign(&signer_key, MessageDigest::sha256())
953            .unwrap();
954
955        certificate.build().to_der().unwrap()
956    }
957
958    fn random_serial_number() -> openssl::asn1::Asn1Integer {
959        let mut serial = BigNum::new().unwrap();
960        serial.rand(159, MsbOption::MAYBE_ZERO, false).unwrap();
961        serial.to_asn1_integer().unwrap()
962    }
963
964    fn make_payload(
965        archive_identity: &ArchiveIdentity,
966        metadata: &RecipientRecordMetadata,
967        identity: &ParsedRecipientIdentity,
968        suite: HpkeSuite,
969    ) -> Vec<u8> {
970        make_payload_with_lengths(
971            archive_identity,
972            metadata,
973            identity,
974            suite,
975            suite.enc_length,
976            suite.ciphertext_length,
977        )
978    }
979
980    fn make_payload_with_lengths(
981        archive_identity: &ArchiveIdentity,
982        metadata: &RecipientRecordMetadata,
983        identity: &ParsedRecipientIdentity,
984        suite: HpkeSuite,
985        enc_len: usize,
986        ciphertext_len: usize,
987    ) -> Vec<u8> {
988        let mut payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN + enc_len + ciphertext_len];
989        payload[0..2].copy_from_slice(&KEYWRAP_PAYLOAD_VERSION.to_le_bytes());
990        payload[2..4].copy_from_slice(&suite.kem_id.to_le_bytes());
991        payload[4..6].copy_from_slice(&suite.kdf_id.to_le_bytes());
992        payload[6..8].copy_from_slice(&suite.aead_id.to_le_bytes());
993        payload[8..10].copy_from_slice(&u16::try_from(enc_len).unwrap().to_le_bytes());
994        payload[10..12].copy_from_slice(&u16::try_from(ciphertext_len).unwrap().to_le_bytes());
995        payload[16..48].copy_from_slice(&compute_key_wrap_context_digest(
996            archive_identity,
997            metadata,
998            identity,
999            suite,
1000        ));
1001
1002        let enc_start = KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN;
1003        let enc_end = enc_start + enc_len;
1004        let ct_end = enc_end + ciphertext_len;
1005        for value in payload[enc_start..enc_end].iter_mut() {
1006            *value = 0xAA;
1007        }
1008        for value in payload[enc_end..ct_end].iter_mut() {
1009            *value = 0x55;
1010        }
1011
1012        payload
1013    }
1014
1015    fn recipient_record_input_with_payload(
1016        profile_payload: Vec<u8>,
1017        cert_der: &[u8],
1018    ) -> RecipientRecordInput {
1019        let identity_digest = sha256_digest(cert_der);
1020        RecipientRecordInput {
1021            archive_identity: archive_identity(),
1022            metadata: RecipientRecordMetadata {
1023                profile_id: KEYWRAP_PROFILE_ID,
1024                recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1025                recipient_identity_digest: identity_digest,
1026            },
1027            recipient_identity_bytes: cert_der.to_vec(),
1028            profile_payload_bytes: profile_payload,
1029        }
1030    }
1031
1032    fn recipient_record_input_for_suite(cert_der: &[u8], suite: HpkeSuite) -> RecipientRecordInput {
1033        let identity = parse_x509_recipient_identity(cert_der).unwrap();
1034        let archive_identity = archive_identity();
1035        let metadata = RecipientRecordMetadata {
1036            profile_id: KEYWRAP_PROFILE_ID,
1037            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1038            recipient_identity_digest: identity.recipient_identity_digest,
1039        };
1040        let payload = make_payload(&archive_identity, &metadata, &identity, suite);
1041        RecipientRecordInput {
1042            archive_identity,
1043            metadata,
1044            recipient_identity_bytes: cert_der.to_vec(),
1045            profile_payload_bytes: payload,
1046        }
1047    }
1048
1049    fn recipient_record_input_from_record(
1050        archive_identity: ArchiveIdentity,
1051        record: RecipientRecordV1,
1052    ) -> RecipientRecordInput {
1053        RecipientRecordInput {
1054            archive_identity,
1055            metadata: RecipientRecordMetadata {
1056                profile_id: record.profile_id,
1057                recipient_identity_type: record.recipient_identity_type,
1058                recipient_identity_digest: record.recipient_identity_digest,
1059            },
1060            recipient_identity_bytes: record.recipient_identity_bytes,
1061            profile_payload_bytes: record.profile_payload_bytes,
1062        }
1063    }
1064
1065    #[test]
1066    fn malformed_recipient_identity_is_invalid() {
1067        let cert_der = b"not-a-certificate".to_vec();
1068        let profile_payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1069
1070        let input = recipient_record_input_with_payload(profile_payload, &cert_der);
1071        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1072
1073        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1074    }
1075
1076    #[test]
1077    fn unsupported_identity_type_is_returned() {
1078        let cert_der = test_certificate_der();
1079        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1080
1081        let archive_identity = archive_identity();
1082        let suite =
1083            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1084                .unwrap();
1085        let payload = make_payload(
1086            &archive_identity,
1087            &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            &identity,
1093            suite,
1094        );
1095        let mut input = recipient_record_input_with_payload(payload, &cert_der);
1096        input.metadata.recipient_identity_type = 0;
1097
1098        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1099
1100        assert!(matches!(
1101            result,
1102            KeyWrapOutcome::UnsupportedRecipientIdentity
1103        ));
1104    }
1105
1106    #[test]
1107    fn unsupported_suite_is_invalid() {
1108        let cert_der = test_certificate_der();
1109        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1110        let archive_identity = archive_identity();
1111        let metadata = RecipientRecordMetadata {
1112            profile_id: KEYWRAP_PROFILE_ID,
1113            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1114            recipient_identity_digest: identity.recipient_identity_digest,
1115        };
1116
1117        let unsupported_suite = HpkeSuite {
1118            kem_id: 0x00ff,
1119            kdf_id: 0x00ff,
1120            aead_id: 0x00ff,
1121            enc_length: 0,
1122            ciphertext_length: 0,
1123        };
1124
1125        let payload = make_payload_with_lengths(
1126            &archive_identity,
1127            &metadata,
1128            &identity,
1129            unsupported_suite,
1130            0,
1131            0,
1132        );
1133
1134        let input = recipient_record_input_with_payload(payload, &cert_der);
1135        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1136
1137        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1138    }
1139
1140    #[test]
1141    fn wrong_lengths_rejected_as_invalid_record() {
1142        let cert_der = test_certificate_der();
1143        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1144        let archive_identity = archive_identity();
1145        let metadata = RecipientRecordMetadata {
1146            profile_id: KEYWRAP_PROFILE_ID,
1147            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1148            recipient_identity_digest: identity.recipient_identity_digest,
1149        };
1150        let suite =
1151            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1152                .unwrap();
1153
1154        let payload = make_payload_with_lengths(
1155            &archive_identity,
1156            &metadata,
1157            &identity,
1158            suite,
1159            suite.enc_length - 1,
1160            suite.ciphertext_length,
1161        );
1162        let input = recipient_record_input_with_payload(payload, &cert_der);
1163
1164        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1165
1166        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1167    }
1168
1169    #[test]
1170    fn wrong_context_digest_is_invalid_record() {
1171        let (cert_der, private_key) = x25519_recipient_material();
1172        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1173        let archive_identity = archive_identity();
1174        let metadata = RecipientRecordMetadata {
1175            profile_id: KEYWRAP_PROFILE_ID,
1176            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1177            recipient_identity_digest: identity.recipient_identity_digest,
1178        };
1179        let suite =
1180            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1181                .unwrap();
1182        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1183        payload[16] ^= 0xFF;
1184
1185        let input = recipient_record_input_with_payload(payload, &cert_der);
1186        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1187
1188        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1189    }
1190
1191    #[test]
1192    fn revision_45_never_retries_revision_44_hpke_aad() {
1193        let (cert_der, private_key) = x25519_recipient_material();
1194        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1195        let archive_identity = archive_identity();
1196        let metadata = RecipientRecordMetadata {
1197            profile_id: KEYWRAP_PROFILE_ID,
1198            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1199            recipient_identity_digest: identity.recipient_identity_digest,
1200        };
1201        let suite =
1202            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1203                .unwrap();
1204        let payload = hpke_seal_master_key_with_aad_domain(
1205            &cert_der,
1206            &archive_identity,
1207            &metadata,
1208            &identity,
1209            &[0x42; 32],
1210            suite,
1211            b"tzap-keywrap-master-key-v44\0",
1212        )
1213        .unwrap();
1214        let input = RecipientRecordInput {
1215            archive_identity,
1216            metadata,
1217            recipient_identity_bytes: cert_der,
1218            profile_payload_bytes: payload,
1219        };
1220
1221        assert!(matches!(
1222            dispatch_key_wrap_record(input, &StaticLookup { private_key }),
1223            KeyWrapOutcome::InvalidRecord
1224        ));
1225    }
1226
1227    #[test]
1228    fn revision_44_archive_identity_is_rejected_without_compatibility_dispatch() {
1229        let (cert_der, _) = x25519_recipient_material();
1230        let mut identity = archive_identity();
1231        identity.volume_format_rev = 44;
1232        assert!(matches!(
1233            wrap_master_key_for_recipient(
1234                identity,
1235                &cert_der,
1236                &[0x42; 32],
1237                KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1238            ),
1239            Err(KeyWrapOutcome::UnsupportedArchiveIdentity)
1240        ));
1241    }
1242
1243    #[test]
1244    fn no_matching_private_key_precedes_context_digest_check() {
1245        let (cert_der, _) = x25519_recipient_material();
1246        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1247        let archive_identity = archive_identity();
1248        let metadata = RecipientRecordMetadata {
1249            profile_id: KEYWRAP_PROFILE_ID,
1250            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1251            recipient_identity_digest: identity.recipient_identity_digest,
1252        };
1253        let suite =
1254            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1255                .unwrap();
1256        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1257        payload[16] ^= 0xFF;
1258
1259        let input = recipient_record_input_with_payload(payload, &cert_der);
1260        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1261
1262        assert!(matches!(result, KeyWrapOutcome::NoMatchingPrivateKey));
1263    }
1264
1265    #[test]
1266    fn certificate_policy_rejection_precedes_context_digest_check() {
1267        let (cert_der, _) = x25519_recipient_material();
1268        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1269        let archive_identity = archive_identity();
1270        let metadata = RecipientRecordMetadata {
1271            profile_id: KEYWRAP_PROFILE_ID,
1272            recipient_identity_type: KEYWRAP_RECIPIENT_IDENTITY_TYPE_BYTES,
1273            recipient_identity_digest: identity.recipient_identity_digest,
1274        };
1275        let suite =
1276            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1277                .unwrap();
1278        let mut payload = make_payload(&archive_identity, &metadata, &identity, suite);
1279        payload[16] ^= 0xFF;
1280
1281        let input = recipient_record_input_with_payload(payload, &cert_der);
1282        let result = dispatch_key_wrap_record(input, &RejectLookup);
1283
1284        assert!(matches!(result, KeyWrapOutcome::CertificatePolicyRejected));
1285    }
1286
1287    #[test]
1288    fn valid_structure_with_no_matching_private_key_is_no_matching() {
1289        let archive_identity = archive_identity();
1290        let (cert_der, _) = x25519_recipient_material();
1291        let master_key = [0x42u8; 32];
1292        let record = wrap_master_key_for_recipient(
1293            archive_identity.clone(),
1294            &cert_der,
1295            &master_key,
1296            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1297        )
1298        .unwrap();
1299        let input = recipient_record_input_from_record(archive_identity, record);
1300
1301        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1302
1303        assert!(matches!(result, KeyWrapOutcome::NoMatchingPrivateKey));
1304    }
1305
1306    #[test]
1307    fn key_usage_without_key_agreement_is_invalid_record() {
1308        let cert_der = test_certificate_der_with_bad_key_usage();
1309        let payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1310        let input = recipient_record_input_with_payload(payload, &cert_der);
1311
1312        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1313
1314        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1315    }
1316
1317    #[test]
1318    fn malformed_key_usage_extension_is_invalid_record() {
1319        let cert_der = test_certificate_der_with_malformed_key_usage();
1320        let payload = vec![0u8; KEYWRAP_PROFILE_PAYLOAD_HEADER_LEN];
1321        let input = recipient_record_input_with_payload(payload, &cert_der);
1322
1323        let result = dispatch_key_wrap_record(input, &NoMatchLookup);
1324
1325        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1326    }
1327
1328    #[test]
1329    fn certificate_policy_rejection_is_reported() {
1330        let archive_identity = archive_identity();
1331        let (cert_der, _) = x25519_recipient_material();
1332        let master_key = [0x42u8; 32];
1333        let record = wrap_master_key_for_recipient(
1334            archive_identity.clone(),
1335            &cert_der,
1336            &master_key,
1337            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1338        )
1339        .unwrap();
1340        let input = recipient_record_input_from_record(archive_identity, record);
1341
1342        let result = dispatch_key_wrap_record(input, &RejectLookup);
1343
1344        assert!(matches!(result, KeyWrapOutcome::CertificatePolicyRejected));
1345    }
1346
1347    #[test]
1348    fn suite_certificate_mismatch_is_unsupported_before_private_key_lookup() {
1349        let (p256_cert_der, _) = p256_recipient_material();
1350        let x25519_suite =
1351            HpkeSuite::for_profile(X25519_KEM_ID, HKDF_SHA256_KDF_ID, CHACHA20POLY1305_AEAD_ID)
1352                .unwrap();
1353        let result = dispatch_key_wrap_record(
1354            recipient_record_input_for_suite(&p256_cert_der, x25519_suite),
1355            &NoMatchLookup,
1356        );
1357        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1358
1359        let (x25519_cert_der, _) = x25519_recipient_material();
1360        let p256_suite =
1361            HpkeSuite::for_profile(P256_KEM_ID, HKDF_SHA256_KDF_ID, AES256GCM_AEAD_ID).unwrap();
1362        let result = dispatch_key_wrap_record(
1363            recipient_record_input_for_suite(&x25519_cert_der, p256_suite),
1364            &NoMatchLookup,
1365        );
1366        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1367
1368        let p384_cert_der = p384_recipient_certificate();
1369        let result = dispatch_key_wrap_record(
1370            recipient_record_input_for_suite(&p384_cert_der, p256_suite),
1371            &NoMatchLookup,
1372        );
1373        assert!(matches!(result, KeyWrapOutcome::UnsupportedSuite));
1374    }
1375
1376    #[test]
1377    fn writer_reports_unsupported_suite_for_certificate_mismatch() {
1378        let master_key = [0x42u8; 32];
1379        let (p256_cert_der, _) = p256_recipient_material();
1380        let result = wrap_master_key_for_recipient(
1381            archive_identity(),
1382            &p256_cert_der,
1383            &master_key,
1384            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1385        );
1386        assert!(matches!(result, Err(KeyWrapOutcome::UnsupportedSuite)));
1387
1388        let (x25519_cert_der, _) = x25519_recipient_material();
1389        let result = wrap_master_key_for_recipient(
1390            archive_identity(),
1391            &x25519_cert_der,
1392            &master_key,
1393            KeyWrapSuite::P256HkdfSha256Aes256Gcm,
1394        );
1395        assert!(matches!(result, Err(KeyWrapOutcome::UnsupportedSuite)));
1396    }
1397
1398    #[test]
1399    fn x25519_wrap_unwrap_returns_candidate_master_key() {
1400        let (cert_der, private_key) = x25519_recipient_material();
1401        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1402        let archive_identity = archive_identity();
1403        let master_key = [0x42u8; 32];
1404        let record = wrap_master_key_for_recipient(
1405            archive_identity.clone(),
1406            &cert_der,
1407            &master_key,
1408            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1409        )
1410        .unwrap();
1411        let input = recipient_record_input_from_record(archive_identity, record);
1412
1413        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1414
1415        assert!(matches!(
1416            result,
1417            KeyWrapOutcome::UnwrappedCandidateMasterKey {
1418                master_key: actual_master_key,
1419                recipient_spki_digest: actual_digest
1420            } if actual_master_key == master_key && actual_digest == identity.recipient_spki_digest
1421        ));
1422    }
1423
1424    #[test]
1425    fn p256_wrap_unwrap_returns_candidate_master_key() {
1426        let (cert_der, private_key) = p256_recipient_material();
1427        let identity = parse_x509_recipient_identity(&cert_der).unwrap();
1428        let archive_identity = archive_identity();
1429        let master_key = [0x24u8; 32];
1430        let record = wrap_master_key_for_recipient(
1431            archive_identity.clone(),
1432            &cert_der,
1433            &master_key,
1434            KeyWrapSuite::P256HkdfSha256Aes256Gcm,
1435        )
1436        .unwrap();
1437        let input = recipient_record_input_from_record(archive_identity, record);
1438
1439        let result = dispatch_key_wrap_record(input, &StaticLookup { private_key });
1440
1441        assert!(matches!(
1442            result,
1443            KeyWrapOutcome::UnwrappedCandidateMasterKey {
1444                master_key: actual_master_key,
1445                recipient_spki_digest: actual_digest
1446            } if actual_master_key == master_key && actual_digest == identity.recipient_spki_digest
1447        ));
1448    }
1449
1450    #[test]
1451    fn multi_recipient_records_open_independently_with_matching_private_key() {
1452        let archive_identity = archive_identity();
1453        let master_key = [0x33u8; 32];
1454        let (bob_cert, bob_key) = x25519_recipient_material();
1455        let (carol_cert, carol_key) = p256_recipient_material();
1456        let (ops_cert, ops_key) = x25519_recipient_material();
1457        let recipients = [
1458            (
1459                bob_cert,
1460                bob_key,
1461                KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1462            ),
1463            (carol_cert, carol_key, KeyWrapSuite::P256HkdfSha256Aes256Gcm),
1464            (
1465                ops_cert,
1466                ops_key,
1467                KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1468            ),
1469        ];
1470        let records = recipients
1471            .iter()
1472            .map(|(cert_der, _, suite)| {
1473                wrap_master_key_for_recipient(
1474                    archive_identity.clone(),
1475                    cert_der,
1476                    &master_key,
1477                    *suite,
1478                )
1479                .unwrap()
1480            })
1481            .collect::<Vec<_>>();
1482
1483        for (cert_der, private_key, _) in recipients {
1484            let lookup = MatchingLookup {
1485                matches: vec![(cert_der.clone(), private_key)],
1486            };
1487            let unwrapped = records
1488                .clone()
1489                .into_iter()
1490                .filter_map(|record| {
1491                    let input =
1492                        recipient_record_input_from_record(archive_identity.clone(), record);
1493                    match dispatch_key_wrap_record(input, &lookup) {
1494                        KeyWrapOutcome::UnwrappedCandidateMasterKey {
1495                            master_key: candidate,
1496                            ..
1497                        } => Some(candidate),
1498                        KeyWrapOutcome::NoMatchingPrivateKey => None,
1499                        other => panic!("unexpected key-wrap outcome: {other:?}"),
1500                    }
1501                })
1502                .collect::<Vec<_>>();
1503
1504            assert_eq!(unwrapped, vec![master_key]);
1505        }
1506    }
1507
1508    #[test]
1509    fn wrong_private_key_is_invalid_record() {
1510        let (cert_der, _private_key) = x25519_recipient_material();
1511        let (_wrong_cert_der, wrong_private_key) = x25519_recipient_material();
1512        let archive_identity = archive_identity();
1513        let master_key = [0x5au8; 32];
1514        let record = wrap_master_key_for_recipient(
1515            archive_identity.clone(),
1516            &cert_der,
1517            &master_key,
1518            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1519        )
1520        .unwrap();
1521        let input = recipient_record_input_from_record(archive_identity, record);
1522
1523        let result = dispatch_key_wrap_record(
1524            input,
1525            &StaticLookup {
1526                private_key: wrong_private_key,
1527            },
1528        );
1529
1530        assert!(matches!(result, KeyWrapOutcome::InvalidRecord));
1531    }
1532
1533    #[test]
1534    fn signing_certificate_key_usage_does_not_satisfy_recipient_wrap() {
1535        let cert_der = test_certificate_der_with_bad_key_usage();
1536        let master_key = [0x42u8; 32];
1537
1538        let result = wrap_master_key_for_recipient(
1539            archive_identity(),
1540            &cert_der,
1541            &master_key,
1542            KeyWrapSuite::X25519HkdfSha256ChaCha20Poly1305,
1543        );
1544
1545        assert!(matches!(result, Err(KeyWrapOutcome::InvalidRecord)));
1546    }
1547}