aranya_crypto/
policy.rs

1//! Aranya policy related routines.
2
3use core::borrow::Borrow as _;
4
5use spideroak_crypto::hash::{Digest, Hash};
6use zerocopy::{Immutable, IntoBytes, KnownLayout, Unaligned};
7
8use crate::{
9    aranya::{Signature, SigningKeyId},
10    ciphersuite::{CipherSuite, CipherSuiteExt as _},
11    id::{IdExt as _, custom_id},
12};
13
14custom_id! {
15    /// Uniquely identifies a group.
16    #[derive(Immutable, IntoBytes, KnownLayout, Unaligned)]
17    pub struct GroupId;
18}
19
20custom_id! {
21    /// Uniquely identifies a policy.
22    #[derive(Immutable, IntoBytes, KnownLayout, Unaligned)]
23    pub struct PolicyId;
24}
25
26custom_id! {
27    /// The ID of a policy command.
28    #[derive(Immutable, IntoBytes, KnownLayout, Unaligned)]
29    pub struct CmdId;
30}
31
32/// Computes the command's unique ID.
33pub(crate) fn cmd_id<CS: CipherSuite>(
34    cmd: &Digest<<CS::Hash as Hash>::DigestSize>,
35    sig: &Signature<CS>,
36) -> CmdId {
37    // id = H(
38    //     "PolicyCommandId-v1",
39    //     command,
40    //     signature,
41    // )
42    CmdId::new::<CS>(
43        b"PolicyCommandId-v1",
44        [cmd.as_bytes(), sig.raw_sig().borrow()],
45    )
46}
47
48/// Computes a merge command's ID.
49pub fn merge_cmd_id<CS: CipherSuite>(left: CmdId, right: CmdId) -> CmdId {
50    // id = H(
51    //     "MergeCommandId-v1",
52    //     left_id,
53    //     right_id,
54    // )
55    CmdId::new::<CS>(b"MergeCommandId-v1", [left.as_bytes(), right.as_bytes()])
56}
57
58/// A policy command.
59#[derive(Copy, Clone, Debug)]
60pub struct Cmd<'a> {
61    /// The command encoded in its canonical format.
62    pub data: &'a [u8],
63    /// The name of the command.
64    ///
65    /// E.g., `AddDevice`.
66    pub name: &'a str,
67    /// The parent command in the graph.
68    pub parent_id: &'a CmdId,
69}
70
71impl Cmd<'_> {
72    /// Returns the digest of the command and its contextual
73    /// binding.
74    pub(crate) fn digest<CS: CipherSuite>(
75        &self,
76        author: SigningKeyId,
77    ) -> Digest<<CS::Hash as Hash>::DigestSize> {
78        // digest = H(
79        //     "SignPolicyCommand-v1",
80        //     suites,
81        //     pk,
82        //     name,
83        //     parent_id,
84        //     msg,
85        // )
86        //
87        // Bind the signature to the current cipher suite,
88        CS::tuple_hash(
89            b"SignPolicyCommand-v1",
90            [
91                // and to the author's public key,
92                author.as_bytes(),
93                // and to the type of command being signed,
94                self.name.as_bytes(),
95                // and to the parent command,
96                self.parent_id.as_bytes(),
97                // and finally the command data itself.
98                self.data,
99            ],
100        )
101    }
102}
103
104custom_id! {
105    /// Uniquely identifies a role.
106    #[derive(Immutable, IntoBytes, KnownLayout, Unaligned)]
107    pub struct RoleId;
108}
109
110/// Computes the ID of a policy role.
111///
112/// `cmd` must be the command that created (or is creating) the
113/// role. `name` is the name of the role, e.g., `admin`.
114pub fn role_id<CS: CipherSuite>(cmd_id: CmdId, name: &str, policy_id: PolicyId) -> RoleId {
115    // id = H(
116    //     "RoleId-v1",
117    //     cmd_id,
118    //     name,
119    //     policy_id,
120    // )
121    RoleId::new::<CS>(
122        b"RoleId-v1",
123        [cmd_id.as_bytes(), name.as_bytes(), policy_id.as_bytes()],
124    )
125}
126
127custom_id! {
128    /// Uniquely identifies a label.
129    #[derive(Immutable, IntoBytes, KnownLayout, Unaligned)]
130    pub struct LabelId;
131}
132
133/// Computes the ID of a label.
134///
135/// `cmd` must be the command that created (or is creating) the
136/// label. `name` is the name of the label, e.g., `telemetry`.
137pub fn label_id<CS: CipherSuite>(cmd_id: CmdId, name: &str, policy_id: PolicyId) -> LabelId {
138    // id = H(
139    //     "LabelId-v1",
140    //     cmd_id,
141    //     name,
142    //     policy_id,
143    // )
144    LabelId::new::<CS>(
145        b"LabelId-v1",
146        [cmd_id.as_bytes(), name.as_bytes(), policy_id.as_bytes()],
147    )
148}
149
150#[cfg(test)]
151mod tests {
152    use spideroak_crypto::{ed25519::Ed25519, rust};
153
154    use super::*;
155    use crate::{default::DhKemP256HkdfSha256, test_util::TestCs};
156
157    type CS = TestCs<
158        rust::Aes256Gcm,
159        rust::Sha256,
160        rust::HkdfSha512,
161        DhKemP256HkdfSha256,
162        rust::HmacSha512,
163        Ed25519,
164    >;
165
166    /// Golden test for [`label_id`].
167    #[test]
168    fn test_label_id() {
169        let tests = [
170            (
171                CmdId::default(),
172                "foo",
173                PolicyId::default(),
174                "C1PupQYTjr2ouZ3DohnRFEaHR4yoTnMkarbBK4TGhJoi",
175            ),
176            (
177                CmdId::default(),
178                "bar",
179                PolicyId::default(),
180                "Eq71P2UhRVMt7R1s1ZB6m1kSuuzwBZwAd21BEv3gmtBC",
181            ),
182            (
183                CmdId::from_bytes([b'A'; 32]),
184                "bar",
185                PolicyId::default(),
186                "B4XqE83yLS1i8AiyMxGKo2wtrwvqrhUers5ou3eRfH8z",
187            ),
188            (
189                CmdId::from_bytes([b'A'; 32]),
190                "baz",
191                PolicyId::from_bytes([b'B'; 32]),
192                "ACnKJXFwd9e2tSnakXgP8SMiYHBSQLUetWgRjHjyQo8y",
193            ),
194        ];
195        for (i, (cmd_id, name, policy_id, want)) in tests.iter().enumerate() {
196            let got = label_id::<CS>(*cmd_id, name, *policy_id);
197            let want = LabelId::decode(*want).unwrap();
198            assert_eq!(got, want, "#{i}");
199        }
200    }
201
202    /// Golden test for [`role_id`].
203    #[test]
204    fn test_role_id() {
205        let tests = [
206            (
207                CmdId::default(),
208                "foo",
209                PolicyId::default(),
210                "BoukxZv6twB39TdXkzMafUxsT1uvpmMJbr6nsKLBg7VT",
211            ),
212            (
213                CmdId::default(),
214                "bar",
215                PolicyId::default(),
216                "CEEjmy5R6Q7RXBqFtt1nrh597Ytr7bCc2aEWJfixEp9K",
217            ),
218            (
219                CmdId::from_bytes([b'A'; 32]),
220                "bar",
221                PolicyId::default(),
222                "9NEW3iaJim8iipkeBCJPJ3v75pEH92iLtrqo8sddkqER",
223            ),
224            (
225                CmdId::from_bytes([b'A'; 32]),
226                "baz",
227                PolicyId::from_bytes([b'B'; 32]),
228                "4sVA51vurQexYL8NFxGYnhj7RTf51udZg7Qd1dhsgBnx",
229            ),
230        ];
231        for (i, (cmd_id, name, policy_id, want)) in tests.iter().enumerate() {
232            let got = role_id::<CS>(*cmd_id, name, *policy_id);
233            let want = RoleId::decode(*want).unwrap();
234            assert_eq!(got, want, "#{i}");
235        }
236    }
237}