Skip to main content

hessra_cap_token/
verify.rs

1extern crate biscuit_auth as biscuit;
2
3use biscuit::Algorithm;
4use biscuit::macros::{authorizer, check, fact};
5use chrono::Utc;
6use hessra_token_core::{
7    Biscuit, PublicKey, TokenError, parse_capability_failure, parse_check_failure,
8};
9
10/// Builder for verifying Hessra capability tokens with flexible configuration.
11///
12/// By default, capability verification only checks resource + operation.
13/// Subject verification is optional via `.with_subject()`.
14///
15/// # Example
16/// ```no_run
17/// use hessra_cap_token::{CapabilityVerifier, HessraCapability};
18/// use hessra_token_core::{KeyPair, TokenTimeConfig};
19///
20/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
21/// let keypair = KeyPair::new();
22/// let public_key = keypair.public();
23/// let token = HessraCapability::new(
24///     "user123".to_string(),
25///     "resource456".to_string(),
26///     "read".to_string(),
27///     TokenTimeConfig::default(),
28/// )
29/// .issue(&keypair)?;
30///
31/// // Basic capability verification (no subject check)
32/// CapabilityVerifier::new(
33///     token.clone(),
34///     public_key,
35///     "resource456".to_string(),
36///     "read".to_string(),
37/// )
38/// .verify()?;
39///
40/// // With optional subject verification
41/// CapabilityVerifier::new(
42///     token.clone(),
43///     public_key,
44///     "resource456".to_string(),
45///     "read".to_string(),
46/// )
47/// .with_subject("user123".to_string())
48/// .verify()?;
49/// # Ok(())
50/// # }
51/// ```
52pub struct CapabilityVerifier {
53    token: String,
54    public_key: PublicKey,
55    resource: String,
56    operation: String,
57    subject: Option<String>,
58    namespace: Option<String>,
59    designations: Vec<(String, String)>,
60}
61
62impl CapabilityVerifier {
63    /// Creates a new capability verifier for a base64-encoded token.
64    ///
65    /// # Arguments
66    /// * `token` - The base64-encoded capability token to verify
67    /// * `public_key` - The public key used to verify the token signature
68    /// * `resource` - The resource identifier to verify
69    /// * `operation` - The operation to verify
70    pub fn new(token: String, public_key: PublicKey, resource: String, operation: String) -> Self {
71        Self {
72            token,
73            public_key,
74            resource,
75            operation,
76            subject: None,
77            namespace: None,
78            designations: Vec::new(),
79        }
80    }
81
82    /// Adds an optional subject verification check.
83    ///
84    /// When set, the authorizer adds a check that the minted subject matches.
85    /// This is optional -- pure capability verification does not require it.
86    ///
87    /// # Arguments
88    /// * `subject` - The subject to verify in the token's right fact
89    pub fn with_subject(mut self, subject: String) -> Self {
90        self.subject = Some(subject);
91        self
92    }
93
94    /// Adds a namespace restriction to the verification.
95    ///
96    /// # Arguments
97    /// * `namespace` - The namespace to verify against (e.g., "example.com")
98    pub fn with_namespace(mut self, namespace: String) -> Self {
99        self.namespace = Some(namespace);
100        self
101    }
102
103    /// Adds a designation fact to the verification.
104    ///
105    /// Each designation provides a `designation(label, value)` fact that the
106    /// token's designation checks will verify against.
107    ///
108    /// # Arguments
109    /// * `label` - The designation dimension (e.g., "tenant_id")
110    /// * `value` - The specific value (e.g., "t-123")
111    pub fn with_designation(mut self, label: String, value: String) -> Self {
112        self.designations.push((label, value));
113        self
114    }
115
116    /// Performs the token verification with the configured parameters.
117    ///
118    /// # Returns
119    /// * `Ok(())` - If the token is valid and meets all verification requirements
120    /// * `Err(TokenError)` - If verification fails for any reason
121    pub fn verify(self) -> Result<(), TokenError> {
122        let biscuit = Biscuit::from_base64(&self.token, self.public_key)?;
123        let now = Utc::now().timestamp();
124        let resource = self.resource.clone();
125        let operation = self.operation.clone();
126
127        // Build the capability authorizer -- no subject fact needed
128        let mut authz = authorizer!(
129            r#"
130                time({now});
131                resource({resource});
132                operation({operation});
133                allow if true;
134            "#
135        );
136
137        // Optional: add subject check when caller wants to verify who minted the token
138        if let Some(ref subject) = self.subject {
139            let subject = subject.clone();
140            let resource = self.resource.clone();
141            let operation = self.operation.clone();
142            authz = authz.check(check!(
143                r#"check if right({subject}, {resource}, {operation});"#
144            ))?;
145        }
146
147        // Add namespace fact if specified
148        if let Some(namespace) = self.namespace.clone() {
149            authz = authz.fact(fact!(r#"namespace({namespace});"#))?;
150        }
151
152        // Add designation facts
153        for (label, value) in &self.designations {
154            let label = label.clone();
155            let value = value.clone();
156            authz = authz.fact(fact!(r#"designation({label}, {value});"#))?;
157        }
158
159        match authz.build(&biscuit)?.authorize() {
160            Ok(_) => Ok(()),
161            Err(e) => Err(convert_capability_error(
162                e,
163                self.subject.as_deref(),
164                Some(&self.resource),
165                Some(&self.operation),
166                &self.namespace,
167            )),
168        }
169    }
170}
171
172/// Takes a public key encoded as a string in the format "ed25519/..." or "secp256r1/..."
173/// and returns a PublicKey.
174pub fn biscuit_key_from_string(key: String) -> Result<PublicKey, TokenError> {
175    let parts = key.split('/').collect::<Vec<&str>>();
176    if parts.len() != 2 {
177        return Err(TokenError::invalid_key_format(
178            "Key must be in format 'algorithm/hexkey'",
179        ));
180    }
181
182    let alg = match parts[0] {
183        "ed25519" => Algorithm::Ed25519,
184        "secp256r1" => Algorithm::Secp256r1,
185        _ => {
186            return Err(TokenError::invalid_key_format(
187                "Unsupported algorithm, must be ed25519 or secp256r1",
188            ));
189        }
190    };
191
192    let key_bytes = hex::decode(parts[1])?;
193
194    let key = PublicKey::from_bytes(&key_bytes, alg)
195        .map_err(|e| TokenError::invalid_key_format(e.to_string()))?;
196
197    Ok(key)
198}
199
200/// Convert biscuit authorization errors to detailed capability errors
201fn convert_capability_error(
202    err: biscuit::error::Token,
203    subject: Option<&str>,
204    resource: Option<&str>,
205    operation: Option<&str>,
206    namespace: &Option<String>,
207) -> TokenError {
208    use biscuit::error::{Logic, Token};
209
210    match err {
211        Token::FailedLogic(logic_err) => match &logic_err {
212            Logic::Unauthorized { checks, .. } | Logic::NoMatchingPolicy { checks } => {
213                for failed_check in checks.iter() {
214                    let (block_id, check_id, rule) = match failed_check {
215                        biscuit::error::FailedCheck::Block(block_check) => (
216                            block_check.block_id,
217                            block_check.check_id,
218                            block_check.rule.clone(),
219                        ),
220                        biscuit::error::FailedCheck::Authorizer(auth_check) => {
221                            (0, auth_check.check_id, auth_check.rule.clone())
222                        }
223                    };
224
225                    let parsed_error = parse_check_failure(block_id, check_id, &rule);
226
227                    match parsed_error {
228                        TokenError::NamespaceMismatch {
229                            expected,
230                            block_id,
231                            check_id,
232                            ..
233                        } => {
234                            return TokenError::NamespaceMismatch {
235                                expected,
236                                provided: namespace.clone(),
237                                block_id,
238                                check_id,
239                            };
240                        }
241                        TokenError::Expired { .. } => return parsed_error,
242                        _ => {}
243                    }
244                }
245
246                // Check if this looks like a rights denial (no matching policy)
247                if matches!(logic_err, Logic::NoMatchingPolicy { .. }) {
248                    return parse_capability_failure(
249                        subject,
250                        resource,
251                        operation,
252                        &format!("{checks:?}"),
253                    );
254                }
255
256                TokenError::from(Token::FailedLogic(logic_err))
257            }
258            other => TokenError::from(Token::FailedLogic(other.clone())),
259        },
260        other => TokenError::from(other),
261    }
262}