hessra_token/
lib.rs

1//! # Hessra Token
2//!
3//! Core verification library for Hessra authentication tokens.
4//!
5//! This crate provides functionality for creating, verifying and attesting biscuit tokens
6//! used in the Hessra authentication system. It is designed to be WASM-compatible
7//! and has no networking dependencies.
8//!
9//! ## Features
10//!
11//! - Token creation: Create new tokens with configurable time settings
12//! - Token verification: Verify tokens without contacting the authorization server
13//! - Token attestation: Add service node attestations to tokens
14//! - WASM compatibility: Can be compiled to WebAssembly for use in browsers
15//!
16//! ## Usage
17//!
18//! ```no_run
19//! use hessra_token::{create_biscuit, verify_token_local, biscuit_key_from_string, TokenTimeConfig, KeyPair, encode_token};
20//!
21//! fn main() -> Result<(), hessra_token::TokenError> {
22//!     // Create a new token
23//!     let keypair = KeyPair::new();
24//!     let token = create_biscuit(
25//!         "user123".to_string(),
26//!         "resource456".to_string(),
27//!         "read".to_string(),
28//!         keypair,
29//!         TokenTimeConfig::default(),
30//!     ).map_err(|e| hessra_token::TokenError::generic(e.to_string()))?;
31//!     
32//!     // Verify the token
33//!     let token_string = encode_token(&token);
34//!     let public_key = biscuit_key_from_string("ed25519/01234567890abcdef".to_string())?;
35//!     verify_token_local(&token_string, public_key, "user123", "resource456", "read")?;
36//!     
37//!     println!("Token creation and verification successful!");
38//!     Ok(())
39//! }
40//! ```
41
42mod attest;
43mod error;
44mod mint;
45mod utils;
46mod verify;
47
48pub use attest::{
49    add_multi_party_attestation, add_multi_party_attestation_to_token, add_service_node_attestation,
50};
51pub use error::TokenError;
52pub use mint::{
53    create_biscuit, create_multi_party_biscuit, create_multi_party_biscuit_with_time,
54    create_multi_party_token, create_multi_party_token_with_time, create_raw_multi_party_biscuit,
55    create_service_chain_biscuit, create_service_chain_token, create_service_chain_token_with_time,
56    create_token, create_token_with_time, TokenTimeConfig,
57};
58pub use utils::{decode_token, encode_token, parse_token, public_key_from_pem_file};
59pub use verify::{
60    biscuit_key_from_string, verify_biscuit_local, verify_service_chain_biscuit_local, ServiceNode,
61};
62pub use verify::{verify_service_chain_token_local, verify_token_local};
63
64// Re-export biscuit types that are needed for public API
65pub use biscuit_auth::{Biscuit, KeyPair, PublicKey};
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use biscuit_auth::macros::biscuit;
71    use serde_json::Value;
72    use std::fs;
73
74    #[test]
75    fn test_verify_biscuit_local() {
76        // Create a test keypair
77        let keypair = KeyPair::new();
78        let public_key = keypair.public();
79
80        // Create a simple test biscuit
81        let biscuit_builder = biscuit!(
82            r#"
83                right("alice", "resource1", "read");
84            "#
85        );
86        let biscuit = biscuit_builder.build(&keypair).unwrap();
87        let token_bytes = biscuit.to_vec().unwrap();
88
89        // Verify the biscuit
90        let result = verify_biscuit_local(
91            token_bytes,
92            public_key,
93            "alice".to_string(),
94            "resource1".to_string(),
95            "read".to_string(),
96        );
97        assert!(result.is_ok());
98    }
99
100    #[test]
101    fn test_verify_service_chain_biscuit() {
102        // Create test keypairs
103        let root_keypair = KeyPair::new();
104        let service_keypair = KeyPair::new();
105        let service_public_key_hex = hex::encode(service_keypair.public().to_bytes());
106        let service_public_key_str = format!("ed25519/{}", service_public_key_hex);
107
108        // Create a simple test biscuit with separate node facts
109        let biscuit_builder = biscuit!(
110            r#"
111                right("alice", "resource1", "write");
112                node("resource1", "service1");
113            "#
114        );
115        let biscuit = biscuit_builder.build(&root_keypair).unwrap();
116        let token_bytes = biscuit.to_vec().unwrap();
117
118        // Define service nodes
119        let service_nodes = vec![ServiceNode {
120            component: "service1".to_string(),
121            public_key: service_public_key_str,
122        }];
123
124        // Verify the biscuit with service chain
125        let result = verify_service_chain_biscuit_local(
126            token_bytes,
127            root_keypair.public(),
128            "alice".to_string(),
129            "resource1".to_string(),
130            "write".to_string(),
131            service_nodes,
132            None,
133        );
134        assert!(result.is_ok());
135    }
136
137    #[test]
138    fn test_add_service_node_attestation() {
139        // Create test keypairs
140        let root_keypair = KeyPair::new();
141        let service_keypair = KeyPair::new();
142
143        // Create a simple test biscuit
144        let biscuit_builder = biscuit!(
145            r#"
146                right("alice", "resource1", "read");
147                right("alice", "resource1", "write");
148            "#
149        );
150        let biscuit = biscuit_builder.build(&root_keypair).unwrap();
151        let token_bytes = biscuit.to_vec().unwrap();
152
153        // Add service node attestation
154        let attested_token = add_service_node_attestation(
155            token_bytes,
156            root_keypair.public(),
157            "resource1",
158            &service_keypair,
159        );
160        assert!(attested_token.is_ok());
161
162        // Verify the biscuit still works
163        let result = verify_biscuit_local(
164            attested_token.unwrap(),
165            root_keypair.public(),
166            "alice".to_string(),
167            "resource1".to_string(),
168            "read".to_string(),
169        );
170        assert!(result.is_ok());
171    }
172
173    #[test]
174    fn test_base64_utils() {
175        // Create a test keypair and biscuit
176        let keypair = KeyPair::new();
177        let biscuit_builder = biscuit!(
178            r#"
179                right("alice", "resource1", "read");
180            "#
181        );
182        let biscuit = biscuit_builder.build(&keypair).unwrap();
183        let original_bytes = biscuit.to_vec().unwrap();
184
185        // Test encoding
186        let encoded = encode_token(&original_bytes);
187        assert!(!encoded.is_empty());
188
189        // Test decoding
190        let decoded = decode_token(&encoded).unwrap();
191        assert_eq!(original_bytes, decoded);
192
193        // Test decoding with invalid input
194        let result = decode_token("invalid-base64!");
195        assert!(result.is_err());
196    }
197
198    #[test]
199    fn test_verify_token_string() {
200        // Create a test keypair and biscuit
201        let keypair = KeyPair::new();
202        let biscuit_builder = biscuit!(
203            r#"
204                right("alice", "resource1", "read");
205                right("alice", "resource1", "write");
206            "#
207        );
208        let biscuit = biscuit_builder.build(&keypair).unwrap();
209        let token_bytes = biscuit.to_vec().unwrap();
210        let token_string = encode_token(&token_bytes);
211
212        // Test verify_token
213        let result = verify_token_local(
214            &token_string,
215            keypair.public(),
216            "alice",
217            "resource1",
218            "read",
219        );
220        assert!(result.is_ok());
221
222        // Test with invalid subject
223        let result =
224            verify_token_local(&token_string, keypair.public(), "bob", "resource1", "read");
225        assert!(result.is_err());
226    }
227
228    #[test]
229    fn test_token_verification_from_json() {
230        // Load the test tokens from JSON
231        let json_data =
232            fs::read_to_string("tests/test_tokens.json").expect("Failed to read test_tokens.json");
233        let tokens: Value =
234            serde_json::from_str(&json_data).expect("Failed to parse test_tokens.json");
235
236        // Load the public key
237        let public_key = public_key_from_pem_file("tests/hessra_key.pem")
238            .expect("Failed to load test public key");
239
240        // Test each token
241        for token_value in tokens["tokens"].as_array().unwrap() {
242            let name = token_value["name"].as_str().unwrap();
243            let token_string = token_value["token"].as_str().unwrap();
244            let metadata = &token_value["metadata"];
245
246            // Get values from metadata
247            let subject = metadata["subject"].as_str().unwrap();
248            let resource = metadata["resource"].as_str().unwrap();
249            let expected_result = metadata["expected_result"].as_bool().unwrap();
250            let description = metadata["description"].as_str().unwrap_or("No description");
251
252            println!("Testing token '{}': {}", name, description);
253
254            // Verify the token
255            let result = parse_token(token_string, public_key).and_then(|biscuit| {
256                // Print the token blocks for debugging
257                println!("Token blocks: {}", biscuit.print());
258
259                if metadata["type"].as_str().unwrap() == "singleton" {
260                    verify_token_local(token_string, public_key, subject, resource, "read")
261                } else {
262                    // Create test service nodes
263                    let service_nodes = vec![
264                        ServiceNode {
265                            component: "auth_service".to_string(),
266                            public_key: "ed25519/0123456789abcdef0123456789abcdef".to_string(),
267                        },
268                        ServiceNode {
269                            component: "payment_service".to_string(),
270                            public_key: "ed25519/fedcba9876543210fedcba9876543210".to_string(),
271                        },
272                    ];
273
274                    verify_service_chain_token_local(
275                        token_string,
276                        public_key,
277                        subject,
278                        resource,
279                        "read",
280                        service_nodes,
281                        None,
282                    )
283                }
284            });
285
286            // Check if the result matches expectations
287            let verification_succeeded = result.is_ok();
288            assert_eq!(
289                verification_succeeded, expected_result,
290                "Token '{}' verification resulted in {}, expected: {} - {}",
291                name, verification_succeeded, expected_result, description
292            );
293
294            println!(
295                "✓ Token '{}' - Verification: {}",
296                name,
297                if verification_succeeded == expected_result {
298                    "PASSED"
299                } else {
300                    "FAILED"
301                }
302            );
303        }
304    }
305
306    #[test]
307    fn test_service_chain_tokens_from_json() {
308        // Load the test tokens from JSON
309        let json_data =
310            fs::read_to_string("tests/test_tokens.json").expect("Failed to read test_tokens.json");
311        let tokens: Value =
312            serde_json::from_str(&json_data).expect("Failed to parse test_tokens.json");
313
314        // Load the public key
315        let public_key = public_key_from_pem_file("tests/hessra_key.pem")
316            .expect("Failed to load test public key");
317
318        // Find the service chain token (order_service)
319        if let Some(tokens_array) = tokens["tokens"].as_array() {
320            if let Some(order_service_token) = tokens_array
321                .iter()
322                .find(|t| t["name"].as_str().unwrap() == "argo-cli1_access_order_service")
323            {
324                let token_string = order_service_token["token"].as_str().unwrap();
325                let subject = order_service_token["metadata"]["subject"].as_str().unwrap();
326                let resource = order_service_token["metadata"]["resource"]
327                    .as_str()
328                    .unwrap();
329                let expected_result = order_service_token["metadata"]["expected_result"]
330                    .as_bool()
331                    .unwrap();
332
333                // Create test service nodes
334                let service_nodes = vec![
335                    ServiceNode {
336                        component: "auth_service".to_string(),
337                        public_key: "ed25519/0123456789abcdef0123456789abcdef".to_string(),
338                    },
339                    ServiceNode {
340                        component: "payment_service".to_string(),
341                        public_key: "ed25519/fedcba9876543210fedcba9876543210".to_string(),
342                    },
343                ];
344
345                // Test the token with service chain verification
346                let result = verify_service_chain_token_local(
347                    token_string,
348                    public_key,
349                    subject,
350                    resource,
351                    "read",
352                    service_nodes,
353                    None,
354                );
355
356                // The test should fail because service attestations haven't been added
357                assert_eq!(
358                    result.is_ok(),
359                    expected_result,
360                    "Service chain verification for '{}' resulted in {}, expected: {}",
361                    order_service_token["name"].as_str().unwrap(),
362                    result.is_ok(),
363                    expected_result
364                );
365            }
366        }
367    }
368
369    #[test]
370    fn test_service_chain_lifecycle() {
371        // Load test data from service_chain_tokens.json
372        let json_data = fs::read_to_string("tests/service_chain_tokens.json")
373            .expect("Failed to read service_chain_tokens.json");
374        let tokens: Value =
375            serde_json::from_str(&json_data).expect("Failed to parse service_chain_tokens.json");
376
377        // Extract tokens for each stage
378        let initial_token = tokens["tokens"][0]["token"].as_str().unwrap();
379        let token_after_auth = tokens["tokens"][1]["token"].as_str().unwrap();
380        let token_after_payment = tokens["tokens"][2]["token"].as_str().unwrap();
381        let final_token = tokens["tokens"][3]["token"].as_str().unwrap();
382
383        // Get service details
384        let subject = "uri:urn:test:argo-cli1";
385        let resource = "order_service";
386
387        // Load the public key from the PEM file
388        let root_public_key = public_key_from_pem_file("tests/hessra_key.pem")
389            .expect("Failed to load test public key");
390
391        // Parse the public keys from the JSON
392        let auth_service_pk_str = tokens["tokens"][1]["metadata"]["service_nodes"][0]["public_key"]
393            .as_str()
394            .unwrap();
395        let payment_service_pk_str = tokens["tokens"][2]["metadata"]["service_nodes"][1]
396            ["public_key"]
397            .as_str()
398            .unwrap();
399        let order_service_pk_str = tokens["tokens"][3]["metadata"]["service_nodes"][2]
400            ["public_key"]
401            .as_str()
402            .unwrap();
403
404        // For this test we'll just skip token generation since we're using pre-made tokens
405        // and focus on the verification of the service chain tokens
406
407        // Step 1: Verify initial token as a regular token
408        let result = verify_token_local(initial_token, root_public_key, subject, resource, "read");
409        assert!(result.is_ok(), "Initial token verification failed");
410
411        // Step 2: Payment Service verifies token with auth service attestation
412        let service_nodes_for_payment = vec![ServiceNode {
413            component: "auth_service".to_string(),
414            public_key: auth_service_pk_str.to_string(),
415        }];
416
417        let result = verify_service_chain_token_local(
418            token_after_auth,
419            root_public_key,
420            subject,
421            resource,
422            "read",
423            service_nodes_for_payment.clone(),
424            None,
425        );
426        assert!(
427            result.is_ok(),
428            "Token with auth attestation verification failed"
429        );
430
431        // Step 3: Order Service verifies token with auth and payment attestations
432        let service_nodes_for_order = vec![
433            ServiceNode {
434                component: "auth_service".to_string(),
435                public_key: auth_service_pk_str.to_string(),
436            },
437            ServiceNode {
438                component: "payment_service".to_string(),
439                public_key: payment_service_pk_str.to_string(),
440            },
441        ];
442
443        let result = verify_service_chain_token_local(
444            token_after_payment,
445            root_public_key,
446            subject,
447            resource,
448            "read",
449            service_nodes_for_order.clone(),
450            None,
451        );
452        assert!(
453            result.is_ok(),
454            "Token with payment attestation verification failed"
455        );
456
457        // Step 4: Final verification of the complete service chain token
458        let service_nodes_complete = vec![
459            ServiceNode {
460                component: "auth_service".to_string(),
461                public_key: auth_service_pk_str.to_string(),
462            },
463            ServiceNode {
464                component: "payment_service".to_string(),
465                public_key: payment_service_pk_str.to_string(),
466            },
467            ServiceNode {
468                component: "order_service".to_string(),
469                public_key: order_service_pk_str.to_string(),
470            },
471        ];
472
473        let result = verify_service_chain_token_local(
474            final_token,
475            root_public_key,
476            subject,
477            resource,
478            "read",
479            service_nodes_complete.clone(),
480            None,
481        );
482
483        // Print more details if verification fails
484        if result.is_err() {
485            println!("Error details: {:?}", result);
486
487            // Parse the token to check its blocks
488            let decoded_final = decode_token(final_token).unwrap();
489            if let Ok(biscuit) = Biscuit::from(&decoded_final, root_public_key) {
490                println!("Token blocks: {}", biscuit.print());
491            } else {
492                println!("Failed to parse token");
493            }
494        }
495
496        assert!(result.is_ok(), "Final token verification failed");
497
498        // Verify that token not attested by the full chain fails authorization against the full chain
499        let result = verify_service_chain_token_local(
500            token_after_auth,
501            root_public_key,
502            subject,
503            resource,
504            "read",
505            service_nodes_complete,
506            None,
507        );
508        // This should fail because we're missing attestations from payment and order service
509        assert!(
510            result.is_err(),
511            "Incomplete service chain should be rejected"
512        );
513    }
514
515    #[test]
516    fn test_multi_party_token_verification_lifecycle() {
517        let subject = "test@test.com".to_owned();
518        let resource = "res1".to_string();
519        let operation = "read".to_string();
520        let root = KeyPair::new();
521        let public_key = root.public();
522
523        // Create a multi-party node that must attest to the token
524        let approval_service_key = KeyPair::new();
525        let approval_service_public_key = hex::encode(approval_service_key.public().to_bytes());
526        let approval_service_public_key = format!("ed25519/{}", approval_service_public_key);
527        let approval_service_node = ServiceNode {
528            component: "approval_service".to_string(),
529            public_key: approval_service_public_key.clone(),
530        };
531        let nodes = vec![approval_service_node];
532
533        // Step 1: Create a new multi-party token successfully
534        let token = create_multi_party_biscuit(
535            subject.clone(),
536            resource.clone(),
537            operation.clone(),
538            root,
539            &nodes,
540        );
541        assert!(token.is_ok(), "Failed to create multi-party token");
542        let token = token.unwrap();
543        let token_string = encode_token(&token);
544
545        println!("✓ Multi-party token created successfully");
546
547        // Step 2: Show that the multi-party token fails to verify without attestation
548        let result = verify_token_local(&token_string, public_key, &subject, &resource, &operation);
549        assert!(
550            result.is_err(),
551            "Multi-party token should fail verification without attestation"
552        );
553        println!("✓ Unattested multi-party token correctly failed verification");
554
555        // Step 3: Attest the token as the approval service
556        let attested_token = add_multi_party_attestation(
557            token,
558            public_key,
559            "approval_service".to_string(),
560            approval_service_key,
561        );
562        assert!(
563            attested_token.is_ok(),
564            "Failed to add multi-party attestation"
565        );
566        let attested_token = attested_token.unwrap();
567        let attested_token_string = encode_token(&attested_token);
568
569        println!("✓ Multi-party attestation added successfully");
570
571        // Step 4: Show that the token now verifies/authorizes
572        let result = verify_token_local(
573            &attested_token_string,
574            public_key,
575            &subject,
576            &resource,
577            &operation,
578        );
579        assert!(
580            result.is_ok(),
581            "Attested multi-party token should pass verification"
582        );
583        println!("✓ Attested multi-party token correctly passed verification");
584
585        // Additional test: Verify that the wrong namespace attestation fails
586        let wrong_service_key = KeyPair::new();
587        let wrong_attested_token = add_multi_party_attestation(
588            decode_token(&token_string).unwrap(),
589            public_key,
590            "wrong_service".to_string(),
591            wrong_service_key,
592        );
593        assert!(wrong_attested_token.is_ok(), "Attestation should succeed");
594        let wrong_attested_token_string = encode_token(&wrong_attested_token.unwrap());
595
596        let result = verify_token_local(
597            &wrong_attested_token_string,
598            public_key,
599            &subject,
600            &resource,
601            &operation,
602        );
603        assert!(
604            result.is_err(),
605            "Token attested by wrong namespace should fail verification"
606        );
607        println!("✓ Token attested by wrong namespace correctly failed verification");
608    }
609
610    #[test]
611    fn test_multi_party_token_with_multiple_parties() {
612        let subject = "test@test.com".to_owned();
613        let resource = "sensitive_resource".to_string();
614        let operation = "admin".to_string();
615        let root = KeyPair::new();
616        let public_key = root.public();
617
618        // Create two multi-party nodes that must both attest to the token
619        let legal_dept_key = KeyPair::new();
620        let legal_dept_public_key = hex::encode(legal_dept_key.public().to_bytes());
621        let legal_dept_public_key = format!("ed25519/{}", legal_dept_public_key);
622        let legal_dept_node = ServiceNode {
623            component: "legal_dept".to_string(),
624            public_key: legal_dept_public_key.clone(),
625        };
626
627        let security_team_key = KeyPair::new();
628        let security_team_public_key = hex::encode(security_team_key.public().to_bytes());
629        let security_team_public_key = format!("ed25519/{}", security_team_public_key);
630        let security_team_node = ServiceNode {
631            component: "security_team".to_string(),
632            public_key: security_team_public_key.clone(),
633        };
634
635        let nodes = vec![legal_dept_node, security_team_node];
636
637        // Create a multi-party token requiring both attestations
638        let token = create_multi_party_biscuit(
639            subject.clone(),
640            resource.clone(),
641            operation.clone(),
642            root,
643            &nodes,
644        );
645        assert!(token.is_ok(), "Failed to create multi-party token");
646        let token = token.unwrap();
647        let token_string = encode_token(&token);
648
649        // Verify token fails without any attestations
650        let result = verify_token_local(&token_string, public_key, &subject, &resource, &operation);
651        assert!(result.is_err(), "Token should fail without attestations");
652
653        // Add attestation from legal department only
654        let partially_attested_token = add_multi_party_attestation(
655            decode_token(&token_string).unwrap(),
656            public_key,
657            "legal_dept".to_string(),
658            legal_dept_key,
659        )
660        .unwrap();
661        let partially_attested_token_string = encode_token(&partially_attested_token);
662
663        // Verify token still fails with only one attestation
664        let result = verify_token_local(
665            &partially_attested_token_string,
666            public_key,
667            &subject,
668            &resource,
669            &operation,
670        );
671        assert!(
672            result.is_err(),
673            "Token should fail with only one of two required attestations"
674        );
675
676        // Add attestation from security team
677        let fully_attested_token = add_multi_party_attestation(
678            partially_attested_token,
679            public_key,
680            "security_team".to_string(),
681            security_team_key,
682        )
683        .unwrap();
684        let fully_attested_token_string = encode_token(&fully_attested_token);
685
686        // Now the token should pass verification
687        let result = verify_token_local(
688            &fully_attested_token_string,
689            public_key,
690            &subject,
691            &resource,
692            &operation,
693        );
694        assert!(
695            result.is_ok(),
696            "Token should pass with both required attestations"
697        );
698        println!("✓ Multi-party token with multiple parties verified successfully");
699    }
700}