hessra_token/
mint.rs

1extern crate biscuit_auth as biscuit;
2
3use crate::verify::{biscuit_key_from_string, ServiceNode};
4
5use biscuit::macros::{biscuit, rule};
6use biscuit::{Biscuit, BiscuitBuilder, KeyPair};
7use chrono::Utc;
8use std::error::Error;
9use tracing::info;
10
11/// TokenTimeConfig allows control over token creation times and durations
12/// This is used to create tokens with custom start times and durations
13/// for testing purposes. In the future, this can be enhanced to support
14/// variable length tokens, such as long-lived bearer tokens.
15#[derive(Debug, Clone, Copy)]
16pub struct TokenTimeConfig {
17    /// Optional custom start time (now time override)
18    pub start_time: Option<i64>,
19    /// Duration in seconds (default: 300 seconds = 5 minutes)
20    pub duration: i64,
21}
22
23impl Default for TokenTimeConfig {
24    fn default() -> Self {
25        Self {
26            start_time: None,
27            duration: 300, // 5 minutes in seconds
28        }
29    }
30}
31
32fn _create_base_biscuit_builder(
33    subject: String,
34    resource: String,
35) -> Result<BiscuitBuilder, Box<dyn Error>> {
36    create_base_biscuit_builder_with_time(subject, resource, TokenTimeConfig::default())
37}
38
39fn create_base_biscuit_builder_with_time(
40    subject: String,
41    resource: String,
42    time_config: TokenTimeConfig,
43) -> Result<BiscuitBuilder, Box<dyn Error>> {
44    let start_time = time_config
45        .start_time
46        .unwrap_or_else(|| Utc::now().timestamp());
47    let expiration = start_time + time_config.duration;
48
49    let biscuit_builder = biscuit!(
50        r#"
51            right({subject}, {resource}, "read");
52            right({subject}, {resource}, "write");
53            check if time($time), $time < {expiration};
54        "#
55    );
56
57    Ok(biscuit_builder)
58}
59
60pub fn create_raw_biscuit(
61    subject: String,
62    resource: String,
63    key: KeyPair,
64    time_config: TokenTimeConfig,
65) -> Result<Biscuit, Box<dyn Error>> {
66    let biscuit =
67        create_base_biscuit_builder_with_time(subject, resource, time_config)?.build(&key)?;
68
69    info!("biscuit (authority): {}", biscuit);
70
71    Ok(biscuit)
72}
73
74/// Creates a new biscuit token with the specified subject and resource.
75///
76/// This function creates a token that grants read and write access to the specified resource
77/// for the given subject. The token will be valid for 5 minutes by default.
78///
79/// # Arguments
80///
81/// * `subject` - The subject (user) identifier
82/// * `resource` - The resource identifier to grant access to
83/// * `key` - The key pair used to sign the token
84/// * `time_config` - Optional time configuration for token validity
85///
86/// # Returns
87///
88/// * `Ok(Vec<u8>)` - The binary token data if successful
89/// * `Err(Box<dyn Error>)` - If token creation fails
90pub fn create_biscuit(
91    subject: String,
92    resource: String,
93    key: KeyPair,
94    time_config: TokenTimeConfig,
95) -> Result<Vec<u8>, Box<dyn Error>> {
96    let biscuit = create_raw_biscuit(subject, resource, key, time_config)?;
97    let token = biscuit.to_vec()?;
98    Ok(token)
99}
100
101fn create_base64_biscuit(
102    subject: String,
103    resource: String,
104    key: KeyPair,
105    time_config: TokenTimeConfig,
106) -> Result<String, Box<dyn Error>> {
107    let biscuit = create_raw_biscuit(subject, resource, key, time_config)?;
108    let token = biscuit.to_base64()?;
109    Ok(token)
110}
111
112pub fn create_token(
113    subject: String,
114    resource: String,
115    key: KeyPair,
116) -> Result<String, Box<dyn Error>> {
117    create_base64_biscuit(subject, resource, key, TokenTimeConfig::default())
118}
119
120pub fn create_token_with_time(
121    subject: String,
122    resource: String,
123    key: KeyPair,
124    time_config: TokenTimeConfig,
125) -> Result<String, Box<dyn Error>> {
126    create_base64_biscuit(subject, resource, key, time_config)
127}
128
129/// Creates a new biscuit token with service chain attestations.
130/// Creates a new biscuit token with service chain attestations.
131///
132/// This function creates a token that grants access to the specified resource for the given subject,
133/// and includes attestations for each service node in the chain. The token will be valid for 5 minutes by default.
134///
135/// # Arguments
136///
137/// * `subject` - The subject (user) identifier
138/// * `resource` - The resource identifier to grant access to
139/// * `key` - The key pair used to sign the token
140/// * `nodes` - Vector of service nodes that will attest to the token
141///
142/// # Returns
143///
144/// * `Ok(Vec<u8>)` - The binary token data if successful
145/// * `Err(Box<dyn Error>)` - If token creation fails
146pub fn create_service_chain_biscuit(
147    subject: String,
148    resource: String,
149    key: KeyPair,
150    nodes: &Vec<ServiceNode>,
151    time_config: TokenTimeConfig,
152) -> Result<Vec<u8>, Box<dyn Error>> {
153    let biscuit = create_raw_service_chain_biscuit(subject, resource, key, nodes, time_config)?;
154    let token = biscuit.to_vec()?;
155    Ok(token)
156}
157
158fn create_base64_service_chain_biscuit(
159    subject: String,
160    resource: String,
161    key: KeyPair,
162    nodes: &Vec<ServiceNode>,
163    time_config: TokenTimeConfig,
164) -> Result<String, Box<dyn Error>> {
165    let biscuit = create_raw_service_chain_biscuit(subject, resource, key, nodes, time_config)?;
166    let token = biscuit.to_base64()?;
167    Ok(token)
168}
169
170pub fn create_raw_service_chain_biscuit(
171    subject: String,
172    resource: String,
173    key: KeyPair,
174    nodes: &Vec<ServiceNode>,
175    time_config: TokenTimeConfig,
176) -> Result<Biscuit, Box<dyn Error>> {
177    create_service_chain_biscuit_with_time(subject, resource, key, nodes, time_config)
178}
179
180pub fn create_service_chain_token(
181    subject: String,
182    resource: String,
183    key: KeyPair,
184    nodes: &Vec<ServiceNode>,
185) -> Result<String, Box<dyn Error>> {
186    create_base64_service_chain_biscuit(subject, resource, key, nodes, TokenTimeConfig::default())
187}
188
189/// Creates a new biscuit token with service chain attestations and custom time settings.
190///
191/// This function creates a token that grants access to the specified resource for the given subject,
192/// includes attestations for each service node in the chain, and allows custom time configuration.
193///
194/// # Arguments
195///
196/// * `subject` - The subject (user) identifier
197/// * `resource` - The resource identifier to grant access to
198/// * `key` - The key pair used to sign the token
199/// * `nodes` - Vector of service nodes that will attest to the token
200/// * `time_config` - Time configuration for token validity
201///
202/// # Returns
203///
204/// * `Ok(Vec<u8>)` - The binary token data if successful
205/// * `Err(Box<dyn Error>)` - If token creation fails
206pub fn create_service_chain_biscuit_with_time(
207    subject: String,
208    resource: String,
209    key: KeyPair,
210    nodes: &Vec<ServiceNode>,
211    time_config: TokenTimeConfig,
212) -> Result<Biscuit, Box<dyn Error>> {
213    let service = resource.clone();
214    let mut biscuit_builder = create_base_biscuit_builder_with_time(subject, service, time_config)?;
215
216    // Add each node in the service chain to the biscuit builder
217    for node in nodes {
218        let component = node.component.clone();
219        let public_key = biscuit_key_from_string(node.public_key.clone())?;
220        biscuit_builder = biscuit_builder.rule(rule!(
221            r#"
222                node($s, {component}) <- service($s) trusting {public_key};
223            "#
224        ))?;
225    }
226
227    let biscuit = biscuit_builder.build(&key)?;
228
229    info!("biscuit (authority): {}", biscuit);
230
231    Ok(biscuit)
232}
233
234pub fn create_service_chain_token_with_time(
235    subject: String,
236    resource: String,
237    key: KeyPair,
238    nodes: &Vec<ServiceNode>,
239    time_config: TokenTimeConfig,
240) -> Result<String, Box<dyn Error>> {
241    let biscuit =
242        create_service_chain_biscuit_with_time(subject, resource, key, nodes, time_config)?;
243    let token = biscuit.to_base64()?;
244    Ok(token)
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::verify::{verify_biscuit_local, verify_service_chain_biscuit_local};
251    use biscuit::macros::block;
252    use biscuit::Biscuit;
253    #[test]
254    fn test_create_biscuit() {
255        let subject = "test@test.com".to_owned();
256        let resource: String = "res1".to_string();
257        let root = KeyPair::new();
258        let public_key = root.public();
259        let token = create_biscuit(
260            subject.clone(),
261            resource.clone(),
262            root,
263            TokenTimeConfig::default(),
264        )
265        .unwrap();
266
267        let res = verify_biscuit_local(token, public_key, subject, resource);
268        assert!(res.is_ok());
269    }
270
271    #[test]
272    fn test_biscuit_expiration() {
273        let subject = "test@test.com".to_owned();
274        let resource = "res1".to_string();
275        let root = KeyPair::new();
276        let public_key = root.public();
277        // Create a biscuit with a 5 minute expiration from now
278        let token = create_biscuit(
279            subject.clone(),
280            resource.clone(),
281            root,
282            TokenTimeConfig::default(),
283        )
284        .unwrap();
285
286        let res =
287            verify_biscuit_local(token.clone(), public_key, subject.clone(), resource.clone());
288        assert!(res.is_ok());
289
290        // Create a biscuit with a start time over 5 minutes ago
291        let root = KeyPair::new();
292        let token = create_biscuit(
293            subject.clone(),
294            resource.clone(),
295            root,
296            TokenTimeConfig {
297                start_time: Some(Utc::now().timestamp() - 301),
298                duration: 300,
299            },
300        )
301        .unwrap();
302        let res = verify_biscuit_local(token, public_key, subject, resource);
303        assert!(res.is_err());
304    }
305
306    #[test]
307    fn test_custom_token_time_config() {
308        let subject = "test@test.com".to_owned();
309        let resource = "res1".to_string();
310        let root = KeyPair::new();
311        let public_key = root.public();
312
313        // Create token with custom start time (1 hour in the past) and longer duration (1 hour)
314        let past_time = Utc::now().timestamp() - 3600;
315        let time_config = TokenTimeConfig {
316            start_time: Some(past_time),
317            duration: 7200, // 2 hours
318        };
319
320        let token = create_biscuit(subject.clone(), resource.clone(), root, time_config).unwrap();
321
322        // Token should be valid at a time between start and expiration
323        let res =
324            verify_biscuit_local(token.clone(), public_key, subject.clone(), resource.clone());
325        assert!(res.is_ok());
326    }
327
328    #[test]
329    fn test_service_chain_biscuit() {
330        let subject = "test@test.com".to_owned();
331        let resource = "res1".to_string();
332        let root = KeyPair::new();
333        let public_key = root.public();
334        let chain_key = KeyPair::new();
335        let chain_public_key = hex::encode(chain_key.public().to_bytes());
336        let chain_public_key = format!("ed25519/{}", chain_public_key);
337        let chain_node = ServiceNode {
338            component: "edge_function".to_string(),
339            public_key: chain_public_key.clone(),
340        };
341        let nodes = vec![chain_node];
342        let token = create_service_chain_biscuit(
343            subject.clone(),
344            resource.clone(),
345            root,
346            &nodes,
347            TokenTimeConfig::default(),
348        );
349        if let Err(e) = &token {
350            println!("Error: {}", e);
351        }
352        assert!(token.is_ok());
353        let token = token.unwrap();
354        let res =
355            verify_biscuit_local(token.clone(), public_key, subject.clone(), resource.clone());
356        assert!(res.is_ok());
357        let biscuit = Biscuit::from(&token, public_key).unwrap();
358        let third_party_request = biscuit.third_party_request().unwrap();
359        let third_party_block = block!(
360            r#"
361            service("res1");
362            "#
363        );
364        let third_party_block = third_party_request
365            .create_block(&chain_key.private(), third_party_block)
366            .unwrap();
367        let attenuated_biscuit = biscuit
368            .append_third_party(chain_key.public(), third_party_block)
369            .unwrap();
370        let attenuated_token = attenuated_biscuit.to_vec().unwrap();
371        let res = verify_service_chain_biscuit_local(
372            attenuated_token,
373            public_key,
374            subject.clone(),
375            resource.clone(),
376            nodes,
377            None,
378        );
379        assert!(res.is_ok());
380    }
381
382    #[test]
383    fn test_service_chain_biscuit_with_component_name() {
384        let subject = "test@test.com".to_owned();
385        let resource = "res1".to_string();
386        let root = KeyPair::new();
387        let public_key = root.public();
388
389        // Create two chain nodes
390        let chain_key1 = KeyPair::new();
391        let chain_public_key1 = hex::encode(chain_key1.public().to_bytes());
392        let chain_public_key1 = format!("ed25519/{}", chain_public_key1);
393        let chain_node1 = ServiceNode {
394            component: "edge_function".to_string(),
395            public_key: chain_public_key1.clone(),
396        };
397
398        let chain_key2 = KeyPair::new();
399        let chain_public_key2 = hex::encode(chain_key2.public().to_bytes());
400        let chain_public_key2 = format!("ed25519/{}", chain_public_key2);
401        let chain_node2 = ServiceNode {
402            component: "middleware".to_string(),
403            public_key: chain_public_key2.clone(),
404        };
405
406        let nodes = vec![chain_node1.clone(), chain_node2.clone()];
407
408        // Create the initial token using the first node
409        let token = create_service_chain_biscuit(
410            subject.clone(),
411            resource.clone(),
412            root,
413            &nodes,
414            TokenTimeConfig::default(),
415        );
416        assert!(token.is_ok());
417        let token = token.unwrap();
418
419        // Create the biscuit and add third-party blocks
420        let biscuit = Biscuit::from(&token, public_key).unwrap();
421        let third_party_request = biscuit.third_party_request().unwrap();
422        let third_party_block = block!(
423            r#"
424                service("res1");
425            "#
426        );
427        let third_party_block = third_party_request
428            .create_block(&chain_key1.private(), third_party_block)
429            .unwrap();
430        let attenuated_biscuit = biscuit
431            .append_third_party(chain_key1.public(), third_party_block)
432            .unwrap();
433        let attenuated_token = attenuated_biscuit.to_vec().unwrap();
434
435        // Test with the "edge_function" component name - should pass
436        // the first node in the service chain checking itself is valid
437        // since it is checking the base biscuit
438        let res = verify_service_chain_biscuit_local(
439            attenuated_token.clone(),
440            public_key,
441            subject.clone(),
442            resource.clone(),
443            nodes.clone(),
444            Some("edge_function".to_string()),
445        );
446        // This should fail - since we're not verifying any nodes when checking up to but not including "edge_function"
447        assert!(res.is_ok());
448
449        // Create a chain with two nodes
450        let nodes = vec![chain_node1.clone(), chain_node2.clone()];
451
452        // Test with "middleware" component - should succeed verifying node1 only
453        let res = verify_service_chain_biscuit_local(
454            attenuated_token.clone(),
455            public_key,
456            subject.clone(),
457            resource.clone(),
458            nodes.clone(),
459            Some("middleware".to_string()),
460        );
461        assert!(res.is_ok());
462    }
463
464    #[test]
465    fn test_service_chain_biscuit_with_nonexistent_component() {
466        let subject = "test@test.com".to_owned();
467        let resource = "res1".to_string();
468        let root = KeyPair::new();
469        let public_key = root.public();
470        let chain_key = KeyPair::new();
471        let chain_public_key = hex::encode(chain_key.public().to_bytes());
472        let chain_public_key = format!("ed25519/{}", chain_public_key);
473        let chain_node = ServiceNode {
474            component: "edge_function".to_string(),
475            public_key: chain_public_key.clone(),
476        };
477        let nodes = vec![chain_node];
478        let token = create_service_chain_biscuit(
479            subject.clone(),
480            resource.clone(),
481            root,
482            &nodes,
483            TokenTimeConfig::default(),
484        );
485        assert!(token.is_ok());
486        let token = token.unwrap();
487
488        let biscuit = Biscuit::from(&token, public_key).unwrap();
489        let third_party_request = biscuit.third_party_request().unwrap();
490        let third_party_block = block!(
491            r#"
492            service("res1");
493            "#
494        );
495        let third_party_block = third_party_request
496            .create_block(&chain_key.private(), third_party_block)
497            .unwrap();
498        let attenuated_biscuit = biscuit
499            .append_third_party(chain_key.public(), third_party_block)
500            .unwrap();
501        let attenuated_token = attenuated_biscuit.to_vec().unwrap();
502
503        // Test with a component name that doesn't exist in the chain
504        let res = verify_service_chain_biscuit_local(
505            attenuated_token,
506            public_key,
507            subject.clone(),
508            resource.clone(),
509            nodes.clone(),
510            Some("nonexistent_component".to_string()),
511        );
512        assert!(res.is_err());
513
514        // Verify the error message contains the component name
515        let err = res.unwrap_err().to_string();
516        assert!(err.contains("nonexistent_component"));
517    }
518
519    #[test]
520    fn test_service_chain_biscuit_with_multiple_nodes() {
521        let subject = "test@test.com".to_owned();
522        let resource = "res1".to_string();
523        let root = KeyPair::new();
524        let public_key = root.public();
525
526        // Create three chain nodes
527        let chain_key1 = KeyPair::new();
528        let chain_public_key1 = hex::encode(chain_key1.public().to_bytes());
529        let chain_public_key1 = format!("ed25519/{}", chain_public_key1);
530        let chain_node1 = ServiceNode {
531            component: "edge_function".to_string(),
532            public_key: chain_public_key1.clone(),
533        };
534
535        let chain_key2 = KeyPair::new();
536        let chain_public_key2 = hex::encode(chain_key2.public().to_bytes());
537        let chain_public_key2 = format!("ed25519/{}", chain_public_key2);
538        let chain_node2 = ServiceNode {
539            component: "middleware".to_string(),
540            public_key: chain_public_key2.clone(),
541        };
542
543        let chain_key3 = KeyPair::new();
544        let chain_public_key3 = hex::encode(chain_key3.public().to_bytes());
545        let chain_public_key3 = format!("ed25519/{}", chain_public_key3);
546        let chain_node3 = ServiceNode {
547            component: "backend".to_string(),
548            public_key: chain_public_key3.clone(),
549        };
550
551        // Create the initial token with the first node
552        let nodes = vec![
553            chain_node1.clone(),
554            chain_node2.clone(),
555            chain_node3.clone(),
556        ];
557        let token = create_service_chain_biscuit(
558            subject.clone(),
559            resource.clone(),
560            root,
561            &nodes,
562            TokenTimeConfig::default(),
563        );
564        assert!(token.is_ok());
565        let token = token.unwrap();
566
567        println!("Created initial token");
568
569        // Create the biscuit and add node1's block
570        let biscuit = Biscuit::from(&token, public_key).unwrap();
571        let third_party_request1 = biscuit.third_party_request().unwrap();
572        let third_party_block1 = block!(
573            r#"
574                service("res1");
575            "#
576        );
577        let third_party_block1 = third_party_request1
578            .create_block(&chain_key1.private(), third_party_block1)
579            .unwrap();
580        let attenuated_biscuit1 = biscuit
581            .append_third_party(chain_key1.public(), third_party_block1)
582            .unwrap();
583
584        // Chain with all three nodes
585        let all_nodes = vec![
586            chain_node1.clone(),
587            chain_node2.clone(),
588            chain_node3.clone(),
589        ];
590        let attenuated_token1 = attenuated_biscuit1.to_vec().unwrap();
591
592        // Test 1: Verify up to but not including middleware
593        // This should verify edge_function only
594        let res = verify_service_chain_biscuit_local(
595            attenuated_token1.clone(),
596            public_key,
597            subject.clone(),
598            resource.clone(),
599            all_nodes.clone(),
600            Some("middleware".to_string()),
601        );
602        assert!(res.is_ok());
603
604        // Test 3: Verify up to but not including backend
605        // This should try to verify both edge_function and middleware
606        // but since the middleware attenuation wasn't added, it will fail
607        let res = verify_service_chain_biscuit_local(
608            attenuated_token1.clone(),
609            public_key,
610            subject.clone(),
611            resource.clone(),
612            all_nodes.clone(),
613            Some("backend".to_string()),
614        );
615        assert!(res.is_err());
616    }
617
618    #[test]
619    fn test_service_chain_biscuit_with_custom_time() {
620        let subject = "test@test.com".to_owned();
621        let resource = "res1".to_string();
622        let root = KeyPair::new();
623        let public_key = root.public();
624        let chain_key = KeyPair::new();
625        let chain_public_key = hex::encode(chain_key.public().to_bytes());
626        let chain_public_key = format!("ed25519/{}", chain_public_key);
627        let chain_node = ServiceNode {
628            component: "edge_function".to_string(),
629            public_key: chain_public_key.clone(),
630        };
631        let nodes = vec![chain_node];
632
633        // Create a valid token with default time configuration (5 minutes)
634        let valid_token = create_service_chain_biscuit_with_time(
635            subject.clone(),
636            resource.clone(),
637            root,
638            &nodes,
639            TokenTimeConfig::default(),
640        );
641        assert!(valid_token.is_ok());
642        let valid_token = valid_token.unwrap().to_vec().unwrap();
643
644        // Verify the valid token works
645        let res = verify_biscuit_local(
646            valid_token.clone(),
647            public_key,
648            subject.clone(),
649            resource.clone(),
650        );
651        assert!(res.is_ok());
652
653        // Create an expired token (start time 6 minutes ago with 5 minute duration)
654        let expired_time_config = TokenTimeConfig {
655            start_time: Some(Utc::now().timestamp() - 360), // 6 minutes ago
656            duration: 300,                                  // 5 minutes
657        };
658
659        // Create a new key pair for the expired token
660        let root2 = KeyPair::new();
661        let public_key2 = root2.public();
662
663        let expired_token = create_service_chain_biscuit_with_time(
664            subject.clone(),
665            resource.clone(),
666            root2,
667            &nodes,
668            expired_time_config,
669        );
670        assert!(expired_token.is_ok());
671        let expired_token = expired_token.unwrap().to_vec().unwrap();
672
673        // Verify expired token fails
674        let res = verify_biscuit_local(expired_token, public_key2, subject, resource);
675        assert!(res.is_err());
676    }
677}