Skip to main content

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}