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}