aranya_crypto/
policy.rs

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