Skip to main content

basil_cose/
kdf.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! KDF party identities and the ECDH-ES + HKDF-256 content-key derivation.
6//!
7//! The HKDF info is the RFC 9053 §5.2 `COSE_KDF_Context` (serialized by the
8//! codec seam), never a bespoke label. Party identities ride in the recipient
9//! protected headers (`-21`/`-24`) so any opener, including one that is not
10//! the sealer, like the broker unseal RPC, can rebuild the context from the
11//! message alone.
12
13use alloc::vec::Vec;
14
15use hkdf::Hkdf;
16use sha2::Sha256;
17use zeroize::Zeroizing;
18
19use crate::error::ProfileError;
20
21/// One party's identity slot in the `COSE_KDF_Context` (RFC 9053 §5.2).
22///
23/// v1 exposes identity only; the nonce and "other" slots are pinned nil by
24/// the profile (their header parameters are rejected on decode).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct PartyIdentity(Option<Vec<u8>>);
27
28impl PartyIdentity {
29    /// The nil identity (anonymous party).
30    #[must_use]
31    pub const fn nil() -> Self {
32        Self(None)
33    }
34
35    /// A concrete party identity
36    ///
37    /// # Errors
38    /// [`ProfileError::EmptyPartyIdentity`] if the bytes are empty; an empty
39    /// identity is indistinguishable in intent from nil, so the profile
40    /// forbids it.
41    pub fn from_bytes(identity: Vec<u8>) -> Result<Self, ProfileError> {
42        if identity.is_empty() {
43            return Err(ProfileError::EmptyPartyIdentity);
44        }
45        Ok(Self(Some(identity)))
46    }
47
48    /// The identity bytes, when present.
49    #[must_use]
50    pub fn as_bytes(&self) -> Option<&[u8]> {
51        self.0.as_deref()
52    }
53}
54
55/// `PartyU` = message sender, `PartyV` = recipient, per RFC 9053 §5.1.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct KdfParties {
58    /// The sender's identity slot.
59    pub party_u: PartyIdentity,
60    /// The recipient's identity slot.
61    pub party_v: PartyIdentity,
62}
63
64impl KdfParties {
65    /// Both slots nil (the basil invocation v1 posture).
66    #[must_use]
67    pub const fn anonymous() -> Self {
68        Self {
69            party_u: PartyIdentity::nil(),
70            party_v: PartyIdentity::nil(),
71        }
72    }
73
74    /// Whether both slots are nil.
75    #[must_use]
76    pub const fn is_anonymous(&self) -> bool {
77        self.party_u.0.is_none() && self.party_v.0.is_none()
78    }
79}
80
81/// Key-derivation failure marker (only reachable on an out-of-range HKDF
82/// output length, never for the fixed 32-byte key; kept so the construction
83/// cannot `unwrap`).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct KdfFailed;
86
87/// Derive the 256-bit content-encryption key: HKDF-SHA-256 with no salt over
88/// the X25519 shared secret, with the serialized `COSE_KDF_Context` as info.
89pub fn derive_cek(
90    shared_secret: &Zeroizing<[u8; 32]>,
91    kdf_context: &[u8],
92) -> Result<Zeroizing<[u8; 32]>, KdfFailed> {
93    let hk = Hkdf::<Sha256>::new(None, shared_secret.as_slice());
94    let mut okm = Zeroizing::new([0u8; 32]);
95    hk.expand(kdf_context, okm.as_mut_slice())
96        .map_err(|_| KdfFailed)?;
97    Ok(okm)
98}