Skip to main content

hessra_identity_token/
revocation.rs

1//! Revocation utilities for identity tokens
2
3use crate::inspect::inspect_identity_token;
4use hessra_token_core::{Biscuit, PublicKey, RevocationId, TokenError, get_revocation_ids};
5use std::fmt;
6
7/// Represents an identity and its associated revocation ID
8#[derive(Debug, Clone)]
9pub struct IdentityRevocation {
10    /// The identity (subject or delegated actor)
11    pub identity: String,
12    /// The revocation ID for this identity's block
13    pub revocation_id: RevocationId,
14    /// Whether this is a delegated identity (false for the base identity)
15    pub is_delegated: bool,
16    /// The block index (0 for authority block, 1+ for delegation blocks)
17    pub block_index: usize,
18}
19
20impl fmt::Display for IdentityRevocation {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(
23            f,
24            "identity: {}, revocation_id: {}, delegated: {}, block: {}",
25            self.identity,
26            self.revocation_id.to_hex(),
27            self.is_delegated,
28            self.block_index
29        )
30    }
31}
32
33/// Get all identity revocations from an identity token
34pub fn get_identity_revocations(
35    token: String,
36    public_key: PublicKey,
37) -> Result<Vec<IdentityRevocation>, TokenError> {
38    let biscuit = Biscuit::from_base64(&token, public_key)?;
39    let rev_ids = get_revocation_ids(&biscuit);
40    let inspect_result = inspect_identity_token(token.clone(), public_key)?;
41
42    let mut revocations = Vec::new();
43    let base_identity = extract_base_identity(&biscuit)?;
44
45    if let Some(base_rev_id) = rev_ids.first() {
46        revocations.push(IdentityRevocation {
47            identity: base_identity.clone(),
48            revocation_id: base_rev_id.clone(),
49            is_delegated: false,
50            block_index: 0,
51        });
52    }
53
54    if inspect_result.is_delegated {
55        let delegated_identities = extract_all_delegated_identities(&biscuit);
56
57        for (idx, delegated_identity) in delegated_identities.iter().enumerate() {
58            let block_index = idx + 1;
59            if let Some(rev_id) = rev_ids.get(block_index) {
60                revocations.push(IdentityRevocation {
61                    identity: delegated_identity.clone(),
62                    revocation_id: rev_id.clone(),
63                    is_delegated: true,
64                    block_index,
65                });
66            }
67        }
68    }
69
70    Ok(revocations)
71}
72
73/// Get the revocation ID for the current active identity in a token
74pub fn get_active_identity_revocation(
75    token: String,
76    public_key: PublicKey,
77) -> Result<IdentityRevocation, TokenError> {
78    let revocations = get_identity_revocations(token, public_key)?;
79
80    revocations
81        .into_iter()
82        .last()
83        .ok_or_else(|| TokenError::internal("No identities found in token".to_string()))
84}
85
86fn extract_base_identity(biscuit: &Biscuit) -> Result<String, TokenError> {
87    let content = biscuit.print();
88
89    for line in content.lines() {
90        if line.trim().starts_with("subject(") {
91            if let Some(start) = line.find('"') {
92                if let Some(end) = line[start + 1..].find('"') {
93                    return Ok(line[start + 1..start + 1 + end].to_string());
94                }
95            }
96        }
97    }
98
99    Err(TokenError::internal(
100        "No subject found in authority block".to_string(),
101    ))
102}
103
104fn extract_all_delegated_identities(biscuit: &Biscuit) -> Vec<String> {
105    let mut identities = Vec::new();
106    let content = biscuit.print();
107
108    if let Some(blocks_start) = content.find("blocks: [") {
109        let blocks_section = &content[blocks_start..];
110
111        if let Some(blocks_end) = blocks_section.find("\n}") {
112            let blocks_content = &blocks_section[..blocks_end];
113
114            let blocks: Vec<&str> = blocks_content.split("Block {").skip(1).collect();
115
116            for block in blocks.iter() {
117                if let Some(symbols_line_start) = block.find("symbols: [") {
118                    let after_symbols = &block[symbols_line_start + 10..];
119                    if let Some(symbols_end) = after_symbols.find(']') {
120                        let symbols_str = &after_symbols[..symbols_end];
121
122                        for symbol in symbols_str.split(',') {
123                            let symbol = symbol.trim().trim_matches('"');
124                            if symbol.contains(':')
125                                && (symbol.starts_with("urn:")
126                                    || symbol.starts_with("https:")
127                                    || symbol.starts_with("mailto:")
128                                    || symbol.contains("hessra"))
129                            {
130                                identities.push(symbol.to_string());
131                                break;
132                            }
133                        }
134                    }
135                }
136            }
137        }
138    }
139
140    identities
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::{HessraIdentity, add_identity_attenuation_to_token};
147    use hessra_token_core::{KeyPair, TokenTimeConfig};
148
149    #[test]
150    fn test_get_identity_revocations_base_token() {
151        let keypair = KeyPair::new();
152        let public_key = keypair.public();
153        let subject = "urn:hessra:alice".to_string();
154
155        let token = HessraIdentity::new(subject.clone(), TokenTimeConfig::default())
156            .delegatable(true)
157            .issue(&keypair)
158            .expect("Failed to create token");
159
160        let revocations =
161            get_identity_revocations(token, public_key).expect("Failed to get revocations");
162
163        assert_eq!(revocations.len(), 1);
164        assert_eq!(revocations[0].identity, subject);
165        assert!(!revocations[0].is_delegated);
166        assert_eq!(revocations[0].block_index, 0);
167        assert!(!revocations[0].revocation_id.to_hex().is_empty());
168    }
169
170    #[test]
171    fn test_get_identity_revocations_delegated_token() {
172        let keypair = KeyPair::new();
173        let public_key = keypair.public();
174        let base_identity = "urn:hessra:alice".to_string();
175        let delegated_identity = "urn:hessra:alice:laptop".to_string();
176
177        let token = HessraIdentity::new(base_identity.clone(), TokenTimeConfig::default())
178            .delegatable(true)
179            .issue(&keypair)
180            .expect("Failed to create token");
181
182        let delegated_token = add_identity_attenuation_to_token(
183            token,
184            delegated_identity.clone(),
185            public_key,
186            TokenTimeConfig::default(),
187        )
188        .expect("Failed to delegate token");
189
190        let revocations = get_identity_revocations(delegated_token, public_key)
191            .expect("Failed to get revocations");
192
193        assert_eq!(revocations.len(), 2);
194
195        assert_eq!(revocations[0].identity, base_identity);
196        assert!(!revocations[0].is_delegated);
197        assert_eq!(revocations[0].block_index, 0);
198
199        assert_eq!(revocations[1].identity, delegated_identity);
200        assert!(revocations[1].is_delegated);
201        assert_eq!(revocations[1].block_index, 1);
202
203        assert_ne!(
204            revocations[0].revocation_id.to_hex(),
205            revocations[1].revocation_id.to_hex()
206        );
207    }
208
209    #[test]
210    fn test_get_active_identity_revocation() {
211        let keypair = KeyPair::new();
212        let public_key = keypair.public();
213        let base_identity = "urn:hessra:alice".to_string();
214        let delegated_identity = "urn:hessra:alice:laptop".to_string();
215
216        let token = HessraIdentity::new(base_identity.clone(), TokenTimeConfig::default())
217            .delegatable(true)
218            .issue(&keypair)
219            .expect("Failed to create token");
220
221        let active_rev = get_active_identity_revocation(token.clone(), public_key)
222            .expect("Failed to get active revocation");
223        assert_eq!(active_rev.identity, base_identity);
224        assert!(!active_rev.is_delegated);
225
226        let delegated_token = add_identity_attenuation_to_token(
227            token,
228            delegated_identity.clone(),
229            public_key,
230            TokenTimeConfig::default(),
231        )
232        .expect("Failed to delegate token");
233
234        let active_rev = get_active_identity_revocation(delegated_token, public_key)
235            .expect("Failed to get active revocation");
236        assert_eq!(active_rev.identity, delegated_identity);
237        assert!(active_rev.is_delegated);
238    }
239
240    #[test]
241    fn test_multi_level_delegation_revocations() {
242        let keypair = KeyPair::new();
243        let public_key = keypair.public();
244
245        let org_identity = "urn:hessra:company".to_string();
246        let dept_identity = "urn:hessra:company:dept_eng".to_string();
247        let user_identity = "urn:hessra:company:dept_eng:alice".to_string();
248
249        let token = HessraIdentity::new(org_identity.clone(), TokenTimeConfig::default())
250            .delegatable(true)
251            .issue(&keypair)
252            .expect("Failed to create org token");
253
254        let token = add_identity_attenuation_to_token(
255            token,
256            dept_identity.clone(),
257            public_key,
258            TokenTimeConfig::default(),
259        )
260        .expect("Failed to attenuate to department");
261
262        let token = add_identity_attenuation_to_token(
263            token,
264            user_identity.clone(),
265            public_key,
266            TokenTimeConfig::default(),
267        )
268        .expect("Failed to attenuate to user");
269
270        let revocations =
271            get_identity_revocations(token, public_key).expect("Failed to get revocations");
272
273        assert_eq!(revocations.len(), 3);
274
275        assert_eq!(revocations[0].identity, org_identity);
276        assert_eq!(revocations[0].block_index, 0);
277        assert!(!revocations[0].is_delegated);
278
279        assert_eq!(revocations[1].identity, dept_identity);
280        assert_eq!(revocations[1].block_index, 1);
281        assert!(revocations[1].is_delegated);
282
283        assert_eq!(revocations[2].identity, user_identity);
284        assert_eq!(revocations[2].block_index, 2);
285        assert!(revocations[2].is_delegated);
286
287        let rev_ids: Vec<String> = revocations
288            .iter()
289            .map(|r| r.revocation_id.to_hex())
290            .collect();
291        assert_eq!(rev_ids.len(), 3);
292        assert_ne!(rev_ids[0], rev_ids[1]);
293        assert_ne!(rev_ids[1], rev_ids[2]);
294        assert_ne!(rev_ids[0], rev_ids[2]);
295    }
296}