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::add_service_node_attestation;
49pub use error::TokenError;
50pub use mint::{
51    create_biscuit, create_service_chain_biscuit, create_service_chain_token,
52    create_service_chain_token_with_time, create_token, create_token_with_time, TokenTimeConfig,
53};
54pub use utils::{decode_token, encode_token, parse_token, public_key_from_pem_file};
55pub use verify::{
56    biscuit_key_from_string, verify_biscuit_local, verify_service_chain_biscuit_local, ServiceNode,
57};
58pub use verify::{verify_service_chain_token_local, verify_token_local};
59
60// Re-export biscuit types that are needed for public API
61pub use biscuit_auth::{Biscuit, KeyPair, PublicKey};
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use biscuit_auth::macros::biscuit;
67    use serde_json::Value;
68    use std::fs;
69
70    #[test]
71    fn test_verify_biscuit_local() {
72        // Create a test keypair
73        let keypair = KeyPair::new();
74        let public_key = keypair.public();
75
76        // Create a simple test biscuit
77        let biscuit_builder = biscuit!(
78            r#"
79                right("alice", "resource1", "read");
80            "#
81        );
82        let biscuit = biscuit_builder.build(&keypair).unwrap();
83        let token_bytes = biscuit.to_vec().unwrap();
84
85        // Verify the biscuit
86        let result = verify_biscuit_local(
87            token_bytes,
88            public_key,
89            "alice".to_string(),
90            "resource1".to_string(),
91            "read".to_string(),
92        );
93        assert!(result.is_ok());
94    }
95
96    #[test]
97    fn test_verify_service_chain_biscuit() {
98        // Create test keypairs
99        let root_keypair = KeyPair::new();
100        let service_keypair = KeyPair::new();
101        let service_public_key_hex = hex::encode(service_keypair.public().to_bytes());
102        let service_public_key_str = format!("ed25519/{}", service_public_key_hex);
103
104        // Create a simple test biscuit with separate node facts
105        let biscuit_builder = biscuit!(
106            r#"
107                right("alice", "resource1", "write");
108                node("resource1", "service1");
109            "#
110        );
111        let biscuit = biscuit_builder.build(&root_keypair).unwrap();
112        let token_bytes = biscuit.to_vec().unwrap();
113
114        // Define service nodes
115        let service_nodes = vec![ServiceNode {
116            component: "service1".to_string(),
117            public_key: service_public_key_str,
118        }];
119
120        // Verify the biscuit with service chain
121        let result = verify_service_chain_biscuit_local(
122            token_bytes,
123            root_keypair.public(),
124            "alice".to_string(),
125            "resource1".to_string(),
126            "write".to_string(),
127            service_nodes,
128            None,
129        );
130        assert!(result.is_ok());
131    }
132
133    #[test]
134    fn test_add_service_node_attestation() {
135        // Create test keypairs
136        let root_keypair = KeyPair::new();
137        let service_keypair = KeyPair::new();
138
139        // Create a simple test biscuit
140        let biscuit_builder = biscuit!(
141            r#"
142                right("alice", "resource1", "read");
143                right("alice", "resource1", "write");
144            "#
145        );
146        let biscuit = biscuit_builder.build(&root_keypair).unwrap();
147        let token_bytes = biscuit.to_vec().unwrap();
148
149        // Add service node attestation
150        let attested_token = add_service_node_attestation(
151            token_bytes,
152            root_keypair.public(),
153            "resource1",
154            &service_keypair,
155        );
156        assert!(attested_token.is_ok());
157
158        // Verify the biscuit still works
159        let result = verify_biscuit_local(
160            attested_token.unwrap(),
161            root_keypair.public(),
162            "alice".to_string(),
163            "resource1".to_string(),
164            "read".to_string(),
165        );
166        assert!(result.is_ok());
167    }
168
169    #[test]
170    fn test_base64_utils() {
171        // Create a test keypair and biscuit
172        let keypair = KeyPair::new();
173        let biscuit_builder = biscuit!(
174            r#"
175                right("alice", "resource1", "read");
176            "#
177        );
178        let biscuit = biscuit_builder.build(&keypair).unwrap();
179        let original_bytes = biscuit.to_vec().unwrap();
180
181        // Test encoding
182        let encoded = encode_token(&original_bytes);
183        assert!(!encoded.is_empty());
184
185        // Test decoding
186        let decoded = decode_token(&encoded).unwrap();
187        assert_eq!(original_bytes, decoded);
188
189        // Test decoding with invalid input
190        let result = decode_token("invalid-base64!");
191        assert!(result.is_err());
192    }
193
194    #[test]
195    fn test_verify_token_string() {
196        // Create a test keypair and biscuit
197        let keypair = KeyPair::new();
198        let biscuit_builder = biscuit!(
199            r#"
200                right("alice", "resource1", "read");
201                right("alice", "resource1", "write");
202            "#
203        );
204        let biscuit = biscuit_builder.build(&keypair).unwrap();
205        let token_bytes = biscuit.to_vec().unwrap();
206        let token_string = encode_token(&token_bytes);
207
208        // Test verify_token
209        let result = verify_token_local(
210            &token_string,
211            keypair.public(),
212            "alice",
213            "resource1",
214            "read",
215        );
216        assert!(result.is_ok());
217
218        // Test with invalid subject
219        let result =
220            verify_token_local(&token_string, keypair.public(), "bob", "resource1", "read");
221        assert!(result.is_err());
222    }
223
224    #[test]
225    fn test_token_verification_from_json() {
226        // Load the test tokens from JSON
227        let json_data =
228            fs::read_to_string("tests/test_tokens.json").expect("Failed to read test_tokens.json");
229        let tokens: Value =
230            serde_json::from_str(&json_data).expect("Failed to parse test_tokens.json");
231
232        // Load the public key
233        let public_key = public_key_from_pem_file("tests/hessra_key.pem")
234            .expect("Failed to load test public key");
235
236        // Test each token
237        for token_value in tokens["tokens"].as_array().unwrap() {
238            let name = token_value["name"].as_str().unwrap();
239            let token_string = token_value["token"].as_str().unwrap();
240            let metadata = &token_value["metadata"];
241
242            // Get values from metadata
243            let subject = metadata["subject"].as_str().unwrap();
244            let resource = metadata["resource"].as_str().unwrap();
245            let expected_result = metadata["expected_result"].as_bool().unwrap();
246            let description = metadata["description"].as_str().unwrap_or("No description");
247
248            println!("Testing token '{}': {}", name, description);
249
250            // Verify the token
251            let result = parse_token(token_string, public_key).and_then(|biscuit| {
252                // Print the token blocks for debugging
253                println!("Token blocks: {}", biscuit.print());
254
255                if metadata["type"].as_str().unwrap() == "singleton" {
256                    verify_token_local(token_string, public_key, subject, resource, "read")
257                } else {
258                    // Create test service nodes
259                    let service_nodes = vec![
260                        ServiceNode {
261                            component: "auth_service".to_string(),
262                            public_key: "ed25519/0123456789abcdef0123456789abcdef".to_string(),
263                        },
264                        ServiceNode {
265                            component: "payment_service".to_string(),
266                            public_key: "ed25519/fedcba9876543210fedcba9876543210".to_string(),
267                        },
268                    ];
269
270                    verify_service_chain_token_local(
271                        token_string,
272                        public_key,
273                        subject,
274                        resource,
275                        "read",
276                        service_nodes,
277                        None,
278                    )
279                }
280            });
281
282            // Check if the result matches expectations
283            let verification_succeeded = result.is_ok();
284            assert_eq!(
285                verification_succeeded, expected_result,
286                "Token '{}' verification resulted in {}, expected: {} - {}",
287                name, verification_succeeded, expected_result, description
288            );
289
290            println!(
291                "✓ Token '{}' - Verification: {}",
292                name,
293                if verification_succeeded == expected_result {
294                    "PASSED"
295                } else {
296                    "FAILED"
297                }
298            );
299        }
300    }
301
302    #[test]
303    fn test_service_chain_tokens_from_json() {
304        // Load the test tokens from JSON
305        let json_data =
306            fs::read_to_string("tests/test_tokens.json").expect("Failed to read test_tokens.json");
307        let tokens: Value =
308            serde_json::from_str(&json_data).expect("Failed to parse test_tokens.json");
309
310        // Load the public key
311        let public_key = public_key_from_pem_file("tests/hessra_key.pem")
312            .expect("Failed to load test public key");
313
314        // Find the service chain token (order_service)
315        if let Some(tokens_array) = tokens["tokens"].as_array() {
316            if let Some(order_service_token) = tokens_array
317                .iter()
318                .find(|t| t["name"].as_str().unwrap() == "argo-cli1_access_order_service")
319            {
320                let token_string = order_service_token["token"].as_str().unwrap();
321                let subject = order_service_token["metadata"]["subject"].as_str().unwrap();
322                let resource = order_service_token["metadata"]["resource"]
323                    .as_str()
324                    .unwrap();
325                let expected_result = order_service_token["metadata"]["expected_result"]
326                    .as_bool()
327                    .unwrap();
328
329                // Create test service nodes
330                let service_nodes = vec![
331                    ServiceNode {
332                        component: "auth_service".to_string(),
333                        public_key: "ed25519/0123456789abcdef0123456789abcdef".to_string(),
334                    },
335                    ServiceNode {
336                        component: "payment_service".to_string(),
337                        public_key: "ed25519/fedcba9876543210fedcba9876543210".to_string(),
338                    },
339                ];
340
341                // Test the token with service chain verification
342                let result = verify_service_chain_token_local(
343                    token_string,
344                    public_key,
345                    subject,
346                    resource,
347                    "read",
348                    service_nodes,
349                    None,
350                );
351
352                // The test should fail because service attestations haven't been added
353                assert_eq!(
354                    result.is_ok(),
355                    expected_result,
356                    "Service chain verification for '{}' resulted in {}, expected: {}",
357                    order_service_token["name"].as_str().unwrap(),
358                    result.is_ok(),
359                    expected_result
360                );
361            }
362        }
363    }
364
365    #[test]
366    fn test_service_chain_lifecycle() {
367        // Load test data from service_chain_tokens.json
368        let json_data = fs::read_to_string("tests/service_chain_tokens.json")
369            .expect("Failed to read service_chain_tokens.json");
370        let tokens: Value =
371            serde_json::from_str(&json_data).expect("Failed to parse service_chain_tokens.json");
372
373        // Extract tokens for each stage
374        let initial_token = tokens["tokens"][0]["token"].as_str().unwrap();
375        let token_after_auth = tokens["tokens"][1]["token"].as_str().unwrap();
376        let token_after_payment = tokens["tokens"][2]["token"].as_str().unwrap();
377        let final_token = tokens["tokens"][3]["token"].as_str().unwrap();
378
379        // Get service details
380        let subject = "uri:urn:test:argo-cli1";
381        let resource = "order_service";
382
383        // Load the public key from the PEM file
384        let root_public_key = public_key_from_pem_file("tests/hessra_key.pem")
385            .expect("Failed to load test public key");
386
387        // Parse the public keys from the JSON
388        let auth_service_pk_str = tokens["tokens"][1]["metadata"]["service_nodes"][0]["public_key"]
389            .as_str()
390            .unwrap();
391        let payment_service_pk_str = tokens["tokens"][2]["metadata"]["service_nodes"][1]
392            ["public_key"]
393            .as_str()
394            .unwrap();
395        let order_service_pk_str = tokens["tokens"][3]["metadata"]["service_nodes"][2]
396            ["public_key"]
397            .as_str()
398            .unwrap();
399
400        // For this test we'll just skip token generation since we're using pre-made tokens
401        // and focus on the verification of the service chain tokens
402
403        // Step 1: Verify initial token as a regular token
404        let result = verify_token_local(initial_token, root_public_key, subject, resource, "read");
405        assert!(result.is_ok(), "Initial token verification failed");
406
407        // Step 2: Payment Service verifies token with auth service attestation
408        let service_nodes_for_payment = vec![ServiceNode {
409            component: "auth_service".to_string(),
410            public_key: auth_service_pk_str.to_string(),
411        }];
412
413        let result = verify_service_chain_token_local(
414            token_after_auth,
415            root_public_key,
416            subject,
417            resource,
418            "read",
419            service_nodes_for_payment.clone(),
420            None,
421        );
422        assert!(
423            result.is_ok(),
424            "Token with auth attestation verification failed"
425        );
426
427        // Step 3: Order Service verifies token with auth and payment attestations
428        let service_nodes_for_order = vec![
429            ServiceNode {
430                component: "auth_service".to_string(),
431                public_key: auth_service_pk_str.to_string(),
432            },
433            ServiceNode {
434                component: "payment_service".to_string(),
435                public_key: payment_service_pk_str.to_string(),
436            },
437        ];
438
439        let result = verify_service_chain_token_local(
440            token_after_payment,
441            root_public_key,
442            subject,
443            resource,
444            "read",
445            service_nodes_for_order.clone(),
446            None,
447        );
448        assert!(
449            result.is_ok(),
450            "Token with payment attestation verification failed"
451        );
452
453        // Step 4: Final verification of the complete service chain token
454        let service_nodes_complete = vec![
455            ServiceNode {
456                component: "auth_service".to_string(),
457                public_key: auth_service_pk_str.to_string(),
458            },
459            ServiceNode {
460                component: "payment_service".to_string(),
461                public_key: payment_service_pk_str.to_string(),
462            },
463            ServiceNode {
464                component: "order_service".to_string(),
465                public_key: order_service_pk_str.to_string(),
466            },
467        ];
468
469        let result = verify_service_chain_token_local(
470            final_token,
471            root_public_key,
472            subject,
473            resource,
474            "read",
475            service_nodes_complete.clone(),
476            None,
477        );
478
479        // Print more details if verification fails
480        if result.is_err() {
481            println!("Error details: {:?}", result);
482
483            // Parse the token to check its blocks
484            let decoded_final = decode_token(final_token).unwrap();
485            if let Ok(biscuit) = Biscuit::from(&decoded_final, root_public_key) {
486                println!("Token blocks: {}", biscuit.print());
487            } else {
488                println!("Failed to parse token");
489            }
490        }
491
492        assert!(result.is_ok(), "Final token verification failed");
493
494        // Verify that token not attested by the full chain fails authorization against the full chain
495        let result = verify_service_chain_token_local(
496            token_after_auth,
497            root_public_key,
498            subject,
499            resource,
500            "read",
501            service_nodes_complete,
502            None,
503        );
504        // This should fail because we're missing attestations from payment and order service
505        assert!(
506            result.is_err(),
507            "Incomplete service chain should be rejected"
508        );
509    }
510}