1use crate::StagingBackend;
5
6use trussed::{
7 config::MAX_SERIALIZED_KEY_LENGTH,
8 key,
9 serde_extensions::ExtensionImpl,
10 store::{Filestore, Keystore},
11};
12use trussed_core::types::{Bytes, KeyId, Message};
13use trussed_hpke::*;
14
15type HkdfSha256 = hkdf::Hkdf<sha2::Sha256>;
16type HkdfSha256Extract = hkdf::HkdfExtract<sha2::Sha256>;
17
18use rand_core::{CryptoRng, RngCore};
19use salty::agreement as x25519;
20
21const X25519_KEM_SUITE_ID: &[u8] = b"KEM\x00\x20";
22const X25519_HKDF_SHA256_CHACHA20_POLY1305_HPKE_SUITE_ID: &[u8] = b"HPKE\x00\x20\x00\x01\x00\x03";
23
24fn labeled_extract(
25 suite_id: &[u8],
26 salt: &[u8],
27 label: &[u8],
28 ikm: &[u8],
29) -> (HkdfSha256, [u8; 32]) {
30 let mut extract_ctx = HkdfSha256Extract::new(Some(salt));
31 extract_ctx.input_ikm(b"HPKE-v1");
32 extract_ctx.input_ikm(suite_id);
33 extract_ctx.input_ikm(label);
34 extract_ctx.input_ikm(ikm);
35 let (prk, hkdf) = extract_ctx.finalize();
36 (hkdf, prk.into())
37}
38
39fn labeled_expand(
40 suite_id: &[u8],
41 prk: &HkdfSha256,
42 label: &[u8],
43 info: &[u8],
44 buffer: &mut [u8],
45) -> Result<(), hkdf::InvalidLength> {
46 let Ok(l): Result<u16, _> = buffer.len().try_into() else {
47 return Err(hkdf::InvalidLength);
48 };
49 prk.expand_multi_info(
50 &[&l.to_be_bytes(), b"HPKE-v1", suite_id, label, info],
51 buffer,
52 )
53}
54
55fn extract_and_expand(dh: x25519::SharedSecret, kem_context: &[u8]) -> [u8; 32] {
56 let (prk, _) = labeled_extract(X25519_KEM_SUITE_ID, b"", b"eae_prk", &dh.to_bytes());
57 let mut shr = [0; 32];
58 labeled_expand(
59 X25519_KEM_SUITE_ID,
60 &prk,
61 b"shared_secret",
62 kem_context,
63 &mut shr,
64 )
65 .map_err(|_err| {
66 error!("Length of shr is known to be OK: {_err:?}");
67 })
68 .unwrap();
69 shr
70}
71
72fn encap<R: CryptoRng + RngCore>(
73 pkr: x25519::PublicKey,
74 cspnrg: &mut R,
75) -> ([u8; 32], x25519::PublicKey) {
76 let seed = &mut [0; 32];
77 cspnrg.fill_bytes(seed);
78 let secret = x25519::SecretKey::from_seed(seed);
79 let dh = secret.agree(&pkr);
80 let enc = secret.public();
81
82 let kem_context = &mut [0; 64];
83 kem_context[0..32].copy_from_slice(&enc.to_bytes());
84 kem_context[32..].copy_from_slice(&pkr.to_bytes());
85 let shared_secret = extract_and_expand(dh, kem_context);
86 (shared_secret, enc)
87}
88
89fn decap(enc: x25519::PublicKey, skr: x25519::SecretKey) -> [u8; 32] {
90 let dh = skr.agree(&enc);
91 let kem_context = &mut [0; 64];
92 kem_context[0..32].copy_from_slice(&enc.to_bytes());
93 kem_context[32..].copy_from_slice(&skr.public().to_bytes());
94 extract_and_expand(dh, kem_context)
95}
96
97enum Role {
98 Sender,
99 Receiver,
100}
101
102const MODE_BASE: u8 = 0x00;
103
104#[cfg_attr(test, derive(Clone))]
105struct Context {
106 key: [u8; NK],
107 base_nonce: [u8; NN],
108 #[allow(unused)]
110 exporter_secret: [u8; NH],
111 }
114
115trait Aead:
116 AeadMutInPlace
117 + KeyInit<KeySize = <ChaCha20Poly1305 as KeySizeUser>::KeySize>
118 + AeadCore<
119 NonceSize = <ChaCha20Poly1305 as AeadCore>::NonceSize,
120 TagSize = <ChaCha20Poly1305 as AeadCore>::TagSize,
121 >
122{
123 #[cfg(test)]
125 const AEAD_ID: u16;
126 const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8];
127}
128
129impl Aead for ChaCha20Poly1305 {
130 #[cfg(test)]
131 const AEAD_ID: u16 = 0x0003;
132 const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8] =
133 X25519_HKDF_SHA256_CHACHA20_POLY1305_HPKE_SUITE_ID;
134}
135
136impl Aead for ChaCha8Poly1305 {
137 #[cfg(test)]
139 const AEAD_ID: u16 = 0xFFFE;
140 const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8] = b"HPKE\x00\x20\x00\x01\xFF\xFE";
141}
142
143const NK: usize = 32;
144const NN: usize = 12;
145const NH: usize = 32;
146
147fn key_schedule<T: Aead>(_role: Role, shared_secret: [u8; 32], info: &[u8]) -> Context {
148 let (_, psk_id_hash) = labeled_extract(
149 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
150 b"",
151 b"psk_id_hash",
152 b"",
153 );
154 let (_, info_hash) = labeled_extract(
155 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
156 b"",
157 b"info_hash",
158 info,
159 );
160 let mut key_schedule_context = [0; 65];
161 key_schedule_context[0] = MODE_BASE;
162 key_schedule_context[1..33].copy_from_slice(&psk_id_hash);
163 key_schedule_context[33..].copy_from_slice(&info_hash);
164 let (secret, _) = labeled_extract(
165 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
166 &shared_secret,
167 b"secret",
168 b"",
169 );
170 let mut key = [0; NK];
171 labeled_expand(
172 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
173 &secret,
174 b"key",
175 &key_schedule_context,
176 &mut key,
177 )
178 .map_err(|_err| {
179 error!("KEY is not too large: {_err:?}");
180 })
181 .unwrap();
182 let mut base_nonce = [0; NN];
183 labeled_expand(
184 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
185 &secret,
186 b"base_nonce",
187 &key_schedule_context,
188 &mut base_nonce,
189 )
190 .map_err(|_err| {
191 error!("NONCE is not too large: {_err:?}");
192 })
193 .unwrap();
194 let mut exporter_secret = [0; NH];
195 labeled_expand(
196 T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID,
197 &secret,
198 b"exp",
199 &key_schedule_context,
200 &mut exporter_secret,
201 )
202 .map_err(|_err| {
203 error!("EXP is not too large: {_err:?}");
204 })
205 .unwrap();
206 Context {
207 key,
208 base_nonce,
209 exporter_secret,
210 }
211}
212
213fn setup_base_s<R: CryptoRng + RngCore, T: Aead>(
214 pkr: x25519::PublicKey,
215 info: &[u8],
216 cspnrg: &mut R,
217) -> (x25519::PublicKey, Context) {
218 let (shared_secret, enc) = encap(pkr, cspnrg);
219 (enc, key_schedule::<T>(Role::Sender, shared_secret, info))
220}
221
222fn setup_base_r<T: Aead>(enc: x25519::PublicKey, skr: x25519::SecretKey, info: &[u8]) -> Context {
223 let shared_secret = decap(enc, skr);
224 key_schedule::<T>(Role::Receiver, shared_secret, info)
225}
226
227const TAG_LEN: usize = 16;
228
229use chacha20poly1305::{
230 aead::{AeadCore, AeadMutInPlace, KeyInit, KeySizeUser},
231 ChaCha20Poly1305, ChaCha8Poly1305,
232};
233
234impl Context {
235 fn seal_in_place_detached<T: Aead>(self, aad: &[u8], plaintext: &mut [u8]) -> [u8; TAG_LEN] {
236 let nonce = (&self.base_nonce).into();
238 let mut aead = T::new((&self.key).into());
239 let tag = aead
240 .encrypt_in_place_detached(nonce, aad, plaintext)
241 .map_err(|_err| {
242 error!("Not used to encrypt data too large: {_err:?}");
243 })
244 .unwrap();
245
246 tag.into()
247 }
248
249 fn open_in_place_detached<T: Aead>(
250 self,
251 aad: &[u8],
252 ciphertext: &mut [u8],
253 tag: [u8; TAG_LEN],
254 ) -> Result<(), aead::Error> {
255 let nonce = (&self.base_nonce).into();
256 let mut aead = T::new((&self.key).into());
257 aead.decrypt_in_place_detached(nonce, aad, ciphertext, (&tag).into())
258 }
259}
260
261fn seal<R: CryptoRng + RngCore, T: Aead>(
262 pkr: x25519::PublicKey,
263 info: &[u8],
264 aad: &[u8],
265 plaintext: &mut [u8],
266 csprng: &mut R,
267) -> (x25519::PublicKey, [u8; TAG_LEN]) {
268 let (enc, ctx) = setup_base_s::<_, T>(pkr, info, csprng);
269 let tag = ctx.seal_in_place_detached::<T>(aad, plaintext);
270 (enc, tag)
271}
272fn seal8<R: CryptoRng + RngCore>(
274 pkr: x25519::PublicKey,
275 info: &[u8],
276 aad: &[u8],
277 plaintext: &mut [u8],
278 csprng: &mut R,
279) -> (x25519::PublicKey, [u8; TAG_LEN]) {
280 seal::<R, ChaCha8Poly1305>(pkr, info, aad, plaintext, csprng)
281}
282
283fn open<T: Aead>(
284 enc: x25519::PublicKey,
285 skr: x25519::SecretKey,
286 info: &[u8],
287 aad: &[u8],
288 ciphertext: &mut [u8],
289 tag: [u8; TAG_LEN],
290) -> Result<(), aead::Error> {
291 let ctx = setup_base_r::<T>(enc, skr, info);
292 ctx.open_in_place_detached::<T>(aad, ciphertext, tag)
293}
294
295fn open8(
297 enc: x25519::PublicKey,
298 skr: x25519::SecretKey,
299 info: &[u8],
300 aad: &[u8],
301 ciphertext: &mut [u8],
302 tag: [u8; TAG_LEN],
303) -> Result<(), aead::Error> {
304 open::<ChaCha8Poly1305>(enc, skr, info, aad, ciphertext, tag)
305}
306
307fn load_public_key(
308 key_id: &KeyId,
309 keystore: &mut impl Keystore,
310) -> Result<x25519::PublicKey, trussed_core::Error> {
311 let public_bytes: [u8; 32] = keystore
312 .load_key(key::Secrecy::Public, Some(key::Kind::X255), key_id)?
313 .material
314 .as_slice()
315 .try_into()
316 .map_err(|_| trussed_core::Error::InternalError)?;
317 let public_key = x25519::PublicKey::from(public_bytes);
318 Ok(public_key)
319}
320
321fn load_secret_key(
322 key_id: &KeyId,
323 keystore: &mut impl Keystore,
324) -> Result<x25519::SecretKey, trussed_core::Error> {
325 let secret_bytes: [u8; 32] = keystore
326 .load_key(key::Secrecy::Secret, Some(key::Kind::X255), key_id)?
327 .material
328 .as_slice()
329 .try_into()
330 .map_err(|_| trussed_core::Error::InternalError)?;
331 let secret_key = x25519::SecretKey::from_seed(&secret_bytes);
332 Ok(secret_key)
333}
334
335impl ExtensionImpl<HpkeExtension> for StagingBackend {
336 fn extension_request<P: trussed::Platform>(
337 &mut self,
338 core_ctx: &mut trussed::types::CoreContext,
339 _backend_ctx: &mut Self::Context,
340 request: &<HpkeExtension as trussed_core::serde_extensions::Extension>::Request,
341 resources: &mut trussed::service::ServiceResources<P>,
342 ) -> Result<
343 <HpkeExtension as trussed_core::serde_extensions::Extension>::Reply,
344 trussed_core::Error,
345 > {
346 let filestore = &mut resources.filestore(core_ctx.path.clone());
347 let keystore = &mut resources.keystore(core_ctx.path.clone())?;
348
349 match request {
350 HpkeRequest::Seal(req) => {
351 let mut pt = req.plaintext.clone();
352 let public_key = load_public_key(&req.key, keystore)?;
353 let (pk, tag) = seal8(public_key, &req.info, &req.aad, &mut pt, keystore.rng());
354 let enc = keystore.store_key(
355 req.enc_location,
356 key::Secrecy::Public,
357 key::Kind::X255,
358 &pk.to_bytes(),
359 )?;
360 Ok(HpkeSealReply {
361 enc,
362 ciphertext: pt,
363 tag: tag.into(),
364 }
365 .into())
366 }
367 HpkeRequest::SealKey(req) => {
368 let serialized_key =
370 keystore.load_key(key::Secrecy::Secret, None, &req.key_to_seal)?;
371 let mut message = Message::try_from(&*serialized_key.serialize()).unwrap();
372
373 let public_key = load_public_key(&req.public_key, keystore)?;
374
375 let (pk, tag) = seal8(
376 public_key,
377 &req.info,
378 &req.aad,
379 &mut message,
380 keystore.rng(),
381 );
382
383 message
384 .extend_from_slice(&pk.to_bytes())
385 .map_err(|_| trussed_core::Error::SignDataTooLarge)?;
386 message
387 .extend_from_slice(&tag)
388 .map_err(|_| trussed_core::Error::SignDataTooLarge)?;
389
390 Ok(HpkeSealKeyReply { data: message }.into())
391 }
392 HpkeRequest::SealKeyToFile(req) => {
393 let serialized_key =
395 keystore.load_key(key::Secrecy::Secret, None, &req.key_to_seal)?;
396 let mut message = Bytes::<{ MAX_SERIALIZED_KEY_LENGTH + 32 + 16 }>::try_from(
397 &*serialized_key.serialize(),
398 )
399 .unwrap();
400
401 let public_key = load_public_key(&req.public_key, keystore)?;
402
403 let (pk, tag) = seal8(
404 public_key,
405 &req.info,
406 &req.aad,
407 &mut message,
408 keystore.rng(),
409 );
410
411 message
412 .extend_from_slice(&pk.to_bytes())
413 .map_err(|_| trussed_core::Error::SignDataTooLarge)?;
414 message
415 .extend_from_slice(&tag)
416 .map_err(|_| trussed_core::Error::SignDataTooLarge)?;
417 filestore.write(&req.file, req.location, &message)?;
418
419 Ok(HpkeSealKeyToFileReply {}.into())
420 }
421 HpkeRequest::Open(req) => {
422 let enc = load_public_key(&req.enc_key, keystore)?;
423 let secret_key = load_secret_key(&req.key, keystore)?;
424
425 let mut ct = req.ciphertext.clone();
426 open8(
427 enc,
428 secret_key,
429 &req.info,
430 &req.aad,
431 &mut ct,
432 req.tag.into(),
433 )
434 .map_err(|_| trussed_core::Error::AeadError)?;
435
436 Ok(HpkeOpenReply { plaintext: ct }.into())
437 }
438 HpkeRequest::OpenKey(req) => {
439 let secret_key = load_secret_key(&req.key, keystore)?;
440 let mut ct = req.sealed_key.clone();
441 let (ct, tag) = ct
442 .split_last_chunk_mut()
443 .ok_or(trussed_core::Error::AeadError)?;
444 let (ct, enc_bytes) = ct
445 .split_last_chunk_mut()
446 .ok_or(trussed_core::Error::AeadError)?;
447 let enc = x25519::PublicKey::from(*enc_bytes);
448
449 open8(enc, secret_key, &req.info, &req.aad, ct, *tag)
450 .map_err(|_| trussed_core::Error::AeadError)?;
451
452 let key::Key {
453 flags: _,
454 kind,
455 material,
456 } = key::Key::try_deserialize(ct)?;
457
458 let key =
459 keystore.store_key(req.location, key::Secrecy::Secret, kind, &material)?;
460
461 Ok(HpkeOpenKeyReply { key }.into())
462 }
463 HpkeRequest::OpenKeyFromFile(req) => {
464 let secret_key = load_secret_key(&req.key, keystore)?;
465 let mut ct: Bytes<{ MAX_SERIALIZED_KEY_LENGTH + 32 + 16 }> =
466 filestore.read(&req.sealed_key, req.sealed_location)?;
467 let (ct, tag) = ct
468 .split_last_chunk_mut()
469 .ok_or(trussed_core::Error::AeadError)?;
470 let (ct, enc_bytes) = ct
471 .split_last_chunk_mut()
472 .ok_or(trussed_core::Error::AeadError)?;
473 let enc = x25519::PublicKey::from(*enc_bytes);
474
475 open8(enc, secret_key, &req.info, &req.aad, ct, *tag)
476 .map_err(|_| trussed_core::Error::AeadError)?;
477
478 let key::Key {
479 flags: _,
480 kind,
481 material,
482 } = key::Key::try_deserialize(ct)?;
483
484 let key = keystore.store_key(
485 req.unsealed_location,
486 key::Secrecy::Secret,
487 kind,
488 &material,
489 )?;
490
491 Ok(HpkeOpenKeyFromFileReply { key }.into())
492 }
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use core::num::NonZeroU32;
500
501 use hex_literal::hex;
502
503 use super::*;
504
505 struct TestRng<'a>(&'a [u8]);
506 impl CryptoRng for TestRng<'_> {}
507 impl RngCore for TestRng<'_> {
508 fn next_u32(&mut self) -> u32 {
509 let (value, rem) = self.0.split_first_chunk().unwrap();
510 self.0 = rem;
511 u32::from_be_bytes(*value)
512 }
513 fn next_u64(&mut self) -> u64 {
514 let (value, rem) = self.0.split_first_chunk().unwrap();
515 self.0 = rem;
516 u64::from_be_bytes(*value)
517 }
518
519 fn fill_bytes(&mut self, dest: &mut [u8]) {
520 let (value, rem) = self.0.split_at(dest.len());
521 self.0 = rem;
522 dest.copy_from_slice(value);
523 }
524 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
525 if self.0.len() < dest.len() {
526 let error_code: NonZeroU32 = rand_core::Error::CUSTOM_START.try_into().unwrap();
527 return Err(rand_core::Error::from(error_code));
528 }
529 self.fill_bytes(dest);
530 Ok(())
531 }
532 }
533
534 fn seal20<R: CryptoRng + RngCore>(
536 pkr: x25519::PublicKey,
537 info: &[u8],
538 aad: &[u8],
539 plaintext: &mut [u8],
540 csprng: &mut R,
541 ) -> (x25519::PublicKey, [u8; TAG_LEN]) {
542 seal::<R, ChaCha20Poly1305>(pkr, info, aad, plaintext, csprng)
543 }
544
545 fn open20(
547 enc: x25519::PublicKey,
548 skr: x25519::SecretKey,
549 info: &[u8],
550 aad: &[u8],
551 ciphertext: &mut [u8],
552 tag: [u8; TAG_LEN],
553 ) -> Result<(), aead::Error> {
554 open::<ChaCha20Poly1305>(enc, skr, info, aad, ciphertext, tag)
555 }
556
557 #[allow(non_snake_case)]
558 #[test]
559 fn chacha20() {
560 let info = hex!("4f6465206f6e2061204772656369616e2055726e");
561 let pkEm = hex!("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a");
562 let skEm = hex!("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600");
563 let alice_sk = x25519::SecretKey::from_seed(&skEm);
564 assert_eq!(pkEm, alice_sk.public().to_bytes());
565 let pkRm = hex!("4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a");
566 let skRm = hex!("8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb");
567 let bob_sk = x25519::SecretKey::from_seed(&skRm);
568 assert_eq!(pkRm, bob_sk.public().to_bytes());
569 let expected_shared_secret =
570 hex!("0bbe78490412b4bbea4812666f7916932b828bba79942424abb65244930d69a7");
571 let (shared_secret, enc) = encap(bob_sk.public(), &mut TestRng(&skEm));
572 assert_eq!(enc.to_bytes(), pkEm);
573 assert_eq!(shared_secret, expected_shared_secret);
574
575 assert_eq!(
576 decap(alice_sk.public(), bob_sk.clone()),
577 expected_shared_secret
578 );
579 let (enc, ctx) =
580 setup_base_s::<_, ChaCha20Poly1305>(bob_sk.public(), &info, &mut TestRng(&skEm));
581 assert_eq!(enc.to_bytes(), pkEm);
582 assert_eq!(
583 ctx.key,
584 hex!("ad2744de8e17f4ebba575b3f5f5a8fa1f69c2a07f6e7500bc60ca6e3e3ec1c91")
585 );
586 assert_eq!(ctx.base_nonce, hex!("5c4d98150661b848853b547f"));
587 assert_eq!(
588 ctx.exporter_secret,
589 hex!("a3b010d4994890e2c6968a36f64470d3c824c8f5029942feb11e7a74b2921922")
590 );
591
592 let pt = hex!("4265617574792069732074727574682c20747275746820626561757479");
593 let mut buffer = pt;
594 let aad = hex!("436f756e742d30");
595 let ct = hex!("1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b");
596 let expected_tag = hex!("60b4db21993c62ce81883d2dd1b51a28");
597
598 let (enc, tag) = seal20(
599 bob_sk.public(),
600 &info,
601 &aad,
602 &mut buffer,
603 &mut TestRng(&skEm),
604 );
605 assert_eq!(enc.to_bytes(), pkEm);
606 assert_eq!(buffer, ct);
607 assert_eq!(tag, expected_tag);
608 open20(enc, bob_sk, &info, &aad, &mut buffer, tag).unwrap();
609 assert_eq!(buffer, pt);
610 }
611
612 const X25519_KEM_ID: u16 = 0x0020;
613 const HKDF_SHA256_KDF_ID: u16 = 0x0001;
614 fn assert_suite_id<T: Aead>() {
615 let calculated_id: Vec<u8> = b"HPKE"
616 .iter()
617 .copied()
618 .chain(X25519_KEM_ID.to_be_bytes())
619 .chain(HKDF_SHA256_KDF_ID.to_be_bytes())
620 .chain(T::AEAD_ID.to_be_bytes())
621 .collect();
622 assert_eq!(T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, &calculated_id);
623 }
624
625 #[test]
626 fn ids() {
627 let calculated_id: Vec<u8> = b"KEM"
628 .iter()
629 .copied()
630 .chain(X25519_KEM_ID.to_be_bytes())
631 .collect();
632 assert_eq!(X25519_KEM_SUITE_ID, &calculated_id);
633
634 assert_suite_id::<ChaCha20Poly1305>();
635 assert_suite_id::<ChaCha8Poly1305>();
636 }
637}