1extern crate biscuit_auth as biscuit;
2
3use std::time::Duration;
4
5use biscuit::Biscuit;
6use biscuit::macros::{block, check, fact};
7use chrono::Utc;
8use hessra_token_core::{KeyPair, PublicKey, TokenError, TokenTimeConfig};
9
10use crate::mint::HessraCapability;
11
12impl HessraCapability {
13 pub fn amend(token: &str, root_pk: PublicKey) -> Result<CapabilityAmendment, TokenError> {
20 let biscuit = Biscuit::from_base64(token, root_pk)?;
21 Ok(CapabilityAmendment {
22 biscuit,
23 require_designations: Vec::new(),
24 require_designations_trusting: Vec::new(),
25 valid_for: None,
26 attest_activation: false,
27 attest_designations: Vec::new(),
28 time_config: TokenTimeConfig::default(),
29 })
30 }
31}
32
33pub struct CapabilityAmendment {
43 biscuit: Biscuit,
44 require_designations: Vec<(String, String)>,
45 require_designations_trusting: Vec<(PublicKey, String, String)>,
46 valid_for: Option<Duration>,
47 attest_activation: bool,
48 attest_designations: Vec<(String, String)>,
49 time_config: TokenTimeConfig,
54}
55
56impl CapabilityAmendment {
57 pub fn designation(mut self, label: impl Into<String>, value: impl Into<String>) -> Self {
59 self.require_designations.push((label.into(), value.into()));
60 self
61 }
62
63 pub fn designation_trusting(
66 mut self,
67 pk: PublicKey,
68 label: impl Into<String>,
69 value: impl Into<String>,
70 ) -> Self {
71 self.require_designations_trusting
72 .push((pk, label.into(), value.into()));
73 self
74 }
75
76 pub fn valid_for(mut self, duration: Duration) -> Self {
79 self.valid_for = Some(duration);
80 self
81 }
82
83 pub fn activate(mut self) -> Self {
87 self.attest_activation = true;
88 self
89 }
90
91 pub fn with_designation(mut self, label: impl Into<String>, value: impl Into<String>) -> Self {
96 self.attest_designations.push((label.into(), value.into()));
97 self
98 }
99
100 pub fn with_time(mut self, time_config: TokenTimeConfig) -> Self {
105 self.time_config = time_config;
106 self
107 }
108
109 fn now(&self) -> i64 {
111 self.time_config
112 .start_time
113 .unwrap_or_else(|| Utc::now().timestamp())
114 }
115
116 pub fn attenuate(self) -> Result<String, TokenError> {
121 if self.attest_activation || !self.attest_designations.is_empty() {
122 return Err(TokenError::generic(
123 "attenuate() cannot carry attested facts; use attest(keypair)",
124 ));
125 }
126 let mut block = block!(r#""#);
127 block = self.append_constraints(block)?;
128 let appended = self
129 .biscuit
130 .append(block)
131 .map_err(|e| TokenError::AttenuationFailed {
132 reason: e.to_string(),
133 })?;
134 Ok(appended.to_base64()?)
135 }
136
137 pub fn attest(self, keypair: &KeyPair) -> Result<String, TokenError> {
142 let now = self.now();
143 let mut block = block!(r#""#);
144
145 if self.attest_activation {
146 block = block
147 .fact(fact!(r#"activation({now});"#))
148 .map_err(|e| TokenError::generic(format!("attest activation: {e}")))?;
149 }
150 for (label, value) in &self.attest_designations {
151 let (label, value) = (label.clone(), value.clone());
152 block = block
153 .fact(fact!(r#"designation({label}, {value});"#))
154 .map_err(|e| TokenError::generic(format!("attest designation: {e}")))?;
155 }
156 block = self.append_constraints(block)?;
157
158 let request = self
159 .biscuit
160 .third_party_request()
161 .map_err(|e| TokenError::generic(format!("attest request: {e}")))?;
162 let signed = request
163 .create_block(&keypair.private(), block)
164 .map_err(|e| TokenError::generic(format!("attest sign: {e}")))?;
165 let appended = self
166 .biscuit
167 .append_third_party(keypair.public(), signed)
168 .map_err(|e| TokenError::generic(format!("attest append: {e}")))?;
169 Ok(appended.to_base64()?)
170 }
171
172 fn append_constraints(
174 &self,
175 mut block: biscuit::builder::BlockBuilder,
176 ) -> Result<biscuit::builder::BlockBuilder, TokenError> {
177 for (label, value) in &self.require_designations {
178 let (label, value) = (label.clone(), value.clone());
179 block = block
180 .check(check!(r#"check if designation({label}, {value});"#))
181 .map_err(|e| TokenError::AttenuationFailed {
182 reason: e.to_string(),
183 })?;
184 }
185 for (pk, label, value) in &self.require_designations_trusting {
186 let (pk, label, value) = (*pk, label.clone(), value.clone());
187 block = block
188 .check(check!(
189 r#"check if designation({label}, {value}) trusting {pk};"#
190 ))
191 .map_err(|e| TokenError::AttenuationFailed {
192 reason: e.to_string(),
193 })?;
194 }
195 if let Some(duration) = self.valid_for {
196 let expiration = self.now() + duration.as_secs() as i64;
197 block = block
198 .check(check!(r#"check if time($t), $t < {expiration};"#))
199 .map_err(|e| TokenError::AttenuationFailed {
200 reason: e.to_string(),
201 })?;
202 }
203 Ok(block)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::verify::CapabilityVerifier;
211
212 fn authority_cap(authority: &KeyPair) -> String {
213 HessraCapability::new()
214 .subject("alice")
215 .resource("res1")
216 .operation("read")
217 .issue(authority)
218 .unwrap()
219 }
220
221 #[test]
222 fn keyless_designation_narrowing() {
223 let root = KeyPair::new();
224 let token = authority_cap(&root);
225 let narrowed = HessraCapability::amend(&token, root.public())
226 .unwrap()
227 .designation("tenant", "t-1")
228 .attenuate()
229 .unwrap();
230
231 assert!(
233 CapabilityVerifier::new(
234 narrowed.clone(),
235 root.public(),
236 "res1".into(),
237 "read".into()
238 )
239 .with_designation("tenant".into(), "t-1".into())
240 .verify()
241 .is_ok()
242 );
243 assert!(
245 CapabilityVerifier::new(narrowed, root.public(), "res1".into(), "read".into())
246 .verify()
247 .is_err()
248 );
249 }
250
251 #[test]
252 fn attenuate_rejects_attested_facts() {
253 let root = KeyPair::new();
254 let token = authority_cap(&root);
255 let err = HessraCapability::amend(&token, root.public())
256 .unwrap()
257 .activate()
258 .attenuate();
259 assert!(err.is_err());
260 }
261
262 use std::time::Duration;
269
270 fn roles() -> (KeyPair, KeyPair) {
272 (KeyPair::new(), KeyPair::new())
273 }
274
275 fn latent(authority: &KeyPair, ts: &KeyPair, anchor: &str) -> String {
276 HessraCapability::new()
277 .subject("agent")
278 .resource("res:x")
279 .operation("read")
280 .anchor(anchor)
281 .valid_for(Duration::from_secs(3600))
282 .latent(ts.public())
283 .issue(authority)
284 .unwrap()
285 }
286
287 fn activate(latent: &str, authority: &KeyPair, ts: &KeyPair, facet: &str) -> String {
289 HessraCapability::amend(latent, authority.public())
290 .unwrap()
291 .activate()
292 .valid_for(Duration::from_secs(300))
293 .designation_trusting(ts.public(), "facet", facet)
294 .attest(ts)
295 .unwrap()
296 }
297
298 fn complete(activated: &str, authority: &KeyPair, ts: &KeyPair, facet: &str) -> String {
300 HessraCapability::amend(activated, authority.public())
301 .unwrap()
302 .with_designation("facet", facet)
303 .attest(ts)
304 .unwrap()
305 }
306
307 fn ok(token: &str, authority: &KeyPair, anchor: &str) -> bool {
308 CapabilityVerifier::new(
309 token.to_string(),
310 authority.public(),
311 "res:x".to_string(),
312 "read".to_string(),
313 )
314 .anchor(anchor)
315 .verify()
316 .is_ok()
317 }
318
319 fn ok_at(token: &str, authority: &KeyPair, anchor: &str, at: i64) -> bool {
321 CapabilityVerifier::new(
322 token.to_string(),
323 authority.public(),
324 "res:x".to_string(),
325 "read".to_string(),
326 )
327 .anchor(anchor)
328 .with_time(TokenTimeConfig {
329 start_time: Some(at),
330 ..Default::default()
331 })
332 .verify()
333 .is_ok()
334 }
335
336 #[test]
337 fn requires_both_activation_and_facet() {
338 let (auth, ts) = roles();
339 let latent = latent(&auth, &ts, "tool:x");
340
341 assert!(!ok(&latent, &auth, "tool:x"));
343 let activated = activate(&latent, &auth, &ts, "facet-A");
345 assert!(!ok(&activated, &auth, "tool:x"));
346 let completed = complete(&activated, &auth, &ts, "facet-A");
348 assert!(ok(&completed, &auth, "tool:x"));
349 }
350
351 #[test]
352 fn facet_bound_to_activation() {
353 let (auth, ts) = roles();
354 let latent = latent(&auth, &ts, "tool:x");
355 let act_a = activate(&latent, &auth, &ts, "facet-A");
356 let act_b = activate(&latent, &auth, &ts, "facet-B");
357
358 assert!(ok(
359 &complete(&act_a, &auth, &ts, "facet-A"),
360 &auth,
361 "tool:x"
362 ));
363 assert!(ok(
364 &complete(&act_b, &auth, &ts, "facet-B"),
365 &auth,
366 "tool:x"
367 ));
368 assert!(!ok(
370 &complete(&act_b, &auth, &ts, "facet-A"),
371 &auth,
372 "tool:x"
373 ));
374 assert!(!ok(
375 &complete(&act_a, &auth, &ts, "facet-B"),
376 &auth,
377 "tool:x"
378 ));
379 }
380
381 #[test]
382 fn activation_from_wrong_key_rejected() {
383 let (auth, ts) = roles();
386 let rogue = KeyPair::new();
387 let latent = latent(&auth, &ts, "tool:x");
388 let activated = activate(&latent, &auth, &rogue, "facet-A");
389 let completed = complete(&activated, &auth, &ts, "facet-A");
390 assert!(!ok(&completed, &auth, "tool:x"));
391 }
392
393 #[test]
394 fn facet_from_wrong_key_rejected() {
395 let (auth, ts) = roles();
398 let rogue = KeyPair::new();
399 let latent = latent(&auth, &ts, "tool:x");
400 let activated = activate(&latent, &auth, &ts, "facet-A");
401 let forged = complete(&activated, &auth, &rogue, "facet-A");
402 assert!(!ok(&forged, &auth, "tool:x"));
403 }
404
405 #[test]
406 fn activation_shortens_lifetime() {
407 let (auth, ts) = roles();
411 let base = 1_000_000_000;
412 let clock = TokenTimeConfig {
413 start_time: Some(base),
414 ..Default::default()
415 };
416
417 let latent = HessraCapability::new()
419 .subject("agent")
420 .resource("res:x")
421 .operation("read")
422 .anchor("tool:x")
423 .valid_for(Duration::from_secs(3600))
424 .latent(ts.public())
425 .with_time(clock)
426 .issue(&auth)
427 .unwrap();
428
429 let activated = HessraCapability::amend(&latent, auth.public())
431 .unwrap()
432 .activate()
433 .valid_for(Duration::from_secs(300))
434 .designation_trusting(ts.public(), "facet", "f")
435 .with_time(clock)
436 .attest(&ts)
437 .unwrap();
438 let completed = HessraCapability::amend(&activated, auth.public())
439 .unwrap()
440 .with_designation("facet", "f")
441 .with_time(clock)
442 .attest(&ts)
443 .unwrap();
444
445 assert!(ok_at(&completed, &auth, "tool:x", base + 299));
447 assert!(!ok_at(&completed, &auth, "tool:x", base + 301));
450 }
451
452 #[test]
453 fn anchor_binds_latent_to_one_tool() {
454 let (auth, ts) = roles();
455 let latent = latent(&auth, &ts, "tool:a");
456 let activated = activate(&latent, &auth, &ts, "facet-A");
457 let completed = complete(&activated, &auth, &ts, "facet-A");
458 assert!(!ok(&completed, &auth, "tool:b"));
459 assert!(ok(&completed, &auth, "tool:a"));
460 }
461}