aranya_crypto/apq.rs
1//! Cryptography code for [APQ].
2//!
3//! [APQ]: https://git.spideroak-inc.com/spideroak-inc/apq
4
5#![forbid(unsafe_code)]
6#![cfg(feature = "apq")]
7#![cfg_attr(docsrs, doc(cfg(feature = "apq")))]
8
9use core::{cell::OnceCell, fmt, iter, ops::Add, result::Result};
10
11use serde::{Deserialize, Serialize};
12use siphasher::sip128::SipHasher24;
13use spideroak_crypto::{
14 aead::{Aead, BufferTooSmallError, KeyData, OpenError, SealError},
15 csprng::{Csprng, Random},
16 generic_array::{ArrayLength, GenericArray},
17 hex::ToHex as _,
18 import::{Import, ImportError},
19 kem::DecapKey as _,
20 keys::PublicKey as _,
21 signer::{SigningKey as _, VerifyingKey as _},
22 typenum::{Sum, U64},
23 zeroize::{Zeroize as _, ZeroizeOnDrop},
24};
25use zerocopy::{
26 ByteEq, Immutable, IntoBytes, KnownLayout, Unaligned,
27 byteorder::{BE, U32},
28};
29
30use crate::{
31 aranya::{Encap, Signature},
32 ciphersuite::{CipherSuite, CipherSuiteExt as _},
33 error::Error,
34 hpke::{self, Mode},
35 id::{IdError, custom_id},
36 misc::{ciphertext, kem_key, signing_key},
37};
38
39/// A sender's identity.
40pub struct Sender<'a, CS: CipherSuite> {
41 /// The sender's public key.
42 pub enc_key: &'a SenderPublicKey<CS>,
43 /// The sender's verifying key.
44 pub sign_key: &'a SenderVerifyingKey<CS>,
45}
46
47/// The current APQ version.
48#[derive(Copy, Clone, Debug, Default)]
49pub struct Version(u32);
50
51impl Version {
52 /// Creates a new version.
53 pub const fn new(version: u32) -> Self {
54 Self(version)
55 }
56
57 /// Returns the version as a `u32`.
58 pub const fn as_u32(self) -> u32 {
59 self.0
60 }
61
62 const fn to_be_bytes(self) -> [u8; 4] {
63 self.0.to_be_bytes()
64 }
65}
66
67/// The APQ topic being used.
68#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
69pub struct Topic([u8; 16]);
70
71impl Topic {
72 /// Creates a new topic.
73 pub fn new<T: AsRef<[u8]>>(topic: T) -> Self {
74 let d = SipHasher24::new().hash(topic.as_ref());
75 Self(d.as_bytes())
76 }
77
78 /// Converts itself into its byte representation.
79 pub fn as_bytes(&self) -> &[u8; 16] {
80 &self.0
81 }
82
83 /// Converts itself into its byte representation.
84 pub fn to_bytes(self) -> [u8; 16] {
85 self.0
86 }
87}
88
89impl AsRef<[u8]> for Topic {
90 fn as_ref(&self) -> &[u8] {
91 &self.0[..]
92 }
93}
94
95impl From<[u8; 16]> for Topic {
96 fn from(val: [u8; 16]) -> Self {
97 Self(val)
98 }
99}
100
101impl From<Topic> for [u8; 16] {
102 fn from(topic: Topic) -> [u8; 16] {
103 topic.0
104 }
105}
106
107impl fmt::Display for Topic {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "{}", self.0.to_hex())
110 }
111}
112
113custom_id! {
114 /// Uniquely identifies a [`TopicKey`].
115 pub struct TopicKeyId;
116}
117
118/// A [symmetric key] used to encrypt queue messages for
119/// a particular topic.
120///
121/// [symmetric key]: https://git.spideroak-inc.com/spideroak-inc/aranya-docs/blob/main/src/apq.md#topickey
122pub struct TopicKey<CS: CipherSuite> {
123 // TopicKey is quite similar to GroupKey. However, unlike
124 // GroupKey, we do not compute the key from the seed each
125 // time we encrypt some data. Instead, we compute the key
126 // when creating a TopicKey.
127 //
128 // We do this because APQ has a higher throughput than
129 // Aranya, so recomputing the key each time could be too
130 // expensive.
131 //
132 // The downside is that we still have to keep the seed in
133 // memory alongside the key in case we need to send the seed
134 // to a receiver. So, each TopicKey ends up being ~twice as
135 // large and we have to handle two pieces of key material.
136 key: <CS::Aead as Aead>::Key,
137 seed: [u8; 64],
138 id: OnceCell<Result<TopicKeyId, IdError>>,
139}
140
141impl<CS: CipherSuite> ZeroizeOnDrop for TopicKey<CS> {}
142impl<CS: CipherSuite> Drop for TopicKey<CS> {
143 fn drop(&mut self) {
144 self.seed.zeroize();
145 }
146}
147
148impl<CS: CipherSuite> Clone for TopicKey<CS> {
149 fn clone(&self) -> Self {
150 Self {
151 key: self.key.clone(),
152 seed: self.seed,
153 id: OnceCell::new(),
154 }
155 }
156}
157
158impl<CS: CipherSuite> TopicKey<CS> {
159 /// Creates a new, random `TopicKey`.
160 pub fn new<R: Csprng>(rng: R, version: Version, topic: &Topic) -> Result<Self, Error> {
161 Self::from_seed(Random::random(rng), version, topic)
162 }
163
164 /// Uniquely identifies the [`TopicKey`].
165 ///
166 /// Two keys with the same ID are the same key.
167 #[inline]
168 pub fn id(&self) -> Result<TopicKeyId, IdError> {
169 self.id
170 .get_or_init(|| {
171 // prk = LabeledExtract(
172 // "TopicKeyId-v1",
173 // {0}^n,
174 // "prk",
175 // seed,
176 // )
177 // TopicKey = LabeledExpand(
178 // "TopicKeyId-v1",
179 // prk,
180 // "id",
181 // {0}^0,
182 // )
183 const DOMAIN: &[u8] = b"TopicKeyId-v1";
184 let prk = CS::labeled_extract(DOMAIN, &[], b"prk", iter::once::<&[u8]>(&self.seed));
185 CS::labeled_expand(DOMAIN, &prk, b"id", [])
186 .map_err(|_| IdError::new("unable to expand PRK"))
187 .map(TopicKeyId::from_bytes)
188 })
189 .clone()
190 }
191
192 /// The size in bytes of the overhead added to plaintexts
193 /// when encrypted.
194 pub const OVERHEAD: usize = CS::Aead::NONCE_SIZE + CS::Aead::OVERHEAD;
195
196 /// Returns the size in bytes of the overhead added to
197 /// plaintexts when encrypted.
198 ///
199 /// Same as [`OVERHEAD`][Self::OVERHEAD].
200 pub const fn overhead(&self) -> usize {
201 Self::OVERHEAD
202 }
203
204 /// Encrypts and authenticates `plaintext`.
205 ///
206 /// The resulting ciphertext is written to `dst`, which must
207 /// be at least [`overhead`][Self::overhead] bytes longer
208 /// than `plaintext.len()`.
209 ///
210 /// # Example
211 ///
212 /// ```rust
213 /// # #[cfg(all(feature = "alloc", not(feature = "trng")))]
214 /// # {
215 /// use aranya_crypto::{
216 /// BaseId, DeviceId, Rng,
217 /// apq::{Sender, SenderSecretKey, SenderSigningKey, Topic, TopicKey, Version},
218 /// default::{DefaultCipherSuite, DefaultEngine},
219 /// };
220 ///
221 /// const VERSION: Version = Version::new(1);
222 /// let topic = Topic::new("SomeTopic");
223 /// const MESSAGE: &[u8] = b"hello, world!";
224 ///
225 /// let ident = Sender {
226 /// enc_key: &SenderSecretKey::<DefaultCipherSuite>::new(Rng)
227 /// .public()
228 /// .expect("sender encryption key should be valid"),
229 /// sign_key: &SenderSigningKey::<DefaultCipherSuite>::new(Rng)
230 /// .public()
231 /// .expect("sender signing key should be valid"),
232 /// };
233 ///
234 /// let key = TopicKey::new(Rng, VERSION, &topic).expect("should not fail");
235 ///
236 /// let ciphertext = {
237 /// let mut dst = vec![0u8; MESSAGE.len() + key.overhead()];
238 /// key.seal_message(Rng, &mut dst, MESSAGE, VERSION, &topic, &ident)
239 /// .expect("should not fail");
240 /// dst
241 /// };
242 /// let plaintext = {
243 /// let mut dst = vec![0u8; ciphertext.len() - key.overhead()];
244 /// key.open_message(&mut dst, &ciphertext, VERSION, &topic, &ident)
245 /// .expect("should not fail");
246 /// dst
247 /// };
248 /// assert_eq!(&plaintext, MESSAGE);
249 /// # }
250 /// ```
251 pub fn seal_message<R: Csprng>(
252 &self,
253 rng: R,
254 dst: &mut [u8],
255 plaintext: &[u8],
256 version: Version,
257 topic: &Topic,
258 ident: &Sender<'_, CS>,
259 ) -> Result<(), Error> {
260 if dst.len() < self.overhead() {
261 // Not enough room in `dst`.
262 return Err(Error::Seal(SealError::BufferTooSmall(BufferTooSmallError(
263 self.overhead().checked_add(plaintext.len()),
264 ))));
265 }
266 // ad = concat(
267 // "apq msg"
268 // suite_ids,
269 // i2osp(version, 4),
270 // topic,
271 // hash(pk(SenderKey)),
272 // hash(pk(SenderSigningKey)),
273 // )
274 let ad = CS::tuple_hash(
275 b"apq msg",
276 [
277 &version.to_be_bytes()[..],
278 &topic.as_bytes()[..],
279 ident.enc_key.id()?.as_bytes(),
280 ident.sign_key.id()?.as_bytes(),
281 ],
282 );
283 let (nonce, out) = dst.split_at_mut(CS::Aead::NONCE_SIZE);
284 rng.fill_bytes(nonce);
285 Ok(CS::Aead::new(&self.key).seal(out, nonce, plaintext, &ad)?)
286 }
287
288 /// Decrypts and authenticates `ciphertext`.
289 ///
290 /// The resulting plaintext is written to `dst`, which must
291 /// be at least as long as the original plaintext (i.e.,
292 /// `ciphertext.len()` - [`overhead`][Self::overhead] bytes
293 /// long).
294 pub fn open_message(
295 &self,
296 dst: &mut [u8],
297 ciphertext: &[u8],
298 version: Version,
299 topic: &Topic,
300 ident: &Sender<'_, CS>,
301 ) -> Result<(), Error> {
302 if ciphertext.len() < self.overhead() {
303 // Can't find the nonce and/or tag, so it's obviously
304 // invalid.
305 return Err(OpenError::Authentication.into());
306 }
307 let (nonce, ciphertext) = ciphertext.split_at(CS::Aead::NONCE_SIZE);
308 // ad = concat(
309 // "apq msg",
310 // suite_ids,
311 // i2osp(version, 4),
312 // topic,
313 // hash(pk(SenderKey)),
314 // hash(pk(SenderSigningKey)),
315 // )
316 let ad = CS::tuple_hash(
317 b"apq msg",
318 [
319 &version.to_be_bytes()[..],
320 &topic.as_bytes()[..],
321 ident.enc_key.id()?.as_bytes(),
322 ident.sign_key.id()?.as_bytes(),
323 ],
324 );
325 Ok(CS::Aead::new(&self.key).open(dst, nonce, ciphertext, &ad)?)
326 }
327
328 fn from_seed(seed: [u8; 64], version: Version, topic: &Topic) -> Result<Self, Error> {
329 let key = Self::derive_key(&seed, version, topic)?;
330 Ok(Self {
331 key,
332 seed,
333 id: OnceCell::new(),
334 })
335 }
336
337 /// Derives a key for [`Self::open`] and [`Self::seal`].
338 ///
339 /// See <https://git.spideroak-inc.com/spideroak-inc/aranya-docs/blob/main/src/apq.md#topickey-generation>
340 fn derive_key(
341 seed: &[u8; 64],
342 version: Version,
343 topic: &Topic,
344 ) -> Result<<CS::Aead as Aead>::Key, Error> {
345 const DOMAIN: &[u8] = b"APQ-v1";
346 // prk = LabeledExtract("APQ-V1", {0}^512, "topic_key_prk", seed)
347 let prk = CS::labeled_extract(DOMAIN, &[], b"topic_key_prk", iter::once::<&[u8]>(seed));
348 // info = concat(
349 // i2osp(version, 4),
350 // topic,
351 // )
352 // key = LabeledExpand("APQ-v1", prk, "topic_key_key", info)
353 let key: KeyData<CS::Aead> = CS::labeled_expand(
354 DOMAIN,
355 &prk,
356 b"topic_key_key",
357 [&version.to_be_bytes(), topic.as_bytes()],
358 )?;
359
360 Ok(<<CS::Aead as Aead>::Key as Import<_>>::import(
361 key.as_bytes(),
362 )?)
363 }
364}
365
366ciphertext!(EncryptedTopicKey, U64, "An encrypted [`TopicKey`].");
367
368signing_key! {
369 /// The private half of a [SenderSigningKey].
370 ///
371 /// [SenderSigningKey]: https://git.spideroak-inc.com/spideroak-inc/aranya-docs/blob/main/src/apq.md#sendersigningkey
372 sk = SenderSigningKey,
373 pk = SenderVerifyingKey,
374 id = SenderSigningKeyId,
375 context = "APQ Sender Signing Key V1",
376}
377
378impl<CS: CipherSuite> SenderSigningKey<CS> {
379 /// Creates a signature over an encoded record.
380 ///
381 /// # Example
382 ///
383 /// ```rust
384 /// # #[cfg(all(feature = "alloc", not(feature = "trng")))]
385 /// # {
386 /// use aranya_crypto::{
387 /// Rng,
388 /// apq::{SenderSigningKey, Topic, Version},
389 /// default::{DefaultCipherSuite, DefaultEngine},
390 /// };
391 ///
392 /// const VERSION: Version = Version::new(1);
393 /// let topic = Topic::new("SomeTopic");
394 /// const RECORD: &[u8] = b"an encoded record";
395 ///
396 /// let sk = SenderSigningKey::<DefaultCipherSuite>::new(Rng);
397 ///
398 /// let sig = sk.sign(VERSION, &topic, RECORD).expect("should not fail");
399 ///
400 /// sk.public()
401 /// .expect("sender signing key should be valid")
402 /// .verify(VERSION, &topic, RECORD, &sig)
403 /// .expect("should not fail");
404 ///
405 /// sk.public()
406 /// .expect("sender signing key should be valid")
407 /// .verify(Version::new(2), &topic, RECORD, &sig)
408 /// .expect_err("should fail: wrong version");
409 ///
410 /// sk.public()
411 /// .expect("sender signing key should be valid")
412 /// .verify(VERSION, &Topic::new("WrongTopic"), RECORD, &sig)
413 /// .expect_err("should fail: wrong topic");
414 ///
415 /// sk.public()
416 /// .expect("sender signing key should be valid")
417 /// .verify(VERSION, &topic, b"wrong", &sig)
418 /// .expect_err("should fail: wrong record");
419 ///
420 /// let wrong_sig = sk
421 /// .sign(
422 /// Version::new(2),
423 /// &Topic::new("AnotherTopic"),
424 /// b"encoded record",
425 /// )
426 /// .expect("should not fail");
427 /// sk.public()
428 /// .expect("sender signing key should be valid")
429 /// .verify(VERSION, &topic, RECORD, &wrong_sig)
430 /// .expect_err("should fail: wrong signature");
431 /// # }
432 /// ```
433 pub fn sign(
434 &self,
435 version: Version,
436 topic: &Topic,
437 record: &[u8],
438 ) -> Result<Signature<CS>, Error> {
439 // message = concat(
440 // "apq record",
441 // suite_ids,
442 // i2osp(version, 4),
443 // topic,
444 // pk(SenderSigningKey),
445 // encode(record),
446 // )
447 let msg = CS::tuple_hash(
448 b"apq record",
449 [
450 &version.to_be_bytes(),
451 &topic.as_bytes()[..],
452 self.public()?.id()?.as_bytes(),
453 record,
454 ],
455 );
456 let sig = self.sk.sign(&msg)?;
457 Ok(Signature(sig))
458 }
459}
460
461impl<CS: CipherSuite> SenderVerifyingKey<CS> {
462 /// Verifies the signature allegedly created over an encoded
463 /// record.
464 pub fn verify(
465 &self,
466 version: Version,
467 topic: &Topic,
468 record: &[u8],
469 sig: &Signature<CS>,
470 ) -> Result<(), Error> {
471 // message = concat(
472 // "apq record",
473 // suite_ids,
474 // i2osp(version, 4),
475 // topic,
476 // pk(SenderSigningKey),
477 // context,
478 // encode(record),
479 // )
480 let msg = CS::tuple_hash(
481 b"apq record",
482 [
483 &version.to_be_bytes(),
484 &topic.as_bytes()[..],
485 self.id()?.as_bytes(),
486 record,
487 ],
488 );
489 Ok(self.pk.verify(&msg, &sig.0)?)
490 }
491}
492
493kem_key! {
494 /// The private half of a [SenderKey].
495 ///
496 /// [SenderKey]: https://git.spideroak-inc.com/spideroak-inc/aranya-docs/blob/main/src/apq.md#senderkey
497 sk = SenderSecretKey,
498 pk = SenderPublicKey,
499 id = SenderKeyId,
500 context = "APQ Sender Secret Key V1",
501}
502
503kem_key! {
504 /// The private half of a [ReceiverKey].
505 ///
506 /// [ReceiverKey]: https://git.spideroak-inc.com/spideroak-inc/aranya-docs/blob/main/src/apq.md#receiverkey
507 sk = ReceiverSecretKey,
508 pk = ReceiverPublicKey,
509 id = ReceiverKeyId,
510 context = "APQ Receiver Secret Key V1",
511}
512
513impl<CS: CipherSuite> ReceiverSecretKey<CS> {
514 /// Decrypts and authenticates a [`TopicKey`] received from
515 /// a peer.
516 pub fn open_topic_key(
517 &self,
518 version: Version,
519 topic: &Topic,
520 pk: &SenderPublicKey<CS>,
521 enc: &Encap<CS>,
522 ciphertext: &EncryptedTopicKey<CS>,
523 ) -> Result<TopicKey<CS>, Error>
524 where
525 <CS::Aead as Aead>::Overhead: Add<U64>,
526 Sum<<CS::Aead as Aead>::Overhead, U64>: ArrayLength,
527 {
528 // ad = concat(
529 // "TopicKeyRotation-v1",
530 // i2osp(version, 4),
531 // topic,
532 // )
533 let ad = TopicKeyRotationInfo {
534 domain: *b"TopicKeyRotation-v1",
535 version: U32::new(version.as_u32()),
536 topic: topic.0,
537 };
538 // ciphertext = HPKE_OneShotOpen(
539 // mode=mode_auth,
540 // skR=sk(ReceiverKey),
541 // pkS=pk(SenderEncKey),
542 // info="TopicKeyRotation",
543 // enc=enc,
544 // ciphertext=ciphertext,
545 // ad=ad,
546 // )
547 let mut ctx =
548 hpke::setup_recv::<CS>(Mode::Auth(&pk.pk), &enc.0, &self.sk, [ad.as_bytes()])?;
549 let mut seed = [0u8; 64];
550 ctx.open(&mut seed, ciphertext.as_bytes(), ad.as_bytes())?;
551 TopicKey::from_seed(seed, version, topic)
552 }
553}
554
555#[repr(C)]
556#[derive(Copy, Clone, Debug, ByteEq, Immutable, IntoBytes, KnownLayout, Unaligned)]
557struct TopicKeyRotationInfo {
558 /// Always "TopicKeyRotation-v1".
559 domain: [u8; 19],
560 version: U32<BE>,
561 /// [`Topic`].
562 topic: [u8; 16],
563}
564
565impl<CS: CipherSuite> ReceiverPublicKey<CS> {
566 /// Encrypts and authenticates the [`TopicKey`] such that it
567 /// can only be decrypted by the holder of the private half
568 /// of the [`ReceiverPublicKey`].
569 ///
570 /// # Example
571 ///
572 /// ```rust
573 /// # #[cfg(all(feature = "alloc", not(feature = "trng")))]
574 /// # {
575 /// use aranya_crypto::{
576 /// BaseId, DeviceId, Rng,
577 /// apq::{ReceiverSecretKey, SenderSecretKey, Topic, TopicKey, Version},
578 /// default::{DefaultCipherSuite, DefaultEngine},
579 /// };
580 ///
581 /// const VERSION: Version = Version::new(1);
582 /// let topic = Topic::new("SomeTopic");
583 ///
584 /// let send_sk = SenderSecretKey::<DefaultCipherSuite>::new(Rng);
585 /// let send_pk = send_sk.public().expect("sender public key should be valid");
586 /// let recv_sk = ReceiverSecretKey::<DefaultCipherSuite>::new(Rng);
587 /// let recv_pk = recv_sk
588 /// .public()
589 /// .expect("receiver public key should be valid");
590 ///
591 /// let key = TopicKey::new(Rng, VERSION, &topic).expect("should not fail");
592 ///
593 /// // The sender encrypts...
594 /// let (enc, mut ciphertext) = recv_pk
595 /// .seal_topic_key(Rng, VERSION, &topic, &send_sk, &key)
596 /// .expect("should not fail");
597 /// // ...and the receiver decrypts.
598 /// let got = recv_sk
599 /// .open_topic_key(VERSION, &topic, &send_pk, &enc, &ciphertext)
600 /// .expect("should not fail");
601 /// assert_eq!(got.id(), key.id());
602 ///
603 /// // Wrong version.
604 /// recv_sk
605 /// .open_topic_key(Version::new(2), &topic, &send_pk, &enc, &ciphertext)
606 /// .err()
607 /// .expect("should fail: wrong version");
608 ///
609 /// // Wrong topic.
610 /// recv_sk
611 /// .open_topic_key(
612 /// VERSION,
613 /// &Topic::new("WrongTopic"),
614 /// &send_pk,
615 /// &enc,
616 /// &ciphertext,
617 /// )
618 /// .err()
619 /// .expect("should fail: wrong topic");
620 /// # }
621 /// ```
622 pub fn seal_topic_key<R: Csprng>(
623 &self,
624 rng: R,
625 version: Version,
626 topic: &Topic,
627 sk: &SenderSecretKey<CS>,
628 key: &TopicKey<CS>,
629 ) -> Result<(Encap<CS>, EncryptedTopicKey<CS>), Error>
630 where
631 <CS::Aead as Aead>::Overhead: Add<U64>,
632 Sum<<CS::Aead as Aead>::Overhead, U64>: ArrayLength,
633 {
634 // ad = concat(
635 // "TopicKeyRotation-v1",
636 // i2osp(version, 4),
637 // topic,
638 // )
639 let ad = TopicKeyRotationInfo {
640 domain: *b"TopicKeyRotation-v1",
641 version: U32::new(version.as_u32()),
642 topic: topic.0,
643 };
644 // (enc, ciphertext) = HPKE_OneShotSeal(
645 // mode=mode_auth,
646 // pkR=pk(ReceiverKey),
647 // skS=sk(SenderKey),
648 // info=ad,
649 // plaintext=seed,
650 // ad=ad,
651 // )
652 let (enc, mut ctx) =
653 hpke::setup_send::<CS, _>(rng, Mode::Auth(&sk.sk), &self.pk, [ad.as_bytes()])?;
654 let mut dst = GenericArray::default();
655 ctx.seal(&mut dst, &key.seed, ad.as_bytes())?;
656 Ok((Encap(enc), EncryptedTopicKey(dst)))
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use spideroak_crypto::{ed25519::Ed25519, import::Import as _, kem::Kem, rust, signer::Signer};
663
664 use super::*;
665 use crate::{default::DhKemP256HkdfSha256, test_util::TestCs};
666
667 type CS = TestCs<
668 rust::Aes256Gcm,
669 rust::Sha256,
670 rust::HkdfSha512,
671 DhKemP256HkdfSha256,
672 rust::HmacSha512,
673 Ed25519,
674 >;
675
676 /// Golden test for [`SenderSigningKey`] IDs.
677 #[test]
678 fn test_sender_signing_key_id() {
679 let tests = [(
680 [
681 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
682 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
683 0x1d, 0x1e, 0x1f, 0x20,
684 ],
685 "CRHzbYEDN4KoXQvJwXT71ywN2PWd1ddemKussnvjQkR5",
686 )];
687
688 for (i, (key_bytes, expected_id)) in tests.iter().enumerate() {
689 let sk = <<CS as CipherSuite>::Signer as Signer>::SigningKey::import(key_bytes)
690 .expect("should import signing key");
691 let sender_signing_key = SenderSigningKey::<CS>::from_inner(sk);
692
693 let got_id = sender_signing_key.id().expect("should compute ID");
694 let expected =
695 SenderSigningKeyId::decode(expected_id).expect("should decode expected ID");
696
697 assert_eq!(got_id, expected, "test case #{i}");
698 }
699 }
700
701 /// Golden test for [`SenderSecretKey`] IDs.
702 #[test]
703 fn test_sender_secret_key_id() {
704 let tests = [(
705 [
706 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
707 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
708 0x1d, 0x1e, 0x1f, 0x20,
709 ],
710 "9omQm4BTYpdZF5GpAz5oqyDGQsRG9q58348AbFudyAoA",
711 )];
712
713 for (i, (key_bytes, expected_id)) in tests.iter().enumerate() {
714 let sk = <<CS as CipherSuite>::Kem as Kem>::DecapKey::import(key_bytes)
715 .expect("should import decap key");
716 let sender_secret_key = SenderSecretKey::<CS>::from_inner(sk);
717
718 let got_id = sender_secret_key.id().expect("should compute ID");
719 let expected = SenderKeyId::decode(expected_id).expect("should decode expected ID");
720
721 assert_eq!(got_id, expected, "test case #{i}");
722 }
723 }
724
725 /// Golden test for [`ReceiverSecretKey`] IDs.
726 #[test]
727 fn test_receiver_secret_key_id() {
728 let tests = [(
729 [
730 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
731 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
732 0x1d, 0x1e, 0x1f, 0x20,
733 ],
734 "CqiuLwPbbDQWKZQP1eLmDdc5mELZrj1h4hAyBHofCMtc",
735 )];
736
737 for (i, (key_bytes, expected_id)) in tests.iter().enumerate() {
738 let sk = <<CS as CipherSuite>::Kem as Kem>::DecapKey::import(key_bytes)
739 .expect("should import decap key");
740 let receiver_secret_key = ReceiverSecretKey::<CS>::from_inner(sk);
741
742 let got_id = receiver_secret_key.id().expect("should compute ID");
743 let expected = ReceiverKeyId::decode(expected_id).expect("should decode expected ID");
744
745 assert_eq!(got_id, expected, "test case #{i}");
746 }
747 }
748}