Skip to main content

trussed_staging/
hpke.rs

1// Copyright (C) Nitrokey GmbH
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4use 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    /// Used only in tests for comparison with the test vectors
109    #[allow(unused)]
110    exporter_secret: [u8; NH],
111    // Our limited version only allows one encryption/decryption
112    // seq: u128,
113}
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    // The AEAD_ID is the last 2 bytes of the X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID
124    #[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    /// Custom non-standard Id
138    #[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        // We don't increment because the simplified API only allows 1 encryption
237        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}
272/// Seal with X25519-HKDF-SHA256-ChaCha8Poly1305 suite
273fn 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
295/// Open with X25519-HKDF-SHA256-ChaCha20Poly1305 suite
296fn 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                // TODO: need to check both secret and public keys
369                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                // TODO: need to check both secret and public keys
394                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    /// Seal with X25519-HKDF-SHA256-ChaCha20Poly1305 suite
535    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    /// Open with X25519-HKDF-SHA256-ChaCha20Poly1305 suite
546    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}