Skip to main content

macula_rust/record/
authorization.rs

1//! A procedure advertisement's provider authorization (D25), as
2//! macula_record's verify_authorization/3 and own_namespace/1 decide it. A
3//! procedure `org/name` is authorized by the realm's org directory, which
4//! names the org's key, and the org's procedure delegation to the
5//! advertiser, both carried in the advertisement. A procedure in a node's own
6//! namespace, `~<node_id>/name`, is authorized by the advertisement's
7//! signature alone, and needs no realm key. A procedure without a namespace
8//! carries no authorization.
9
10use crate::profile::Profile;
11
12use super::{
13    id_field, malformed, read_procedure_advertisement, text_field, verify, Authorization,
14    ProcedureAdvertisement, Record, RecordError, RecordType, Verified,
15};
16
17/// Starts the namespace of a node's own procedures, `~<node_id>/<name>`.
18pub const OWN_NAMESPACE_PREFIX: &str = "~";
19
20/// What a caller trusts for its realm: the verifier's profile and the realm
21/// key as carried, `None` when none is pinned.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Trust {
24    pub profile: Profile,
25    pub realm_key: Option<Vec<u8>>,
26}
27
28/// A procedure's org namespace: the text before the first `/` of its name.
29/// A name without a slash, or with `_` before it, has none; a name starting
30/// with a slash is malformed.
31pub fn procedure_org(procedure: &str) -> Result<Option<&str>, RecordError> {
32    match procedure.split_once('/') {
33        None => Ok(None),
34        Some(("_", _)) => Ok(None),
35        Some(("", _)) => Err(malformed("a procedure name starting with a slash")),
36        Some((org, _)) => Ok(Some(org)),
37    }
38}
39
40/// Whether `procedure` names a node's own namespace, `~` before its first
41/// slash, spelled well or not.
42pub fn in_own_namespace(procedure: &str) -> bool {
43    matches!(procedure_org(procedure), Ok(Some(org)) if org.starts_with(OWN_NAMESPACE_PREFIX))
44}
45
46/// `name` in the own namespace of `node`: `~<node_id hex>/name`.
47pub fn own_procedure(node: &[u8; 32], name: &str) -> String {
48    let hex: String = node.iter().map(|b| format!("{b:02x}")).collect();
49    format!("{OWN_NAMESPACE_PREFIX}{hex}/{name}")
50}
51
52/// The node_id a `~` namespace names: exactly 64 lowercase hex characters,
53/// the one spelling of a node_id in a namespace.
54pub fn namespace_node(hex_node: &str) -> Result<[u8; 32], RecordError> {
55    let bad = || malformed("a ~ namespace is 64 lowercase hex characters");
56    if hex_node.len() != 64
57        || !hex_node
58            .bytes()
59            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
60    {
61        return Err(bad());
62    }
63    let mut node = [0u8; 32];
64    for (i, byte) in node.iter_mut().enumerate() {
65        *byte = u8::from_str_radix(&hex_node[2 * i..2 * i + 2], 16).map_err(|_| bad())?;
66    }
67    Ok(node)
68}
69
70/// Whether a verified procedure advertisement is in its advertiser's own
71/// namespace and admissible there: `~<node_id>/<name>` where node_id is the
72/// advertiser_node verifying bound to its signer, with no authorization.
73pub fn own_namespace(advertisement: &Verified) -> Result<(), RecordError> {
74    let r = advertisement.record();
75    if r.record_type != RecordType::PROCEDURE_ADVERTISEMENT {
76        return Err(RecordError::NotOwnNamespace);
77    }
78    let read = read_procedure_advertisement(r)?;
79    match procedure_org(&read.procedure) {
80        Ok(Some(org)) if org.starts_with(OWN_NAMESPACE_PREFIX) => {
81            own_node(&org[OWN_NAMESPACE_PREFIX.len()..], &read)
82        }
83        _ => Err(RecordError::NotOwnNamespace),
84    }
85}
86
87fn own_node(hex_node: &str, read: &ProcedureAdvertisement) -> Result<(), RecordError> {
88    let node = namespace_node(hex_node)?;
89    if node != read.advertiser_node {
90        return Err(RecordError::NotOwnNamespace);
91    }
92    if read.authorization != Authorization::None {
93        return Err(RecordError::AuthorizationNotAllowed);
94    }
95    Ok(())
96}
97
98/// A caller's check of a verified procedure advertisement's provider
99/// authorization against the realm it trusts, at `now_ms`. An org procedure
100/// needs an org directory and a procedure delegation, and the realm key: the
101/// directory must verify, carry the realm key and name the advertisement's
102/// realm and the procedure's org; the delegation must verify, signed by the
103/// org key the directory names, for the advertiser; and the advertisement
104/// expires no later than either.
105pub fn verify_authorization(
106    advertisement: &Verified,
107    trust: &Trust,
108    now_ms: i64,
109) -> Result<(), RecordError> {
110    let r = advertisement.record();
111    if r.record_type != RecordType::PROCEDURE_ADVERTISEMENT {
112        return Err(malformed("not a procedure advertisement"));
113    }
114    let read = read_procedure_advertisement(r)?;
115    let org = procedure_org(&read.procedure)?;
116    if let Some(org) = org.filter(|o| o.starts_with(OWN_NAMESPACE_PREFIX)) {
117        return own_node(&org[OWN_NAMESPACE_PREFIX.len()..], &read);
118    }
119    match (org, &read.authorization) {
120        (None, Authorization::None) => Ok(()),
121        (None, _) => Err(RecordError::AuthorizationNotAllowed),
122        (Some(_), Authorization::None) => Err(RecordError::NoAuthorization),
123        (
124            Some(org),
125            Authorization::Delegation {
126                org_directory,
127                procedure_delegation,
128            },
129        ) => delegation_path(
130            r,
131            &read,
132            org,
133            org_directory,
134            procedure_delegation,
135            trust,
136            now_ms,
137        ),
138        (Some(_), Authorization::Unsupported) => Err(RecordError::AuthorizationFormUnsupported),
139        (Some(_), Authorization::Malformed) => Err(malformed(
140            "an org directory and a procedure delegation that are not both byte strings",
141        )),
142    }
143}
144
145fn delegation_path(
146    advertisement: &Record,
147    read: &ProcedureAdvertisement,
148    org: &str,
149    directory_wire: &[u8],
150    delegation_wire: &[u8],
151    trust: &Trust,
152    now_ms: i64,
153) -> Result<(), RecordError> {
154    let realm_key = trust.realm_key.as_ref().ok_or(RecordError::NoRealmKey)?;
155    let directory = verify(directory_wire, trust.profile, now_ms)
156        .map_err(|e| RecordError::OrgDirectoryInvalid(e.to_string()))?
157        .into_record();
158    let named = read_org_directory(&directory)
159        .map_err(|e| RecordError::OrgDirectoryInvalid(e.to_string()))?;
160    let directory_key = directory.signed.as_ref().map(|s| &s.key);
161    if directory_key != Some(realm_key) || named.realm_id != read.realm_id {
162        return Err(RecordError::OrgDirectoryWrongRealm);
163    }
164    if named.org_name != org {
165        return Err(RecordError::OrgDirectoryWrongOrg);
166    }
167    let delegation = verify(delegation_wire, trust.profile, now_ms)
168        .map_err(|e| RecordError::DelegationInvalid(e.to_string()))?
169        .into_record();
170    let granted = read_procedure_delegation(&delegation)
171        .map_err(|e| RecordError::DelegationInvalid(e.to_string()))?;
172    let delegation_key_id = delegation.signed.as_ref().map(|s| s.key_id);
173    if delegation_key_id != Some(named.org_key) || granted.advertiser != read.advertiser_node {
174        return Err(RecordError::DelegationMismatch);
175    }
176    if advertisement.expires_at > directory.expires_at.min(delegation.expires_at) {
177        return Err(RecordError::AuthorizationOutlived);
178    }
179    Ok(())
180}
181
182/// An org directory's payload: a realm's statement that the org `org_name`
183/// is held by the key with key id `org_key`.
184#[derive(Debug, Clone, Default, PartialEq, Eq)]
185pub struct OrgDirectory {
186    pub realm_id: [u8; 32],
187    pub org_name: String,
188    pub org_key: [u8; 32],
189}
190
191/// Reads an org directory's payload.
192pub fn read_org_directory(r: &Record) -> Result<OrgDirectory, RecordError> {
193    if r.record_type != RecordType::ORG_DIRECTORY {
194        return Err(malformed("not an org directory"));
195    }
196    Ok(OrgDirectory {
197        realm_id: id_field(&r.payload, "realm_id"),
198        org_name: text_field(&r.payload, "org_name"),
199        org_key: id_field(&r.payload, "org_key"),
200    })
201}
202
203/// A procedure delegation's payload: an org's grant, signed by its org key,
204/// that the node `advertiser` may serve procedures under the org.
205#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct ProcedureDelegation {
207    pub org_key: [u8; 32],
208    pub advertiser: [u8; 32],
209}
210
211/// Reads a procedure delegation's payload.
212pub fn read_procedure_delegation(r: &Record) -> Result<ProcedureDelegation, RecordError> {
213    if r.record_type != RecordType::PROCEDURE_DELEGATION {
214        return Err(malformed("not a procedure delegation"));
215    }
216    Ok(ProcedureDelegation {
217        org_key: id_field(&r.payload, "org_key"),
218        advertiser: id_field(&r.payload, "advertiser"),
219    })
220}