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