Skip to content

Commit 3b011bd

Browse files
authored
[KeyManager] Add HPKE and DHKEM crypto primitives (google#643)
Introduces a `crypto` module in `km_common` to handle cryptographic operations backed by BoringSSL (`bssl-crypto`). Key Contributions: 1. HPKE Decryption (`decrypt`): - Implements `decrypt` using `bssl_crypto::hpke` for "single-shot" Hybrid Public Key Encryption (HPKE) opening. - Supports the standard HPKE suite: DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-256-GCM. - Maps internal `HpkeAlgorithm` types to BoringSSL parameters. 2. DHKEM Decapsulation (`decaps`): - Implements a standalone `decaps` function for DHKEM(X25519, HKDF-SHA256). - Manually constructs the KEM context (suite ID, labeled IKM/Info) compliant with RFC 9180 to derive the shared secret directly from an encapsulated key and private key. - Validates implementation against test vectors accounting for BoringSSL's internal private key clamping. 3. Dependencies & Error Handling: - Adds `bssl-crypto` for underlying crypto operations. - Uses `thiserror` for structured error definitions (`KeyLenMismatch`, `DecapsError`, `HpkeDecryptionError`).
1 parent a7b52b8 commit 3b011bd

File tree

6 files changed

+672
-28
lines changed

6 files changed

+672
-28
lines changed

keymanager/km_common/src/crypto.rs

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
use crate::algorithms::{HpkeAlgorithm, KemAlgorithm};
2+
pub mod secret_box;
3+
use crate::crypto::secret_box::SecretBox;
4+
use clear_on_drop::clear_stack_on_return;
5+
use thiserror::Error;
6+
7+
mod x25519;
8+
pub use x25519::{X25519PrivateKey, X25519PublicKey};
9+
10+
const CLEAR_STACK_PAGES: usize = 2;
11+
12+
/// A trait for public keys with algorithm-specific implementations.
13+
pub(crate) trait PublicKeyOps: Send + Sync {
14+
/// Encrypts a plaintext using HPKE.
15+
///
16+
/// Returns a tuple containing the encapsulated key and the ciphertext respectively.
17+
fn hpke_seal_internal(
18+
&self,
19+
plaintext: &SecretBox,
20+
aad: &[u8],
21+
algo: &HpkeAlgorithm,
22+
) -> Result<(Vec<u8>, Vec<u8>), Error>;
23+
24+
/// Returns the raw bytes of the public key.
25+
fn as_bytes(&self) -> &[u8];
26+
}
27+
28+
/// A trait for private keys with algorithm-specific implementations.
29+
pub(crate) trait PrivateKeyOps: Send + Sync {
30+
/// Decapsulates the shared secret from an encapsulated key.
31+
///
32+
/// Returns the decapsulated shared secret as a `SecretBox`.
33+
fn decaps_internal(&self, enc: &[u8]) -> Result<SecretBox, Error>;
34+
35+
/// Decrypts a ciphertext using HPKE.
36+
///
37+
/// Returns the decrypted plaintext as a `SecretBox`.
38+
fn hpke_open_internal(
39+
&self,
40+
enc: &[u8],
41+
ciphertext: &[u8],
42+
aad: &[u8],
43+
algo: &HpkeAlgorithm,
44+
) -> Result<SecretBox, Error>;
45+
}
46+
47+
/// A wrapper enum for different public key types.
48+
#[derive(Debug, Clone, PartialEq, Eq)]
49+
pub enum PublicKey {
50+
X25519(X25519PublicKey),
51+
}
52+
53+
impl PublicKey {
54+
/// Returns the raw bytes of the public key.
55+
pub fn as_bytes(&self) -> &[u8] {
56+
match self {
57+
PublicKey::X25519(pk) => pk.as_bytes(),
58+
}
59+
}
60+
}
61+
62+
impl PublicKeyOps for PublicKey {
63+
fn hpke_seal_internal(
64+
&self,
65+
plaintext: &SecretBox,
66+
aad: &[u8],
67+
algo: &HpkeAlgorithm,
68+
) -> Result<(Vec<u8>, Vec<u8>), Error> {
69+
match self {
70+
PublicKey::X25519(pk) => pk.hpke_seal_internal(plaintext, aad, algo),
71+
}
72+
}
73+
74+
fn as_bytes(&self) -> &[u8] {
75+
self.as_bytes()
76+
}
77+
}
78+
79+
/// A wrapper enum for different private key types.
80+
pub enum PrivateKey {
81+
X25519(X25519PrivateKey),
82+
}
83+
84+
impl PrivateKeyOps for PrivateKey {
85+
fn decaps_internal(&self, enc: &[u8]) -> Result<SecretBox, Error> {
86+
match self {
87+
PrivateKey::X25519(sk) => sk.decaps_internal(enc),
88+
}
89+
}
90+
91+
fn hpke_open_internal(
92+
&self,
93+
enc: &[u8],
94+
ciphertext: &[u8],
95+
aad: &[u8],
96+
algo: &HpkeAlgorithm,
97+
) -> Result<SecretBox, Error> {
98+
match self {
99+
PrivateKey::X25519(sk) => sk.hpke_open_internal(enc, ciphertext, aad, algo),
100+
}
101+
}
102+
}
103+
104+
#[derive(Debug, Error)]
105+
pub enum Error {
106+
#[error("Key length mismatch")]
107+
KeyLenMismatch,
108+
#[error("Decapsulation error")]
109+
DecapsError,
110+
#[error("HPKE decryption error")]
111+
HpkeDecryptionError,
112+
#[error("HPKE encryption error")]
113+
HpkeEncryptionError,
114+
#[error("Unsupported algorithm")]
115+
UnsupportedAlgorithm,
116+
#[error("Crypto library error")]
117+
CryptoError,
118+
}
119+
120+
/// Generates a keypair for the given KEM algorithm.
121+
///
122+
/// Returns a tuple containing the public and private keys respectively.
123+
pub fn generate_keypair(algo: KemAlgorithm) -> Result<(PublicKey, PrivateKey), Error> {
124+
clear_stack_on_return(CLEAR_STACK_PAGES, || match algo {
125+
KemAlgorithm::DhkemX25519HkdfSha256 => {
126+
let (pk, sk) = x25519::generate_keypair();
127+
Ok((PublicKey::X25519(pk), PrivateKey::X25519(sk)))
128+
}
129+
_ => Err(Error::UnsupportedAlgorithm),
130+
})
131+
}
132+
133+
/// Decapsulates the shared secret from an encapsulated key using the specified private key.
134+
///
135+
/// Returns the decapsulated shared secret as a `SecretBox`.
136+
pub fn decaps(priv_key: &PrivateKey, enc: &[u8]) -> Result<SecretBox, Error> {
137+
clear_stack_on_return(CLEAR_STACK_PAGES, || priv_key.decaps_internal(enc))
138+
}
139+
140+
/// Decrypts a ciphertext using HPKE (Hybrid Public Key Encryption).
141+
///
142+
/// Returns the decrypted plaintext as a `SecretBox`.
143+
pub fn hpke_open(
144+
priv_key: &PrivateKey,
145+
enc: &[u8],
146+
ciphertext: &[u8],
147+
aad: &[u8],
148+
algo: &HpkeAlgorithm,
149+
) -> Result<SecretBox, Error> {
150+
clear_stack_on_return(CLEAR_STACK_PAGES, || {
151+
priv_key.hpke_open_internal(enc, ciphertext, aad, algo)
152+
})
153+
}
154+
155+
/// Encrypts a plaintext using HPKE (Hybrid Public Key Encryption).
156+
///
157+
/// Returns a tuple containing the encapsulated key and the ciphertext.
158+
pub fn hpke_seal(
159+
pub_key: &PublicKey,
160+
plaintext: &SecretBox,
161+
aad: &[u8],
162+
algo: &HpkeAlgorithm,
163+
) -> Result<(Vec<u8>, Vec<u8>), Error> {
164+
clear_stack_on_return(CLEAR_STACK_PAGES, || {
165+
pub_key.hpke_seal_internal(plaintext, aad, algo)
166+
})
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
use crate::algorithms::{AeadAlgorithm, KdfAlgorithm};
173+
use bssl_crypto::hpke;
174+
175+
#[test]
176+
fn test_decaps_wrapper() {
177+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
178+
let (pk_r, sk_r) = generate_keypair(kem_algo).expect("KEM generation failed");
179+
180+
let hpke_kem = hpke::Kem::X25519HkdfSha256;
181+
let hpke_kdf = hpke::Kdf::HkdfSha256;
182+
let hpke_aead = hpke::Aead::Aes256Gcm;
183+
let params = hpke::Params::new(hpke_kem, hpke_kdf, hpke_aead);
184+
185+
let (_sender_ctx, enc) = hpke::SenderContext::new(&params, pk_r.as_bytes(), b"")
186+
.expect("HPKE setup sender failed");
187+
188+
let result = decaps(&sk_r, &enc).expect("Decaps wrapper failed");
189+
assert_eq!(result.as_slice().len(), 32);
190+
}
191+
192+
#[test]
193+
fn test_decaps_unsupported() {
194+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
195+
let (_pk_r, sk_r) = generate_keypair(kem_algo).expect("KEM generation failed");
196+
197+
let enc = [0u8; 32];
198+
let algo = HpkeAlgorithm {
199+
kem: KemAlgorithm::Unspecified as i32,
200+
kdf: KdfAlgorithm::HkdfSha256 as i32,
201+
aead: AeadAlgorithm::Aes256Gcm as i32,
202+
};
203+
204+
let result = hpke_open(&sk_r, &enc, &[], &[], &algo);
205+
assert!(matches!(result, Err(Error::UnsupportedAlgorithm)));
206+
}
207+
208+
#[test]
209+
fn test_hpke_open_success() {
210+
let hpke_algo = HpkeAlgorithm {
211+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
212+
kdf: KdfAlgorithm::HkdfSha256 as i32,
213+
aead: AeadAlgorithm::Aes256Gcm as i32,
214+
};
215+
216+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
217+
218+
let (pk_r, sk_r) = generate_keypair(kem_algo).expect("HPKE generation failed");
219+
220+
let pt = b"hello world";
221+
let aad = b"additional data";
222+
let info = b"";
223+
224+
let hpke_kem = hpke::Kem::X25519HkdfSha256;
225+
let hpke_kdf = hpke::Kdf::HkdfSha256;
226+
let hpke_aead = hpke::Aead::Aes256Gcm;
227+
let params = hpke::Params::new(hpke_kem, hpke_kdf, hpke_aead);
228+
229+
let (mut sender_ctx, enc) = hpke::SenderContext::new(&params, pk_r.as_bytes(), info)
230+
.expect("HPKE setup sender failed");
231+
let ciphertext = sender_ctx.seal(pt, aad);
232+
233+
let decrypted =
234+
hpke_open(&sk_r, &enc, &ciphertext, aad, &hpke_algo).expect("Decryption failed");
235+
236+
assert_eq!(decrypted.as_slice(), pt);
237+
}
238+
239+
#[test]
240+
fn test_hpke_open_failure() {
241+
let hpke_algo = HpkeAlgorithm {
242+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
243+
kdf: KdfAlgorithm::HkdfSha256 as i32,
244+
aead: AeadAlgorithm::Aes256Gcm as i32,
245+
};
246+
247+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
248+
249+
let (pk_r, sk_r) = generate_keypair(kem_algo).expect("HPKE generation failed");
250+
251+
let pt = b"hello world";
252+
let aad = b"additional data";
253+
let info = b"";
254+
255+
let hpke_kem = hpke::Kem::X25519HkdfSha256;
256+
let hpke_kdf = hpke::Kdf::HkdfSha256;
257+
let hpke_aead = hpke::Aead::Aes256Gcm;
258+
let params = hpke::Params::new(hpke_kem, hpke_kdf, hpke_aead);
259+
260+
let (mut sender_ctx, enc) = hpke::SenderContext::new(&params, pk_r.as_bytes(), info)
261+
.expect("HPKE setup sender failed");
262+
let mut ciphertext = sender_ctx.seal(pt, aad);
263+
264+
// Tamper with ciphertext
265+
if let Some(byte) = ciphertext.get_mut(0) {
266+
*byte ^= 1;
267+
}
268+
269+
let result = hpke_open(&sk_r, &enc, &ciphertext, aad, &hpke_algo);
270+
assert!(matches!(result, Err(Error::HpkeDecryptionError)));
271+
}
272+
273+
#[test]
274+
fn test_hpke_bad_aad() {
275+
let hpke_algo = HpkeAlgorithm {
276+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
277+
kdf: KdfAlgorithm::HkdfSha256 as i32,
278+
aead: AeadAlgorithm::Aes256Gcm as i32,
279+
};
280+
281+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
282+
283+
let (pk_r, sk_r) = generate_keypair(kem_algo).expect("HPKE generation failed");
284+
285+
let pt = b"hello world";
286+
let aad = b"foo";
287+
let info = b"";
288+
289+
let hpke_kem = hpke::Kem::X25519HkdfSha256;
290+
let hpke_kdf = hpke::Kdf::HkdfSha256;
291+
let hpke_aead = hpke::Aead::Aes256Gcm;
292+
let params = hpke::Params::new(hpke_kem, hpke_kdf, hpke_aead);
293+
294+
let (mut sender_ctx, enc) = hpke::SenderContext::new(&params, pk_r.as_bytes(), info)
295+
.expect("HPKE setup sender failed");
296+
let ciphertext = sender_ctx.seal(pt, aad);
297+
298+
// Tamper with aad
299+
let tampered_aad = b"bar";
300+
301+
let result = hpke_open(&sk_r, &enc, &ciphertext, tampered_aad, &hpke_algo);
302+
assert!(matches!(result, Err(Error::HpkeDecryptionError)));
303+
}
304+
305+
#[test]
306+
fn test_hpke_seal_success() {
307+
let hpke_algo = HpkeAlgorithm {
308+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
309+
kdf: KdfAlgorithm::HkdfSha256 as i32,
310+
aead: AeadAlgorithm::Aes256Gcm as i32,
311+
};
312+
let kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
313+
314+
let (pk_r, sk_r) = generate_keypair(kem_algo).expect("HPKE generation failed");
315+
316+
let pt = SecretBox::new(b"hello world".to_vec());
317+
let aad = b"additional data";
318+
319+
// Seal
320+
let (enc, ciphertext) = hpke_seal(&pk_r, &pt, aad, &hpke_algo).expect("HPKE seal failed");
321+
322+
// Decrypt to verify
323+
let decrypted =
324+
hpke_open(&sk_r, &enc, &ciphertext, aad, &hpke_algo).expect("Decryption failed");
325+
assert_eq!(decrypted.as_slice(), pt.as_slice());
326+
}
327+
328+
#[test]
329+
fn test_generate_kem_success() {
330+
let algo = KemAlgorithm::DhkemX25519HkdfSha256;
331+
let (pub_key, _priv_key) = generate_keypair(algo).expect("KEM generation failed");
332+
assert_eq!(pub_key.as_bytes().len(), 32);
333+
}
334+
335+
#[test]
336+
fn test_generate_hpke_success() {
337+
let algo = KemAlgorithm::DhkemX25519HkdfSha256;
338+
339+
let (pub_key, _priv_key) = generate_keypair(algo).expect("HPKE generation failed");
340+
assert_eq!(pub_key.as_bytes().len(), 32);
341+
}
342+
343+
#[test]
344+
fn test_generate_hpke_unsupported() {
345+
let algo = KemAlgorithm::Unspecified;
346+
347+
let result = generate_keypair(algo);
348+
assert!(matches!(result, Err(Error::UnsupportedAlgorithm)));
349+
}
350+
}

0 commit comments

Comments
 (0)