Skip to main content

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