aranya_daemon_api/
service.rs

1#![allow(clippy::disallowed_macros)] // tarpc uses unreachable
2
3use core::{
4    borrow::Borrow,
5    error, fmt,
6    hash::{Hash, Hasher},
7    net::SocketAddr,
8    ops::Deref,
9    time::Duration,
10};
11use std::collections::hash_map::{self, HashMap};
12
13use anyhow::bail;
14pub use aranya_crypto::aqc::CipherSuiteId;
15use aranya_crypto::{
16    aqc::{BidiPskId, UniPskId},
17    custom_id,
18    default::{DefaultCipherSuite, DefaultEngine},
19    subtle::{Choice, ConstantTimeEq},
20    zeroize::{Zeroize, ZeroizeOnDrop},
21    Id,
22};
23use aranya_util::Addr;
24use buggy::Bug;
25pub use semver::Version;
26use serde::{Deserialize, Serialize};
27use tracing::error;
28
29/// CE = Crypto Engine
30pub type CE = DefaultEngine;
31/// CS = Cipher Suite
32pub type CS = DefaultCipherSuite;
33
34/// An error returned by the API.
35// TODO: enum?
36#[derive(Serialize, Deserialize, Debug)]
37pub struct Error(String);
38
39impl From<Bug> for Error {
40    fn from(err: Bug) -> Self {
41        error!(?err);
42        Self(format!("{err:?}"))
43    }
44}
45
46impl From<anyhow::Error> for Error {
47    fn from(err: anyhow::Error) -> Self {
48        error!(?err);
49        Self(format!("{err:?}"))
50    }
51}
52
53impl From<semver::Error> for Error {
54    fn from(err: semver::Error) -> Self {
55        error!(?err);
56        Self(format!("{err:?}"))
57    }
58}
59
60impl From<aranya_crypto::id::IdError> for Error {
61    fn from(err: aranya_crypto::id::IdError) -> Self {
62        error!(%err);
63        Self(err.to_string())
64    }
65}
66
67impl fmt::Display for Error {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        self.0.fmt(f)
70    }
71}
72
73impl error::Error for Error {}
74
75pub type Result<T, E = Error> = core::result::Result<T, E>;
76
77custom_id! {
78    /// The Device ID.
79    pub struct DeviceId;
80}
81
82custom_id! {
83    /// The Team ID (a.k.a Graph ID).
84    pub struct TeamId;
85}
86
87custom_id! {
88    /// An AQC label ID.
89    pub struct LabelId;
90}
91
92custom_id! {
93    /// An AQC bidi channel ID.
94    pub struct AqcBidiChannelId;
95}
96
97custom_id! {
98    /// An AQC uni channel ID.
99    pub struct AqcUniChannelId;
100}
101
102/// A device's public key bundle.
103#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
104pub struct KeyBundle {
105    pub identity: Vec<u8>,
106    pub signing: Vec<u8>,
107    pub encoding: Vec<u8>,
108}
109
110/// A device's role on the team.
111#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
112pub enum Role {
113    Owner,
114    Admin,
115    Operator,
116    Member,
117}
118
119/// A configuration for creating or adding a team to a daemon.
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
121pub struct TeamConfig {
122    // TODO(nikki): any fields added to this should be public
123}
124
125/// A device's network identifier.
126#[derive(Clone, Debug, Serialize, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
127pub struct NetIdentifier(pub String);
128
129impl Borrow<str> for NetIdentifier {
130    #[inline]
131    fn borrow(&self) -> &str {
132        &self.0
133    }
134}
135
136impl<T> AsRef<T> for NetIdentifier
137where
138    T: ?Sized,
139    <Self as Deref>::Target: AsRef<T>,
140{
141    #[inline]
142    fn as_ref(&self) -> &T {
143        self.deref().as_ref()
144    }
145}
146
147impl Deref for NetIdentifier {
148    type Target = str;
149
150    #[inline]
151    fn deref(&self) -> &Self::Target {
152        &self.0
153    }
154}
155
156impl fmt::Display for NetIdentifier {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        self.0.fmt(f)
159    }
160}
161
162/// A serialized command for AQC.
163pub type AqcCtrl = Vec<Box<[u8]>>;
164
165/// A secret.
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct Secret(Box<[u8]>);
168
169impl Secret {
170    /// Provides access to the raw secret bytes.
171    #[inline]
172    pub fn raw_secret_bytes(&self) -> &[u8] {
173        &self.0
174    }
175}
176
177impl<T> From<T> for Secret
178where
179    T: Into<Box<[u8]>>,
180{
181    fn from(value: T) -> Self {
182        Self(value.into())
183    }
184}
185
186impl ConstantTimeEq for Secret {
187    fn ct_eq(&self, other: &Self) -> Choice {
188        self.0.ct_eq(&other.0)
189    }
190}
191
192impl ZeroizeOnDrop for Secret {}
193impl Drop for Secret {
194    fn drop(&mut self) {
195        self.0.zeroize()
196    }
197}
198
199macro_rules! psk_map {
200    (
201        $(#[$meta:meta])*
202        $vis:vis struct $name:ident(PskMap<$psk:ty>);
203    ) => {
204        $(#[$meta])*
205        #[derive(Clone, Debug, Serialize, Deserialize)]
206        #[cfg_attr(test, derive(PartialEq))]
207        $vis struct $name {
208            id: Id,
209            psks: HashMap<CsId, $psk>
210        }
211
212        impl $name {
213            /// Returns the number of PSKs.
214            pub fn len(&self) -> usize {
215                self.psks.len()
216            }
217
218            /// Reports whether `self` is empty.
219            pub fn is_empty(&self) -> bool {
220                self.psks.is_empty()
221            }
222
223            /// Returns the channel ID.
224            pub fn channel_id(&self) -> &Id {
225                &self.id
226            }
227
228            /// Returns the PSK for the cipher suite.
229            pub fn get(&self, suite: CipherSuiteId) -> Option<&$psk> {
230                self.psks.get(&CsId(suite))
231            }
232
233            /// Creates a PSK map from a function that generates
234            /// a PSK for a cipher suite.
235            pub fn try_from_fn<I, E, F>(id: I, mut f: F) -> anyhow::Result<Self>
236            where
237                I: Into<Id>,
238                anyhow::Error: From<E>,
239                F: FnMut(CipherSuiteId) -> Result<$psk, E>,
240            {
241                let id = id.into();
242                let mut psks = HashMap::new();
243                for &suite in CipherSuiteId::all() {
244                    let psk = f(suite)?;
245                    if !bool::from(psk.identity().channel_id().into_id().ct_eq(&id)) {
246                        bail!("PSK identity does not match channel ID");
247                    }
248                    psks.insert(CsId(suite), psk);
249                }
250                Ok(Self { id, psks })
251            }
252        }
253
254        impl IntoIterator for $name {
255            type Item = (CipherSuiteId, $psk);
256            type IntoIter = IntoPsks<$psk>;
257
258            fn into_iter(self) -> Self::IntoIter {
259                IntoPsks {
260                    iter: self.psks.into_iter(),
261                }
262            }
263        }
264
265        #[cfg(test)]
266        impl tests::PskMap for $name {
267            type Psk = $psk;
268
269            fn new() -> Self {
270                Self {
271                    // TODO
272                    id: Id::default(),
273                    psks: HashMap::new(),
274                }
275            }
276
277            fn len(&self) -> usize {
278                self.psks.len()
279            }
280
281            fn insert(&mut self, psk: Self::Psk) {
282                let suite = psk.cipher_suite();
283                let opt = self.psks.insert(CsId(suite), psk);
284                assert!(opt.is_none());
285            }
286        }
287    };
288}
289psk_map! {
290    /// An injective mapping of PSKs to cipher suites for
291    /// a single bidirectional channel.
292    pub struct AqcBidiPsks(PskMap<AqcBidiPsk>);
293}
294
295psk_map! {
296    /// An injective mapping of PSKs to cipher suites for
297    /// a single unidirectional channel.
298    pub struct AqcUniPsks(PskMap<AqcUniPsk>);
299}
300
301/// An injective mapping of PSKs to cipher suites for a single
302/// bidirectional or unidirectional channel.
303#[derive(Clone, Debug, Serialize, Deserialize)]
304pub enum AqcPsks {
305    Bidi(AqcBidiPsks),
306    Uni(AqcUniPsks),
307}
308
309impl IntoIterator for AqcPsks {
310    type IntoIter = AqcPsksIntoIter;
311    type Item = <Self::IntoIter as Iterator>::Item;
312
313    fn into_iter(self) -> Self::IntoIter {
314        match self {
315            AqcPsks::Bidi(psks) => AqcPsksIntoIter::Bidi(psks.into_iter()),
316            AqcPsks::Uni(psks) => AqcPsksIntoIter::Uni(psks.into_iter()),
317        }
318    }
319}
320
321/// An iterator over an AQC channel's PSKs.
322#[derive(Debug)]
323pub enum AqcPsksIntoIter {
324    Bidi(IntoPsks<AqcBidiPsk>),
325    Uni(IntoPsks<AqcUniPsk>),
326}
327
328impl Iterator for AqcPsksIntoIter {
329    type Item = (CipherSuiteId, AqcPsk);
330    fn next(&mut self) -> Option<Self::Item> {
331        match self {
332            AqcPsksIntoIter::Bidi(it) => it.next().map(|(s, k)| (s, AqcPsk::Bidi(k))),
333            AqcPsksIntoIter::Uni(it) => it.next().map(|(s, k)| (s, AqcPsk::Uni(k))),
334        }
335    }
336}
337
338/// An iterator over an AQC channel's PSKs.
339#[derive(Debug)]
340pub struct IntoPsks<V> {
341    iter: hash_map::IntoIter<CsId, V>,
342}
343
344impl<V> Iterator for IntoPsks<V> {
345    type Item = (CipherSuiteId, V);
346
347    fn next(&mut self) -> Option<Self::Item> {
348        self.iter.next().map(|(k, v)| (k.0, v))
349    }
350}
351
352// TODO(eric): Get rid of this once `CipherSuiteId` implements
353// `Hash`.
354#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
355#[serde(transparent)]
356struct CsId(CipherSuiteId);
357
358impl Hash for CsId {
359    fn hash<H: Hasher>(&self, state: &mut H) {
360        self.0.to_bytes().hash(state);
361    }
362}
363
364/// An AQC PSK.
365#[derive(Clone, Debug, Serialize, Deserialize)]
366pub enum AqcPsk {
367    /// Bidirectional.
368    Bidi(AqcBidiPsk),
369    /// Unidirectional.
370    Uni(AqcUniPsk),
371}
372
373impl AqcPsk {
374    /// Returns the PSK identity.
375    #[inline]
376    pub fn identity(&self) -> AqcPskId {
377        match self {
378            Self::Bidi(psk) => AqcPskId::Bidi(psk.identity),
379            Self::Uni(psk) => AqcPskId::Uni(psk.identity),
380        }
381    }
382
383    /// Returns the PSK cipher suite.
384    #[inline]
385    pub fn cipher_suite(&self) -> CipherSuiteId {
386        self.identity().cipher_suite()
387    }
388
389    /// Returns the PSK secret.
390    #[inline]
391    pub fn secret(&self) -> &[u8] {
392        match self {
393            Self::Bidi(psk) => psk.secret.raw_secret_bytes(),
394            Self::Uni(psk) => match &psk.secret {
395                Directed::Send(secret) | Directed::Recv(secret) => secret.raw_secret_bytes(),
396            },
397        }
398    }
399}
400
401impl From<AqcBidiPsk> for AqcPsk {
402    fn from(psk: AqcBidiPsk) -> Self {
403        Self::Bidi(psk)
404    }
405}
406
407impl From<AqcUniPsk> for AqcPsk {
408    fn from(psk: AqcUniPsk) -> Self {
409        Self::Uni(psk)
410    }
411}
412
413impl ConstantTimeEq for AqcPsk {
414    fn ct_eq(&self, other: &Self) -> Choice {
415        // It's fine that matching discriminants isn't constant
416        // time since it isn't secret data.
417        match (self, other) {
418            (Self::Bidi(lhs), Self::Bidi(rhs)) => lhs.ct_eq(rhs),
419            (Self::Uni(lhs), Self::Uni(rhs)) => lhs.ct_eq(rhs),
420            _ => Choice::from(0u8),
421        }
422    }
423}
424
425/// An AQC bidirectional channel PSK.
426#[derive(Clone, Debug, Serialize, Deserialize)]
427pub struct AqcBidiPsk {
428    /// The PSK identity.
429    pub identity: BidiPskId,
430    /// The PSK's secret.
431    pub secret: Secret,
432}
433
434impl AqcBidiPsk {
435    fn identity(&self) -> &BidiPskId {
436        &self.identity
437    }
438
439    #[cfg(test)]
440    fn cipher_suite(&self) -> CipherSuiteId {
441        self.identity.cipher_suite()
442    }
443}
444
445impl ConstantTimeEq for AqcBidiPsk {
446    fn ct_eq(&self, other: &Self) -> Choice {
447        let id = self.identity.ct_eq(&other.identity);
448        let secret = self.secret.ct_eq(&other.secret);
449        id & secret
450    }
451}
452
453impl ZeroizeOnDrop for AqcBidiPsk {}
454
455/// An AQC unidirectional PSK.
456#[derive(Clone, Debug, Serialize, Deserialize)]
457pub struct AqcUniPsk {
458    /// The PSK identity.
459    pub identity: UniPskId,
460    /// The PSK's secret.
461    pub secret: Directed<Secret>,
462}
463
464impl AqcUniPsk {
465    fn identity(&self) -> &UniPskId {
466        &self.identity
467    }
468
469    #[cfg(test)]
470    fn cipher_suite(&self) -> CipherSuiteId {
471        self.identity.cipher_suite()
472    }
473}
474
475impl ConstantTimeEq for AqcUniPsk {
476    fn ct_eq(&self, other: &Self) -> Choice {
477        let id = self.identity.ct_eq(&other.identity);
478        let secret = self.secret.ct_eq(&other.secret);
479        id & secret
480    }
481}
482
483impl ZeroizeOnDrop for AqcUniPsk {}
484
485/// Either send only or receive only.
486#[derive(Clone, Debug, Serialize, Deserialize)]
487pub enum Directed<T> {
488    /// Send only.
489    Send(T),
490    /// Receive only.
491    Recv(T),
492}
493
494impl<T: ConstantTimeEq> ConstantTimeEq for Directed<T> {
495    fn ct_eq(&self, other: &Self) -> Choice {
496        // It's fine that matching discriminants isn't constant
497        // time since the direction isn't secret data.
498        match (self, other) {
499            (Self::Send(lhs), Self::Send(rhs)) => lhs.ct_eq(rhs),
500            (Self::Recv(lhs), Self::Recv(rhs)) => lhs.ct_eq(rhs),
501            _ => Choice::from(0u8),
502        }
503    }
504}
505
506/// An AQC PSK identity.
507#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
508pub enum AqcPskId {
509    /// A bidirectional PSK.
510    Bidi(BidiPskId),
511    /// A unidirectional PSK.
512    Uni(UniPskId),
513}
514
515impl AqcPskId {
516    /// Returns the unique channel ID.
517    pub fn channel_id(&self) -> Id {
518        match self {
519            Self::Bidi(v) => (*v.channel_id()).into(),
520            Self::Uni(v) => (*v.channel_id()).into(),
521        }
522    }
523
524    /// Returns the cipher suite.
525    pub fn cipher_suite(&self) -> CipherSuiteId {
526        match self {
527            Self::Bidi(v) => v.cipher_suite(),
528            Self::Uni(v) => v.cipher_suite(),
529        }
530    }
531
532    /// Converts the ID to its byte encoding.
533    pub fn as_bytes(&self) -> &[u8; 34] {
534        match self {
535            Self::Bidi(v) => v.as_bytes(),
536            Self::Uni(v) => v.as_bytes(),
537        }
538    }
539}
540
541/// Configuration values for syncing with a peer
542#[derive(Clone, Debug, Serialize, Deserialize)]
543pub struct SyncPeerConfig {
544    /// The interval at which syncing occurs
545    pub interval: Duration,
546    /// Determines if a peer should be synced with immediately after they're added
547    pub sync_now: bool,
548}
549
550/// Valid channel operations for a label assignment.
551#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
552pub enum ChanOp {
553    /// The device can only receive data in channels with this
554    /// label.
555    RecvOnly,
556    /// The device can only send data in channels with this
557    /// label.
558    SendOnly,
559    /// The device can send and receive data in channels with this
560    /// label.
561    SendRecv,
562}
563
564/// A label.
565#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
566pub struct Label {
567    pub id: LabelId,
568    pub name: String,
569}
570
571#[tarpc::service]
572pub trait DaemonApi {
573    /// Returns the daemon's version.
574    async fn version() -> Result<Version>;
575
576    /// Gets local address the Aranya sync server is bound to.
577    async fn aranya_local_addr() -> Result<SocketAddr>;
578
579    /// Gets the public key bundle for this device
580    async fn get_key_bundle() -> Result<KeyBundle>;
581
582    /// Gets the public device id.
583    async fn get_device_id() -> Result<DeviceId>;
584
585    /// Adds the peer for automatic periodic syncing.
586    async fn add_sync_peer(addr: Addr, team: TeamId, config: SyncPeerConfig) -> Result<()>;
587
588    /// Sync with peer immediately.
589    async fn sync_now(addr: Addr, team: TeamId, cfg: Option<SyncPeerConfig>) -> Result<()>;
590
591    /// Removes the peer from automatic syncing.
592    async fn remove_sync_peer(addr: Addr, team: TeamId) -> Result<()>;
593
594    /// add a team to the local device store that was created by someone else. Not an aranya action/command.
595    async fn add_team(team: TeamId, cfg: TeamConfig) -> Result<()>;
596
597    /// remove a team from the local device store.
598    async fn remove_team(team: TeamId) -> Result<()>;
599
600    /// Create a new graph/team with the current device as the owner.
601    async fn create_team(cfg: TeamConfig) -> Result<TeamId>;
602    /// Close the team.
603    async fn close_team(team: TeamId) -> Result<()>;
604
605    /// Add device to the team.
606    async fn add_device_to_team(team: TeamId, keys: KeyBundle) -> Result<()>;
607    /// Remove device from the team.
608    async fn remove_device_from_team(team: TeamId, device: DeviceId) -> Result<()>;
609
610    /// Assign a role to a device.
611    async fn assign_role(team: TeamId, device: DeviceId, role: Role) -> Result<()>;
612    /// Revoke a role from a device.
613    async fn revoke_role(team: TeamId, device: DeviceId, role: Role) -> Result<()>;
614
615    /// Assign a QUIC channels network identifier to a device.
616    async fn assign_aqc_net_identifier(
617        team: TeamId,
618        device: DeviceId,
619        name: NetIdentifier,
620    ) -> Result<()>;
621    /// Remove a QUIC channels network identifier from a device.
622    async fn remove_aqc_net_identifier(
623        team: TeamId,
624        device: DeviceId,
625        name: NetIdentifier,
626    ) -> Result<()>;
627
628    // Create a label.
629    async fn create_label(team: TeamId, name: String) -> Result<LabelId>;
630    // Delete a label.
631    async fn delete_label(team: TeamId, label_id: LabelId) -> Result<()>;
632    // Assign a label to a device.
633    async fn assign_label(
634        team: TeamId,
635        device: DeviceId,
636        label_id: LabelId,
637        op: ChanOp,
638    ) -> Result<()>;
639    // Revoke a label from a device.
640    async fn revoke_label(team: TeamId, device: DeviceId, label_id: LabelId) -> Result<()>;
641
642    /// Create a bidirectional QUIC channel.
643    async fn create_aqc_bidi_channel(
644        team: TeamId,
645        peer: NetIdentifier,
646        label_id: LabelId,
647    ) -> Result<(AqcCtrl, AqcBidiPsks)>;
648    /// Create a unidirectional QUIC channel.
649    async fn create_aqc_uni_channel(
650        team: TeamId,
651        peer: NetIdentifier,
652        label_id: LabelId,
653    ) -> Result<(AqcCtrl, AqcUniPsks)>;
654    /// Delete a QUIC bidi channel.
655    async fn delete_aqc_bidi_channel(chan: AqcBidiChannelId) -> Result<AqcCtrl>;
656    /// Delete a QUIC uni channel.
657    async fn delete_aqc_uni_channel(chan: AqcUniChannelId) -> Result<AqcCtrl>;
658    /// Receive AQC ctrl message.
659    async fn receive_aqc_ctrl(team: TeamId, ctrl: AqcCtrl) -> Result<(LabelId, AqcPsks)>;
660
661    /// Query devices on team.
662    async fn query_devices_on_team(team: TeamId) -> Result<Vec<DeviceId>>;
663    /// Query device role.
664    async fn query_device_role(team: TeamId, device: DeviceId) -> Result<Role>;
665    /// Query device keybundle.
666    async fn query_device_keybundle(team: TeamId, device: DeviceId) -> Result<KeyBundle>;
667    /// Query device label assignments.
668    async fn query_device_label_assignments(team: TeamId, device: DeviceId) -> Result<Vec<Label>>;
669    /// Query AQC network ID.
670    async fn query_aqc_net_identifier(
671        team: TeamId,
672        device: DeviceId,
673    ) -> Result<Option<NetIdentifier>>;
674    // Query labels on team.
675    async fn query_labels(team: TeamId) -> Result<Vec<Label>>;
676    /// Query whether a label exists.
677    async fn query_label_exists(team: TeamId, label: LabelId) -> Result<bool>;
678}
679
680#[cfg(test)]
681mod tests {
682    use aranya_crypto::Rng;
683    use serde::de::DeserializeOwned;
684
685    use super::*;
686
687    fn secret(secret: &[u8]) -> Secret {
688        Secret(Box::from(secret))
689    }
690
691    pub(super) trait PskMap:
692        fmt::Debug + PartialEq + Serialize + DeserializeOwned + Sized
693    {
694        type Psk;
695        fn new() -> Self;
696        /// Returns the number of PSKs in the map.
697        fn len(&self) -> usize;
698        /// Adds `psk` to the map.
699        ///
700        /// # Panics
701        ///
702        /// Panics if `psk` already exists.
703        fn insert(&mut self, psk: Self::Psk);
704    }
705
706    impl PartialEq for AqcBidiPsk {
707        fn eq(&self, other: &Self) -> bool {
708            bool::from(self.ct_eq(other))
709        }
710    }
711    impl PartialEq for AqcUniPsk {
712        fn eq(&self, other: &Self) -> bool {
713            bool::from(self.ct_eq(other))
714        }
715    }
716    impl PartialEq for AqcPsk {
717        fn eq(&self, other: &Self) -> bool {
718            bool::from(self.ct_eq(other))
719        }
720    }
721
722    #[track_caller]
723    fn psk_map_test<M, F>(name: &'static str, mut f: F)
724    where
725        M: PskMap,
726        F: FnMut(Secret, Id, CipherSuiteId) -> M::Psk,
727    {
728        let mut psks = M::new();
729        for (i, &suite) in CipherSuiteId::all().iter().enumerate() {
730            let id = Id::random(&mut Rng);
731            let secret = secret(&i.to_le_bytes());
732            psks.insert(f(secret, id, suite));
733        }
734        assert_eq!(psks.len(), CipherSuiteId::all().len(), "{name}");
735
736        let bytes = postcard::to_allocvec(&psks).unwrap();
737        let got = postcard::from_bytes::<M>(&bytes).unwrap();
738        assert_eq!(got, psks, "{name}")
739    }
740
741    /// Test that we can correctly serialize and deserialize
742    /// [`AqcBidiPsk`].
743    #[test]
744    fn test_aqc_bidi_psks_serde() {
745        psk_map_test::<AqcBidiPsks, _>("AqcBidiPsk", |secret, id, suite| AqcBidiPsk {
746            identity: BidiPskId::from((id.into(), suite)),
747            secret,
748        });
749    }
750
751    /// Test that we can correctly serialize and deserialize
752    /// [`AqcUniPsk`].
753    #[test]
754    fn test_aqc_uni_psks_serde() {
755        psk_map_test::<AqcUniPsks, _>("AqcUniPsk (send)", |secret, id, suite| AqcUniPsk {
756            identity: UniPskId::from((id.into(), suite)),
757            secret: Directed::Send(secret),
758        });
759        psk_map_test::<AqcUniPsks, _>("AqcUniPsk (recv)", |secret, id, suite| AqcUniPsk {
760            identity: UniPskId::from((id.into(), suite)),
761            secret: Directed::Recv(secret),
762        });
763    }
764}