Skip to main content

aranya_crypto/
policy.rs

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