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