holographic_memory/core/provenance/
access.rs1use std::time::{SystemTime, UNIX_EPOCH};
18
19use anyhow::{anyhow, ensure, Result};
20use ed25519_dalek::SigningKey;
21use fxhash::FxHashMap;
22use serde::{Deserialize, Serialize};
23
24use super::did::{did_key_from_ed25519, ed25519_from_did_key};
25use super::trust::TrustStore;
26use super::vc::{self, DataIntegrityProof};
27
28pub const ACCESS_CREDENTIAL_TYPE: &str = "HmsAccessCredential";
29const VC_TYPE: &str = "VerifiableCredential";
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum Permission {
34 Read,
35 Write,
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct AccessCredential {
41 #[serde(rename = "@context")]
42 pub context: Vec<String>,
43 #[serde(rename = "type")]
44 pub credential_type: Vec<String>,
45 pub id: String,
46 pub issuer: String,
47 pub valid_from: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub valid_until: Option<String>,
50 pub credential_subject: AccessSubject,
51 pub proof: Option<DataIntegrityProof>,
52}
53
54#[derive(Clone, Debug, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct AccessSubject {
57 pub id: String,
59 pub permissions: Vec<Permission>,
60}
61
62pub fn issue_access_credential(
65 signing_key: &SigningKey,
66 subject_did: &str,
67 permissions: &[Permission],
68 valid_until: Option<&str>,
69) -> Result<AccessCredential> {
70 ed25519_from_did_key(subject_did)?;
71 ensure!(
72 !permissions.is_empty(),
73 "credential must grant a permission"
74 );
75 if let Some(until) = valid_until {
76 vc::parse_iso8601_utc(until)?;
77 }
78 let nonce: [u8; 16] = rand::random();
79 let id: String = nonce.iter().map(|b| format!("{b:02x}")).collect();
80
81 let mut credential = AccessCredential {
82 context: vec![
83 "https://www.w3.org/ns/credentials/v2".to_string(),
84 "https://writerslogic.com/ns/hms/v1".to_string(),
85 ],
86 credential_type: vec![VC_TYPE.to_string(), ACCESS_CREDENTIAL_TYPE.to_string()],
87 id: format!("urn:hms:access:{id}"),
88 issuer: did_key_from_ed25519(&signing_key.verifying_key().to_bytes()),
89 valid_from: vc::chrono_iso8601_now(),
90 valid_until: valid_until.map(str::to_string),
91 credential_subject: AccessSubject {
92 id: subject_did.to_string(),
93 permissions: permissions.to_vec(),
94 },
95 proof: None,
96 };
97 credential.proof = Some(vc::sign_document(signing_key, &credential)?);
98 Ok(credential)
99}
100
101struct Grant {
102 permissions: Vec<Permission>,
103 expires_at: Option<u64>,
104}
105
106#[derive(Default)]
109pub struct AgentRegistry {
110 issuers: TrustStore,
111 grants: FxHashMap<String, Grant>,
112}
113
114impl AgentRegistry {
115 pub fn new(issuers: TrustStore) -> Self {
116 Self {
117 issuers,
118 grants: FxHashMap::default(),
119 }
120 }
121
122 pub fn trust_issuer(&mut self, issuer_did: &str) -> Result<()> {
124 self.issuers.trust_did(issuer_did).map(|_| ())
125 }
126
127 pub fn admit(&mut self, credential: &AccessCredential) -> Result<()> {
130 self.admit_at(credential, unix_now())
131 }
132
133 pub fn admit_at(&mut self, credential: &AccessCredential, now: u64) -> Result<()> {
134 let proof = credential
135 .proof
136 .as_ref()
137 .ok_or_else(|| anyhow!("credential has no proof"))?;
138 ensure!(
139 [VC_TYPE, ACCESS_CREDENTIAL_TYPE]
140 .iter()
141 .all(|t| credential.credential_type.iter().any(|c| c == t)),
142 "credential is not an {ACCESS_CREDENTIAL_TYPE}"
143 );
144 ensure!(
145 !credential.credential_subject.permissions.is_empty(),
146 "credential grants no permission"
147 );
148 ed25519_from_did_key(&credential.credential_subject.id)?;
149
150 let mut unsigned = credential.clone();
151 unsigned.proof = None;
152 let issuer_key = vc::verify_document(&unsigned, &credential.issuer, proof)?;
153 ensure!(
154 self.issuers.is_trusted(&issuer_key),
155 "credential issuer is not trusted"
156 );
157
158 ensure!(
159 vc::parse_iso8601_utc(&credential.valid_from)? <= now,
160 "credential is not yet valid"
161 );
162 let expires_at = credential
163 .valid_until
164 .as_deref()
165 .map(vc::parse_iso8601_utc)
166 .transpose()?;
167 ensure!(expires_at.is_none_or(|t| now < t), "credential has expired");
168
169 self.grants.insert(
170 credential.credential_subject.id.clone(),
171 Grant {
172 permissions: credential.credential_subject.permissions.clone(),
173 expires_at,
174 },
175 );
176 Ok(())
177 }
178
179 pub fn revoke(&mut self, agent_did: &str) -> bool {
181 self.grants.remove(agent_did).is_some()
182 }
183
184 pub fn authorize(&self, agent_did: &str, permission: Permission) -> Result<()> {
186 self.authorize_at(agent_did, permission, unix_now())
187 }
188
189 pub fn authorize_at(&self, agent_did: &str, permission: Permission, now: u64) -> Result<()> {
190 let grant = self
191 .grants
192 .get(agent_did)
193 .ok_or_else(|| anyhow!("agent has no admitted credential"))?;
194 ensure!(
195 grant.expires_at.is_none_or(|t| now < t),
196 "agent credential has expired"
197 );
198 ensure!(
199 grant.permissions.contains(&permission),
200 "agent credential does not grant {permission:?}"
201 );
202 Ok(())
203 }
204}
205
206fn unix_now() -> u64 {
207 SystemTime::now()
208 .duration_since(UNIX_EPOCH)
209 .map(|d| d.as_secs())
210 .unwrap_or_default()
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn key() -> SigningKey {
218 SigningKey::from_bytes(&rand::random())
219 }
220
221 fn did(key: &SigningKey) -> String {
222 did_key_from_ed25519(&key.verifying_key().to_bytes())
223 }
224
225 fn registry_trusting(issuer: &SigningKey) -> AgentRegistry {
226 let mut registry = AgentRegistry::default();
227 registry.trust_issuer(&did(issuer)).unwrap();
228 registry
229 }
230
231 #[test]
232 fn trusted_credential_grants_only_listed_permissions() {
233 let (issuer, agent) = (key(), did(&key()));
234 let mut registry = registry_trusting(&issuer);
235 let credential =
236 issue_access_credential(&issuer, &agent, &[Permission::Read], None).unwrap();
237
238 assert!(registry.authorize(&agent, Permission::Read).is_err());
239 registry.admit(&credential).unwrap();
240 registry.authorize(&agent, Permission::Read).unwrap();
241 assert!(registry.authorize(&agent, Permission::Write).is_err());
242
243 assert!(registry.revoke(&agent));
244 assert!(registry.authorize(&agent, Permission::Read).is_err());
245 }
246
247 #[test]
248 fn untrusted_or_self_issued_credential_is_rejected() {
249 let (issuer, attacker) = (key(), key());
250 let mut registry = registry_trusting(&issuer);
251 let self_issued =
252 issue_access_credential(&attacker, &did(&attacker), &[Permission::Read], None).unwrap();
253 assert!(registry.admit(&self_issued).is_err());
254 assert!(AgentRegistry::default().admit(&self_issued).is_err());
255 }
256
257 #[test]
258 fn tampered_or_unsigned_credential_is_rejected() {
259 let (issuer, agent) = (key(), did(&key()));
260 let mut registry = registry_trusting(&issuer);
261 let credential =
262 issue_access_credential(&issuer, &agent, &[Permission::Read], None).unwrap();
263
264 let mut escalated = credential.clone();
265 escalated
266 .credential_subject
267 .permissions
268 .push(Permission::Write);
269 assert!(registry.admit(&escalated).is_err());
270
271 let mut retargeted = credential.clone();
272 retargeted.credential_subject.id = did(&key());
273 assert!(registry.admit(&retargeted).is_err());
274
275 let mut forged = escalated.clone();
277 forged.proof = None;
278 forged.proof = Some(vc::sign_document(&key(), &forged).unwrap());
279 assert!(registry.admit(&forged).is_err());
280
281 let mut unsigned = credential.clone();
282 unsigned.proof = None;
283 assert!(registry.admit(&unsigned).is_err());
284
285 let mut garbage = credential;
286 garbage.proof.as_mut().unwrap().proof_value = "z1111".to_string();
287 assert!(registry.admit(&garbage).is_err());
288 }
289
290 #[test]
291 fn validity_window_is_enforced_at_its_boundaries() {
292 let (issuer, agent) = (key(), did(&key()));
293 let mut registry = registry_trusting(&issuer);
294 let until = "2100-01-01T00:00:00Z";
295 let expiry = vc::parse_iso8601_utc(until).unwrap();
296 let credential =
297 issue_access_credential(&issuer, &agent, &[Permission::Read], Some(until)).unwrap();
298 let issued = vc::parse_iso8601_utc(&credential.valid_from).unwrap();
299
300 assert!(registry.admit_at(&credential, issued - 1).is_err());
301 assert!(registry.admit_at(&credential, expiry).is_err());
302 registry.admit_at(&credential, issued).unwrap();
303
304 registry
305 .authorize_at(&agent, Permission::Read, expiry - 1)
306 .unwrap();
307 assert!(registry
308 .authorize_at(&agent, Permission::Read, expiry)
309 .is_err());
310 assert!(registry
311 .authorize_at(&agent, Permission::Read, expiry + 1)
312 .is_err());
313 }
314
315 #[test]
316 fn timestamps_round_trip_and_reject_malformed_input() {
317 assert_eq!(vc::parse_iso8601_utc("1970-01-01T00:00:00Z").unwrap(), 0);
318 assert_eq!(
319 vc::parse_iso8601_utc("2024-02-29T12:30:15Z").unwrap(),
320 1_709_209_815
321 );
322 for bad in [
323 "2023-02-29T00:00:00Z",
324 "2024-13-01T00:00:00Z",
325 "2024-01-01T24:00:00Z",
326 "2024-01-01 00:00:00Z",
327 "2024-01-01T00:00:00+00:00",
328 "+024-01-01T00:00:00Z",
329 "",
330 ] {
331 assert!(vc::parse_iso8601_utc(bad).is_err(), "{bad}");
332 }
333 }
334}