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