Skip to main content

basil_cose/
keys.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Shipped local key implementations of the [`Signer`], [`Verifier`], and
6//! [`Recipient`] traits.
7//!
8//! Broker-backed implementations live in the basil client crate; these local
9//! ones hold key material directly, with every secret in a `Zeroizing`
10//! wrapper (or a `ZeroizeOnDrop` dalek type) on success and error paths.
11
12use alloc::collections::BTreeMap;
13use alloc::vec::Vec;
14
15use ed25519_dalek::{Signer as _, SigningKey, VerifyingKey};
16use p256::ecdsa::signature::Verifier as _;
17use p256::ecdsa::{
18    Signature as P256Signature, SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey,
19};
20use x25519_dalek::{PublicKey, StaticSecret};
21use zeroize::Zeroizing;
22
23use crate::alg::SignatureAlgorithm;
24use crate::codec;
25use crate::error::{OpenError, SignError, VerifyError};
26use crate::kdf;
27use crate::traits::{OpenRequest, Recipient, Signer, Verifier};
28use crate::types::{KeyId, Signature};
29
30/// A local Ed25519 signer: a `SigningKey` (`ZeroizeOnDrop`) plus its key id.
31pub struct Ed25519Signer {
32    key: SigningKey,
33    key_id: KeyId,
34}
35
36impl Ed25519Signer {
37    /// Build a signer from the 32 secret seed bytes.
38    #[must_use]
39    pub fn from_secret_bytes(key_id: KeyId, secret: &Zeroizing<[u8; 32]>) -> Self {
40        Self {
41            key: SigningKey::from_bytes(secret),
42            key_id,
43        }
44    }
45
46    /// The Ed25519 public key bytes for this signer.
47    #[must_use]
48    pub fn public_key_bytes(&self) -> [u8; 32] {
49        self.key.verifying_key().to_bytes()
50    }
51}
52
53impl Signer for Ed25519Signer {
54    fn key_id(&self) -> &KeyId {
55        &self.key_id
56    }
57
58    fn algorithm(&self) -> SignatureAlgorithm {
59        SignatureAlgorithm::EdDsa
60    }
61
62    async fn sign(&self, sig_structure: &[u8]) -> Result<Signature, SignError> {
63        let sig = self.key.sign(sig_structure);
64        Signature::from_bytes(sig.to_bytes().to_vec()).map_err(|_| SignError::Provider {
65            message: alloc::string::String::from("empty signature"),
66        })
67    }
68}
69
70/// A local Ed25519 verifier over one or more pinned public keys, looked up
71/// by key id.
72pub struct Ed25519Verifier {
73    keys: BTreeMap<KeyId, VerifyingKey>,
74}
75
76/// Why constructing a local key failed.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum KeyError {
79    /// The bytes are not a valid public key for the key type.
80    InvalidPublicKey,
81    /// The bytes are not a valid private key (scalar) for the key type.
82    InvalidPrivateKey,
83}
84
85impl core::fmt::Display for KeyError {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        match self {
88            Self::InvalidPublicKey => write!(f, "invalid public key"),
89            Self::InvalidPrivateKey => write!(f, "invalid private key"),
90        }
91    }
92}
93
94impl core::error::Error for KeyError {}
95
96impl Ed25519Verifier {
97    /// A verifier pinned to a single key.
98    ///
99    /// # Errors
100    /// [`KeyError::InvalidPublicKey`] if the bytes are not a valid point.
101    pub fn from_key(key_id: KeyId, public: &[u8; 32]) -> Result<Self, KeyError> {
102        let mut keys = BTreeMap::new();
103        keys.insert(
104            key_id,
105            VerifyingKey::from_bytes(public).map_err(|_| KeyError::InvalidPublicKey)?,
106        );
107        Ok(Self { keys })
108    }
109
110    /// Pin an additional key.
111    ///
112    /// # Errors
113    /// [`KeyError::InvalidPublicKey`] if the bytes are not a valid point.
114    pub fn add_key(&mut self, key_id: KeyId, public: &[u8; 32]) -> Result<(), KeyError> {
115        self.keys.insert(
116            key_id,
117            VerifyingKey::from_bytes(public).map_err(|_| KeyError::InvalidPublicKey)?,
118        );
119        Ok(())
120    }
121}
122
123impl Verifier for Ed25519Verifier {
124    async fn verify(
125        &self,
126        key_id: &KeyId,
127        algorithm: SignatureAlgorithm,
128        _protected_headers: &crate::claims::ProtectedHeaders,
129        sig_structure: &[u8],
130        signature: &Signature,
131    ) -> Result<(), VerifyError> {
132        if algorithm != SignatureAlgorithm::EdDsa {
133            return Err(VerifyError::AlgorithmMismatch);
134        }
135        let key = self.keys.get(key_id).ok_or(VerifyError::UnknownKeyId)?;
136        let sig_bytes: [u8; 64] = signature
137            .as_bytes()
138            .try_into()
139            .map_err(|_| VerifyError::SignatureInvalid)?;
140        let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
141        // verify_strict rejects small-order/mixed-order components.
142        key.verify_strict(sig_structure, &sig)
143            .map_err(|_| VerifyError::SignatureInvalid)
144    }
145}
146
147/// A local `ES256` signer: a P-256 `SigningKey` plus its key id.
148///
149/// Signing is deterministic (RFC 6979) and low-`S` normalized, so re-signing
150/// the same `Sig_structure` yields byte-identical output: the profile's
151/// determinism guarantee holds for `ES256` as it does for `EdDSA`.
152pub struct Es256Signer {
153    key: P256SigningKey,
154    key_id: KeyId,
155}
156
157impl Es256Signer {
158    /// Build a signer from the 32 secret scalar bytes (big-endian).
159    ///
160    /// # Errors
161    /// [`KeyError::InvalidPrivateKey`] if the bytes are not a valid non-zero
162    /// P-256 scalar.
163    pub fn from_secret_bytes(
164        key_id: KeyId,
165        secret: &Zeroizing<[u8; 32]>,
166    ) -> Result<Self, KeyError> {
167        let key = P256SigningKey::from_slice(secret.as_slice())
168            .map_err(|_| KeyError::InvalidPrivateKey)?;
169        Ok(Self { key, key_id })
170    }
171
172    /// The uncompressed SEC1 public key bytes (`0x04 || X || Y`, 65 bytes) for
173    /// this signer.
174    #[must_use]
175    pub fn public_key_sec1(&self) -> Vec<u8> {
176        self.key
177            .verifying_key()
178            .to_encoded_point(false)
179            .as_bytes()
180            .to_vec()
181    }
182}
183
184impl Signer for Es256Signer {
185    fn key_id(&self) -> &KeyId {
186        &self.key_id
187    }
188
189    fn algorithm(&self) -> SignatureAlgorithm {
190        SignatureAlgorithm::Es256
191    }
192
193    async fn sign(&self, sig_structure: &[u8]) -> Result<Signature, SignError> {
194        // `try_sign` is the fallible, no-panic entry; the `ecdsa` crate hashes
195        // with SHA-256 and derives the nonce with RFC 6979 (no RNG).
196        let sig: P256Signature =
197            self.key
198                .try_sign(sig_structure)
199                .map_err(|_| SignError::Provider {
200                    message: alloc::string::String::from("ecdsa signing failed"),
201                })?;
202        let sig = sig.normalize_s().unwrap_or(sig);
203        Signature::from_bytes(sig.to_bytes().to_vec()).map_err(|_| SignError::Provider {
204            message: alloc::string::String::from("empty signature"),
205        })
206    }
207}
208
209/// A local `ES256` verifier over one or more pinned P-256 public keys, looked
210/// up by key id.
211pub struct P256Verifier {
212    keys: BTreeMap<KeyId, P256VerifyingKey>,
213}
214
215impl P256Verifier {
216    /// A verifier pinned to a single key, from SEC1 public key bytes
217    /// (compressed or uncompressed).
218    ///
219    /// # Errors
220    /// [`KeyError::InvalidPublicKey`] if the bytes are not a valid P-256 point.
221    pub fn from_sec1(key_id: KeyId, public: &[u8]) -> Result<Self, KeyError> {
222        let mut keys = BTreeMap::new();
223        keys.insert(
224            key_id,
225            P256VerifyingKey::from_sec1_bytes(public).map_err(|_| KeyError::InvalidPublicKey)?,
226        );
227        Ok(Self { keys })
228    }
229
230    /// Pin an additional key from SEC1 public key bytes.
231    ///
232    /// # Errors
233    /// [`KeyError::InvalidPublicKey`] if the bytes are not a valid P-256 point.
234    pub fn add_key(&mut self, key_id: KeyId, public: &[u8]) -> Result<(), KeyError> {
235        self.keys.insert(
236            key_id,
237            P256VerifyingKey::from_sec1_bytes(public).map_err(|_| KeyError::InvalidPublicKey)?,
238        );
239        Ok(())
240    }
241}
242
243impl Verifier for P256Verifier {
244    async fn verify(
245        &self,
246        key_id: &KeyId,
247        algorithm: SignatureAlgorithm,
248        _protected_headers: &crate::claims::ProtectedHeaders,
249        sig_structure: &[u8],
250        signature: &Signature,
251    ) -> Result<(), VerifyError> {
252        if algorithm != SignatureAlgorithm::Es256 {
253            return Err(VerifyError::AlgorithmMismatch);
254        }
255        let key = self.keys.get(key_id).ok_or(VerifyError::UnknownKeyId)?;
256        // `from_slice` requires the fixed 64-byte `r || s` COSE form; a DER or
257        // wrong-length signature is rejected here.
258        let sig = P256Signature::from_slice(signature.as_bytes())
259            .map_err(|_| VerifyError::SignatureInvalid)?;
260        if sig.normalize_s().is_some() {
261            return Err(VerifyError::SignatureInvalid);
262        }
263        key.verify(sig_structure, &sig)
264            .map_err(|_| VerifyError::SignatureInvalid)
265    }
266}
267
268/// A recipient's static X25519 **public** key: the seal target.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct X25519RecipientPublic {
271    /// The recipient key id (becomes the recipient `kid`).
272    pub key_id: KeyId,
273    /// The static X25519 public key bytes.
274    pub public: [u8; 32],
275}
276
277/// A local X25519 recipient: the materialized static private key
278/// (`Zeroizing`) plus its key id.
279pub struct X25519Recipient {
280    private: Zeroizing<[u8; 32]>,
281    key_id: KeyId,
282}
283
284impl X25519Recipient {
285    /// Build a recipient from the 32 private key bytes.
286    #[must_use]
287    pub const fn new(key_id: KeyId, private: Zeroizing<[u8; 32]>) -> Self {
288        Self { private, key_id }
289    }
290
291    /// Wrap a raw private key slice, failing closed (never indexing) on a
292    /// wrong length. The caller should hand the slice from a `Zeroizing`
293    /// buffer; the copy taken here is itself `Zeroizing`.
294    ///
295    /// # Errors
296    /// [`KeyLengthError`] if `bytes` is not exactly 32 bytes.
297    pub fn from_private_slice(key_id: KeyId, bytes: &[u8]) -> Result<Self, KeyLengthError> {
298        let arr: [u8; 32] = bytes.try_into().map_err(|_| KeyLengthError {
299            actual: bytes.len(),
300        })?;
301        Ok(Self::new(key_id, Zeroizing::new(arr)))
302    }
303
304    /// The corresponding public half (derived; the private never leaves).
305    #[must_use]
306    pub fn public(&self) -> X25519RecipientPublic {
307        let secret = StaticSecret::from(*self.private);
308        X25519RecipientPublic {
309            key_id: self.key_id.clone(),
310            public: PublicKey::from(&secret).to_bytes(),
311        }
312    }
313}
314
315/// A private key slice was not exactly 32 bytes.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct KeyLengthError {
318    /// The length actually supplied.
319    pub actual: usize,
320}
321
322impl core::fmt::Display for KeyLengthError {
323    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
324        write!(
325            f,
326            "X25519 private key must be 32 bytes, got {}",
327            self.actual
328        )
329    }
330}
331
332impl core::error::Error for KeyLengthError {}
333
334impl Recipient for X25519Recipient {
335    fn key_id(&self) -> &KeyId {
336        &self.key_id
337    }
338
339    async fn open(&self, request: &OpenRequest<'_>) -> Result<Zeroizing<Vec<u8>>, OpenError> {
340        open_local(self, request)
341    }
342}
343
344/// The synchronous local open: strict decode, party pinning, contributory
345/// ECDH, `COSE_KDF_Context` HKDF, AEAD open with the exact protected bytes.
346fn open_local(
347    recipient: &X25519Recipient,
348    request: &OpenRequest<'_>,
349) -> Result<Zeroizing<Vec<u8>>, OpenError> {
350    // The bytes may arrive from a caller that has not pre-validated them
351    // (the trait is bytes-in for remote-forwarding symmetry), so strict
352    // decode here as well. Claims may or may not be present: both the
353    // sealed embedded layer and the seal-only layer route here.
354    let decoded = decode_any_encrypt(request.cose_encrypt)?;
355
356    if decoded.recipient_kid != recipient.key_id {
357        return Err(OpenError::RecipientKeyMismatch);
358    }
359    if let Some(expected) = request.expected_parties
360        && *expected != decoded.parties
361    {
362        return Err(OpenError::PartyMismatch);
363    }
364
365    let secret = StaticSecret::from(*recipient.private);
366    let ephemeral = PublicKey::from(decoded.ephemeral_x);
367    let shared_secret = secret.diffie_hellman(&ephemeral);
368    // Reject a low-order / all-zero shared secret: an attacker-supplied
369    // small-order ephemeral would force a known AEAD key -> a forgeable
370    // message this would otherwise ACCEPT. Fail closed BEFORE deriving,
371    // with the same opaque error as any other authentication failure.
372    if !shared_secret.was_contributory() {
373        return Err(OpenError::OpenFailed);
374    }
375    let shared = Zeroizing::new(shared_secret.to_bytes());
376
377    let info = codec::kdf_context(
378        decoded.content_algorithm,
379        &decoded.parties,
380        &decoded.recipient_protected,
381    )
382    .map_err(|codec::CodecError| OpenError::OpenFailed)?;
383    let cek = kdf::derive_cek(&shared, &info).map_err(|kdf::KdfFailed| OpenError::OpenFailed)?;
384
385    let aad = codec::enc_structure(&decoded.protected, request.external_aad.as_bytes())
386        .map_err(|codec::CodecError| OpenError::OpenFailed)?;
387
388    crate::encrypt::aead_open(
389        decoded.content_algorithm,
390        &cek,
391        &decoded.iv,
392        &decoded.ciphertext,
393        &aad,
394    )
395}
396
397/// Strict-decode a `COSE_Encrypt` accepting either claims posture (the
398/// recipient does not decide the construction; the calling entry point
399/// already did).
400fn decode_any_encrypt(bytes: &[u8]) -> Result<codec::DecodedEncrypt, OpenError> {
401    match codec::decode_encrypt_strict(bytes, codec::ClaimsExpectation::Optional) {
402        Ok(d) => Ok(d),
403        Err(e) => Err(OpenError::Decode(e)),
404    }
405}