hessra_token_core/
revocation.rs

1//! Revocation identifier utilities for Biscuit tokens
2//!
3//! This module provides utilities for extracting and managing revocation identifiers
4//! from Biscuit tokens. Each block in a token has a unique revocation ID that can be
5//! used to revoke specific tokens or delegation levels.
6
7use crate::Biscuit;
8use std::fmt;
9
10/// A revocation identifier for a Biscuit token block
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RevocationId {
13    inner: Vec<u8>,
14}
15
16impl RevocationId {
17    /// Create a new RevocationId from raw bytes
18    pub fn new(bytes: Vec<u8>) -> Self {
19        Self { inner: bytes }
20    }
21
22    /// Get the raw bytes of the revocation ID
23    pub fn as_bytes(&self) -> &[u8] {
24        &self.inner
25    }
26
27    /// Convert the revocation ID to a hex string for display/storage
28    pub fn to_hex(&self) -> String {
29        hex::encode(&self.inner)
30    }
31
32    /// Create a RevocationId from a hex string
33    pub fn from_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
34        hex::decode(hex_str).map(Self::new)
35    }
36}
37
38impl fmt::Display for RevocationId {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "{}", self.to_hex())
41    }
42}
43
44impl From<Vec<u8>> for RevocationId {
45    fn from(bytes: Vec<u8>) -> Self {
46        Self::new(bytes)
47    }
48}
49
50/// Extract all revocation IDs from a Biscuit token
51///
52/// Returns a vector of revocation IDs, one for each block in the token.
53/// The first ID is for the authority block, followed by any attestation blocks.
54pub fn get_revocation_ids(biscuit: &Biscuit) -> Vec<RevocationId> {
55    biscuit
56        .revocation_identifiers()
57        .into_iter()
58        .map(RevocationId::from)
59        .collect()
60}
61
62/// Get the revocation ID for the authority (first) block
63///
64/// This is useful for authorization tokens which are short-lived and typically
65/// only need the authority block's revocation ID.
66pub fn get_authority_revocation_id(biscuit: &Biscuit) -> Option<RevocationId> {
67    biscuit
68        .revocation_identifiers()
69        .into_iter()
70        .next()
71        .map(RevocationId::from)
72}
73
74/// Get the revocation ID for a specific block by index
75///
76/// - Index 0 is the authority block
77/// - Index 1+ are attestation/delegation blocks
78pub fn get_block_revocation_id(biscuit: &Biscuit, index: usize) -> Option<RevocationId> {
79    biscuit
80        .revocation_identifiers()
81        .into_iter()
82        .nth(index)
83        .map(RevocationId::from)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::KeyPair;
90    use biscuit_auth::macros::biscuit;
91
92    #[test]
93    fn test_revocation_id_hex_conversion() {
94        let bytes = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
95        let rev_id = RevocationId::new(bytes.clone());
96
97        // Test to_hex
98        assert_eq!(rev_id.to_hex(), "0123456789abcdef");
99        assert_eq!(rev_id.to_string(), "0123456789abcdef");
100
101        // Test from_hex
102        let from_hex = RevocationId::from_hex("0123456789abcdef").unwrap();
103        assert_eq!(from_hex.as_bytes(), &bytes[..]);
104        assert_eq!(rev_id, from_hex);
105    }
106
107    #[test]
108    fn test_get_revocation_ids() {
109        let keypair = KeyPair::new();
110
111        // Create a simple biscuit
112        let biscuit = biscuit!(
113            r#"
114                right("alice", "resource1", "read");
115            "#
116        )
117        .build(&keypair)
118        .unwrap();
119
120        let rev_ids = get_revocation_ids(&biscuit);
121
122        // Should have at least one revocation ID (for the authority block)
123        assert!(!rev_ids.is_empty());
124        assert_eq!(rev_ids.len(), 1);
125
126        // The revocation ID should be non-empty
127        assert!(!rev_ids[0].as_bytes().is_empty());
128    }
129
130    #[test]
131    fn test_get_authority_revocation_id() {
132        let keypair = KeyPair::new();
133
134        let biscuit = biscuit!(
135            r#"
136                right("alice", "resource1", "read");
137            "#
138        )
139        .build(&keypair)
140        .unwrap();
141
142        let auth_id = get_authority_revocation_id(&biscuit);
143        assert!(auth_id.is_some());
144
145        // Should match the first ID from get_revocation_ids
146        let all_ids = get_revocation_ids(&biscuit);
147        assert_eq!(auth_id.unwrap(), all_ids[0]);
148    }
149
150    #[test]
151    fn test_unique_revocation_ids() {
152        let keypair = KeyPair::new();
153
154        // Create two identical biscuits
155        let biscuit1 = biscuit!(
156            r#"
157                right("alice", "resource1", "read");
158            "#
159        )
160        .build(&keypair)
161        .unwrap();
162
163        let biscuit2 = biscuit!(
164            r#"
165                right("alice", "resource1", "read");
166            "#
167        )
168        .build(&keypair)
169        .unwrap();
170
171        let rev_id1 = get_authority_revocation_id(&biscuit1).unwrap();
172        let rev_id2 = get_authority_revocation_id(&biscuit2).unwrap();
173
174        // Even with identical content, revocation IDs should be different
175        // (due to different signatures)
176        assert_ne!(rev_id1, rev_id2);
177    }
178}