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