1use 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
17pub const OWN_NAMESPACE_PREFIX: &str = "~";
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Trust {
24 pub profile: Profile,
25 pub realm_key: Option<Vec<u8>>,
26}
27
28pub 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
40pub fn in_own_namespace(procedure: &str) -> bool {
43 matches!(procedure_org(procedure), Ok(Some(org)) if org.starts_with(OWN_NAMESPACE_PREFIX))
44}
45
46pub 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
52pub 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
70pub 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
98pub 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#[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
191pub 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct ProcedureDelegation {
207 pub org_key: [u8; 32],
208 pub advertiser: [u8; 32],
209}
210
211pub 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}