hessra_token_identity/
lib.rs

1mod attenuate;
2mod inspect;
3mod jit;
4mod mint;
5mod revocation;
6mod verify;
7
8pub use attenuate::add_identity_attenuation_to_token;
9pub use inspect::{inspect_identity_token, InspectResult};
10pub use jit::create_short_lived_identity_token;
11pub use mint::{
12    create_domain_restricted_identity_token, create_identity_token,
13    create_non_delegatable_identity_token, HessraIdentity,
14};
15pub use revocation::{
16    get_active_identity_revocation, get_identity_revocations, IdentityRevocation,
17};
18pub use verify::{verify_bearer_token, verify_identity_token, IdentityVerifier};
19
20#[cfg(test)]
21mod tests {
22    use super::*;
23    use hessra_token_core::{KeyPair, TokenTimeConfig};
24
25    #[test]
26    fn test_basic_identity_token_creation_and_verification() {
27        // Create a keypair for signing
28        let keypair = KeyPair::new();
29        let public_key = keypair.public();
30
31        // Test 1: Create and verify non-delegatable realm identity token with exact match
32        let subject = "urn:hessra:alice".to_string();
33        let token = HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
34            .issue(&keypair)
35            .expect("Failed to create identity token");
36
37        // Should pass with exact identity
38        assert!(
39            verify_identity_token(token.clone(), public_key, subject.clone()).is_ok(),
40            "Verification should succeed with exact identity match"
41        );
42
43        // Should pass verification as a bearer token
44        assert!(
45            verify_bearer_token(token.clone(), public_key).is_ok(),
46            "Verification should succeed as a bearer token"
47        );
48
49        // Should fail with different identity
50        assert!(
51            verify_identity_token(token.clone(), public_key, "urn:hessra:bob".to_string()).is_err(),
52            "Verification should fail with different identity"
53        );
54
55        // Non-delegatable realm identity: hierarchical identities should NOT work
56        // because the base token only allows exact match: $a == {subject}
57        assert!(
58            verify_identity_token(
59                token.clone(),
60                public_key,
61                "urn:hessra:alice:agent".to_string()
62            )
63            .is_err(),
64            "Hierarchical identity should fail for non-delegatable realm identity"
65        );
66
67        // Test 2: Create and verify delegatable realm identity token
68        let delegatable_token = HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
69            .delegatable(true)
70            .issue(&keypair)
71            .expect("Failed to create delegatable identity token");
72
73        // Exact identity should work
74        assert!(
75            verify_identity_token(delegatable_token.clone(), public_key, subject.clone()).is_ok(),
76            "Exact identity should work with delegatable token"
77        );
78
79        // Hierarchical identities should work with delegatable tokens
80        assert!(
81            verify_identity_token(
82                delegatable_token.clone(),
83                public_key,
84                "urn:hessra:alice:agent".to_string()
85            )
86            .is_ok(),
87            "Hierarchical identity should work with delegatable realm identity"
88        );
89    }
90
91    #[test]
92    fn test_single_level_delegation() {
93        let keypair = KeyPair::new();
94        let public_key = keypair.public();
95
96        // Create base delegatable identity token
97        let base_identity = "urn:hessra:alice".to_string();
98        let token = HessraIdentity::new(base_identity.clone(), TokenTimeConfig::default())
99            .delegatable(true)
100            .issue(&keypair)
101            .expect("Failed to create delegatable identity token");
102
103        // Attenuate to a more specific identity
104        let delegated_identity = "urn:hessra:alice:laptop".to_string();
105        let attenuated_result = add_identity_attenuation_to_token(
106            token.clone(),
107            delegated_identity.clone(),
108            public_key,
109            TokenTimeConfig::default(),
110        );
111
112        let attenuated_token = match attenuated_result {
113            Ok(t) => t,
114            Err(e) => panic!("Failed to attenuate token: {e:?}"),
115        };
116
117        // Original identity should NOT work with attenuated token (delegation restricts usage)
118        let base_verify_result =
119            verify_identity_token(attenuated_token.clone(), public_key, base_identity.clone());
120        assert!(
121            base_verify_result.is_err(),
122            "Base identity should NOT verify with attenuated token - use original token instead"
123        );
124
125        // Delegated identity should work
126        assert!(
127            verify_identity_token(
128                attenuated_token.clone(),
129                public_key,
130                delegated_identity.clone()
131            )
132            .is_ok(),
133            "Delegated identity should verify"
134        );
135
136        // Delegated identities don't work as bearer tokens
137        assert!(
138            verify_bearer_token(attenuated_token.clone(), public_key).is_err(),
139            "Delegated identity should not verify as a bearer token"
140        );
141
142        // Different branch should fail
143        assert!(
144            verify_identity_token(
145                attenuated_token.clone(),
146                public_key,
147                "urn:hessra:alice:phone".to_string()
148            )
149            .is_err(),
150            "Different branch of delegation should fail"
151        );
152
153        // Completely different identity should fail
154        assert!(
155            verify_identity_token(
156                attenuated_token.clone(),
157                public_key,
158                "urn:hessra:bob".to_string()
159            )
160            .is_err(),
161            "Completely different identity should fail"
162        );
163    }
164
165    #[test]
166    fn test_multi_level_delegation_chain() {
167        let keypair = KeyPair::new();
168        let public_key = keypair.public();
169
170        // Create organizational hierarchy
171        let org_identity = "urn:hessra:company".to_string();
172        let dept_identity = "urn:hessra:company:dept_eng".to_string();
173        let user_identity = "urn:hessra:company:dept_eng:alice".to_string();
174        let device_identity = "urn:hessra:company:dept_eng:alice:laptop".to_string();
175
176        // Create base delegatable token for organization
177        let token = HessraIdentity::new(org_identity.clone(), TokenTimeConfig::default())
178            .delegatable(true)
179            .issue(&keypair)
180            .expect("Failed to create delegatable org token");
181
182        // First attenuation: org -> department
183        let token = add_identity_attenuation_to_token(
184            token,
185            dept_identity.clone(),
186            public_key,
187            TokenTimeConfig::default(),
188        )
189        .expect("Failed to attenuate to department");
190
191        // Second attenuation: department -> user
192        let token = add_identity_attenuation_to_token(
193            token,
194            user_identity.clone(),
195            public_key,
196            TokenTimeConfig::default(),
197        )
198        .expect("Failed to attenuate to user");
199
200        // Third attenuation: user -> device
201        let token = add_identity_attenuation_to_token(
202            token,
203            device_identity.clone(),
204            public_key,
205            TokenTimeConfig::default(),
206        )
207        .expect("Failed to attenuate to device");
208
209        // After all attenuations, only the most specific identity should work
210        // (all checks must pass, so we get the intersection)
211        assert!(
212            verify_identity_token(token.clone(), public_key, org_identity).is_err(),
213            "Organization level should NOT work after delegation to device"
214        );
215        assert!(
216            verify_identity_token(token.clone(), public_key, dept_identity).is_err(),
217            "Department level should NOT work after delegation to device"
218        );
219        assert!(
220            verify_identity_token(token.clone(), public_key, user_identity).is_err(),
221            "User level should NOT work after delegation to device"
222        );
223        assert!(
224            verify_identity_token(token.clone(), public_key, device_identity).is_ok(),
225            "Device level SHOULD work - it's the delegated identity"
226        );
227
228        // Different branches should fail
229        assert!(
230            verify_identity_token(
231                token.clone(),
232                public_key,
233                "urn:hessra:company:dept_hr".to_string()
234            )
235            .is_err(),
236            "Different department should fail"
237        );
238        assert!(
239            verify_identity_token(
240                token.clone(),
241                public_key,
242                "urn:hessra:company:dept_eng:bob".to_string()
243            )
244            .is_err(),
245            "Different user in same department should fail"
246        );
247        assert!(
248            verify_identity_token(
249                token.clone(),
250                public_key,
251                "urn:hessra:company:dept_eng:alice:phone".to_string()
252            )
253            .is_err(),
254            "Different device for same user should fail"
255        );
256    }
257
258    #[test]
259    fn test_time_based_expiration() {
260        let identity = "urn:hessra:alice".to_string();
261
262        // Create token that's already expired
263        let expired_config = TokenTimeConfig {
264            start_time: Some(0), // Unix epoch
265            duration: 1,         // 1 second
266        };
267
268        let expired_keypair = KeyPair::new();
269        let expired_public_key = expired_keypair.public();
270        let expired_token = HessraIdentity::new(identity.clone(), expired_config)
271            .issue(&expired_keypair)
272            .expect("Failed to create expired token");
273
274        // Should fail verification due to expiration
275        assert!(
276            verify_identity_token(expired_token, expired_public_key, identity.clone()).is_err(),
277            "Expired token should fail verification"
278        );
279
280        // Create valid base delegatable token with long duration
281        let valid_config = TokenTimeConfig {
282            start_time: None,
283            duration: 3600, // 1 hour
284        };
285
286        let valid_keypair = KeyPair::new();
287        let valid_public_key = valid_keypair.public();
288        let valid_token = HessraIdentity::new(identity.clone(), valid_config)
289            .delegatable(true)
290            .issue(&valid_keypair)
291            .expect("Failed to create valid delegatable token");
292
293        // Attempt to attenuate with already expired time, this should fail because
294        // the attenuation call verifies that the token attenuation is possible.
295        assert!(
296            add_identity_attenuation_to_token(
297                valid_token.clone(),
298                "urn:hessra:alice:laptop".to_string(),
299                valid_public_key, // Use the same public key that signed the token
300                expired_config,
301            )
302            .is_err(),
303            "Attenuating a token with an expired time should fail"
304        );
305    }
306
307    #[test]
308    fn test_uri_validation_edge_cases() {
309        // Test with different URI schemes
310        // Note: Current implementation assumes ":" as hierarchy delimiter
311        let test_cases = vec![
312            ("urn:hessra:user", "urn:hessra:user:device"),
313            (
314                "https://example.com/user",
315                "https://example.com/user:device",
316            ), // Use : for hierarchy
317            ("mailto:user@example.com", "mailto:user@example.com:device"),
318            ("user", "user:device"), // Simple non-URI format
319        ];
320
321        for (base, delegated) in test_cases {
322            // Create a new keypair for each test case
323            let keypair = KeyPair::new();
324            let public_key = keypair.public();
325
326            let token = HessraIdentity::new(base.to_string(), TokenTimeConfig::default())
327                .delegatable(true)
328                .issue(&keypair)
329                .unwrap_or_else(|_| panic!("Failed to create delegatable token for {base}"));
330
331            let attenuated = add_identity_attenuation_to_token(
332                token,
333                delegated.to_string(),
334                public_key, // Use the same public key
335                TokenTimeConfig::default(),
336            )
337            .unwrap_or_else(|_| panic!("Failed to attenuate {base} to {delegated}"));
338
339            // After attenuation, only the delegated identity should work
340            assert!(
341                verify_identity_token(attenuated.clone(), public_key, base.to_string()).is_err(),
342                "Base identity {base} should NOT verify after delegation"
343            );
344            assert!(
345                verify_identity_token(attenuated, public_key, delegated.to_string()).is_ok(),
346                "Delegated identity {delegated} should verify"
347            );
348        }
349    }
350
351    #[test]
352    fn test_prefix_attack_prevention() {
353        let keypair = KeyPair::new();
354        let public_key = keypair.public();
355
356        // Create delegatable token for alice
357        let alice_token =
358            HessraIdentity::new("urn:hessra:alice".to_string(), TokenTimeConfig::default())
359                .delegatable(true)
360                .issue(&keypair)
361                .expect("Failed to create delegatable alice token");
362
363        // alice2 should not be able to verify (even though "alice" is a prefix of "alice2")
364        assert!(
365            verify_identity_token(
366                alice_token.clone(),
367                public_key,
368                "urn:hessra:alice2".to_string()
369            )
370            .is_err(),
371            "alice2 should not verify against alice token"
372        );
373
374        // Create attenuated token
375        let attenuated = add_identity_attenuation_to_token(
376            alice_token,
377            "urn:hessra:alice:device".to_string(),
378            public_key,
379            TokenTimeConfig::default(),
380        )
381        .expect("Failed to attenuate");
382
383        // Similar prefix attacks on attenuated token
384        assert!(
385            verify_identity_token(
386                attenuated.clone(),
387                public_key,
388                "urn:hessra:alice:device2".to_string()
389            )
390            .is_err(),
391            "device2 should not verify against device"
392        );
393        assert!(
394            verify_identity_token(
395                attenuated,
396                public_key,
397                "urn:hessra:alice2:device".to_string()
398            )
399            .is_err(),
400            "alice2:device should not verify"
401        );
402    }
403
404    #[test]
405    fn test_empty_identity_handling() {
406        let keypair = KeyPair::new();
407        let public_key = keypair.public();
408
409        // Test 1: Non-delegatable empty identity
410        let result =
411            HessraIdentity::new("".to_string(), TokenTimeConfig::default()).issue(&keypair);
412
413        // This should succeed in creation (empty string is valid)
414        assert!(
415            result.is_ok(),
416            "Should be able to create non-delegatable token with empty identity"
417        );
418
419        let token = result.unwrap();
420
421        // Verification with empty identity should work
422        assert!(
423            verify_identity_token(token.clone(), public_key, "".to_string()).is_ok(),
424            "Empty identity should verify against empty identity token"
425        );
426
427        // Non-empty identity should fail (non-delegatable requires exact match)
428        assert!(
429            verify_identity_token(token.clone(), public_key, "urn:hessra:anyone".to_string())
430                .is_err(),
431            "Non-empty identity should not verify against non-delegatable empty identity token"
432        );
433
434        assert!(
435            verify_identity_token(token, public_key, ":something".to_string()).is_err(),
436            "Identity starting with : should not match non-delegatable empty identity"
437        );
438
439        // Test 2: Delegatable empty identity
440        let delegatable_token = HessraIdentity::new("".to_string(), TokenTimeConfig::default())
441            .delegatable(true)
442            .issue(&keypair)
443            .expect("Failed to create delegatable empty identity token");
444
445        // Empty identity should work
446        assert!(
447            verify_identity_token(delegatable_token.clone(), public_key, "".to_string()).is_ok(),
448            "Empty identity should verify against delegatable empty identity token"
449        );
450
451        // Something starting with ":" would pass due to starts_with check
452        assert!(
453            verify_identity_token(delegatable_token, public_key, ":something".to_string()).is_ok(),
454            "Identity starting with : would match delegatable empty identity's hierarchy check"
455        );
456    }
457
458    #[test]
459    fn test_domain_restricted_identity_verification() {
460        use verify::IdentityVerifier;
461
462        let keypair = KeyPair::new();
463        let public_key = keypair.public();
464        let subject = "urn:hessra:alice".to_string();
465        let domain = "example.com".to_string();
466
467        // Test 1: Non-delegatable domain-restricted token
468        let domain_token = HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
469            .domain_restricted(domain.clone())
470            .issue(&keypair)
471            .expect("Failed to create domain-restricted token");
472
473        // Should pass with matching domain using builder
474        assert!(
475            IdentityVerifier::new(domain_token.clone(), public_key)
476                .with_identity(subject.clone())
477                .with_domain(domain.clone())
478                .verify()
479                .is_ok(),
480            "Verification should succeed with matching domain"
481        );
482
483        // Should pass with matching domain using builder and ensuring subject in domain
484        assert!(
485            IdentityVerifier::new(domain_token.clone(), public_key)
486                .with_identity(subject.clone())
487                .with_domain(domain.clone())
488                .ensure_subject_in_domain()
489                .verify()
490                .is_ok(),
491            "Verification should succeed with matching domain"
492        );
493
494        // Should fail without domain context
495        assert!(
496            verify_identity_token(domain_token.clone(), public_key, subject.clone()).is_err(),
497            "Verification should fail without domain context"
498        );
499
500        // Should fail with wrong domain
501        assert!(
502            IdentityVerifier::new(domain_token.clone(), public_key)
503                .with_identity(subject.clone())
504                .with_domain("wrong.com".to_string())
505                .verify()
506                .is_err(),
507            "Verification should fail with wrong domain"
508        );
509
510        // Bearer verification should fail (needs domain fact)
511        assert!(
512            verify_bearer_token(domain_token.clone(), public_key).is_err(),
513            "Bearer verification should fail without domain context"
514        );
515
516        // Bearer with domain should pass
517        assert!(
518            IdentityVerifier::new(domain_token.clone(), public_key)
519                .with_domain(domain.clone())
520                .verify()
521                .is_ok(),
522            "Bearer verification should pass with domain context"
523        );
524
525        // Test 2: Delegatable domain-restricted token
526        let delegatable_domain_token =
527            HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
528                .delegatable(true)
529                .domain_restricted(domain.clone())
530                .issue(&keypair)
531                .expect("Failed to create delegatable domain-restricted token");
532
533        // Should pass with exact identity and domain
534        assert!(
535            IdentityVerifier::new(delegatable_domain_token.clone(), public_key)
536                .with_identity(subject.clone())
537                .with_domain(domain.clone())
538                .ensure_subject_in_domain()
539                .verify()
540                .is_ok(),
541            "Delegatable token should verify with exact identity and domain"
542        );
543
544        // Should pass with hierarchical identity and domain
545        assert!(
546            IdentityVerifier::new(delegatable_domain_token.clone(), public_key)
547                .with_identity("urn:hessra:alice:laptop".to_string())
548                .with_domain(domain.clone())
549                .ensure_subject_in_domain()
550                .verify()
551                .is_ok(),
552            "Delegatable token should verify with hierarchical identity and domain"
553        );
554
555        // Should fail with hierarchical identity but no domain
556        assert!(
557            verify_identity_token(
558                delegatable_domain_token.clone(),
559                public_key,
560                "urn:hessra:alice:laptop".to_string()
561            )
562            .is_err(),
563            "Delegatable token should fail without domain context"
564        );
565
566        // Test 3: Non-domain-restricted token with domain context
567        // (extra context shouldn't break verification)
568        let regular_token = HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
569            .issue(&keypair)
570            .expect("Failed to create regular token");
571
572        // Regular token should pass with or without domain context
573        assert!(
574            verify_identity_token(regular_token.clone(), public_key, subject.clone()).is_ok(),
575            "Regular token should verify without domain context"
576        );
577
578        assert!(
579            IdentityVerifier::new(regular_token.clone(), public_key)
580                .with_identity(subject.clone())
581                .with_domain(domain.clone())
582                .verify()
583                .is_ok(),
584            "Regular token should verify even with extra domain context"
585        );
586
587        // Test 4: Ensure subject in domain
588        // This should fail because the subject is not associated with the domain
589        assert!(
590            IdentityVerifier::new(regular_token.clone(), public_key)
591                .with_identity(subject.clone())
592                .with_domain(domain.clone())
593                .ensure_subject_in_domain()
594                .verify()
595                .is_err(),
596            "Regular token should fail to verify with ensure subject in domain"
597        );
598    }
599}