hessra_token/
lib.rs

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