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