hessra_context_token/
verify.rs1extern crate biscuit_auth as biscuit;
2
3use biscuit::Biscuit;
4use biscuit::macros::authorizer;
5use chrono::Utc;
6use hessra_token_core::{PublicKey, TokenError};
7
8pub struct ContextVerifier {
29 token: String,
30 public_key: PublicKey,
31}
32
33impl ContextVerifier {
34 pub fn new(token: String, public_key: PublicKey) -> Self {
40 Self { token, public_key }
41 }
42
43 pub fn verify(self) -> Result<(), TokenError> {
53 let biscuit = Biscuit::from_base64(&self.token, self.public_key)?;
54 let now = Utc::now().timestamp();
55
56 let authz = authorizer!(
57 r#"
58 time({now});
59 allow if true;
60 "#
61 );
62
63 authz
64 .build(&biscuit)
65 .map_err(|e| TokenError::internal(format!("failed to build authorizer: {e}")))?
66 .authorize()
67 .map_err(TokenError::from)?;
68
69 Ok(())
70 }
71
72 pub fn check_precluded_exposures(self, precluded: &[String]) -> Result<(), TokenError> {
99 let biscuit = Biscuit::from_base64(&self.token, self.public_key)?;
100 let now = Utc::now().timestamp();
101
102 let authz = authorizer!(
104 r#"
105 time({now});
106 allow if true;
107 "#
108 );
109
110 authz
111 .build(&biscuit)
112 .map_err(|e| TokenError::internal(format!("failed to build authorizer: {e}")))?
113 .authorize()
114 .map_err(TokenError::from)?;
115
116 if precluded.is_empty() {
117 return Ok(());
118 }
119
120 let block_count = biscuit.block_count();
122 for i in 0..block_count {
123 let block_source = biscuit.print_block_source(i).unwrap_or_default();
124 for line in block_source.lines() {
125 let trimmed = line.trim();
126 if let Some(rest) = trimmed.strip_prefix("exposure(") {
127 if let Some(label_str) = rest.strip_suffix(");") {
128 let label = label_str.trim_matches('"');
129 if precluded.iter().any(|p| p == label) {
130 return Err(TokenError::internal(format!(
131 "precluded exposure label present: {label}"
132 )));
133 }
134 }
135 }
136 }
137 }
138
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::mint::HessraContext;
147 use hessra_token_core::{KeyPair, TokenTimeConfig};
148
149 #[test]
150 fn test_verify_valid_token() {
151 let keypair = KeyPair::new();
152 let public_key = keypair.public();
153
154 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
155 .issue(&keypair)
156 .expect("Failed to create context token");
157
158 ContextVerifier::new(token, public_key)
159 .verify()
160 .expect("Should verify valid token");
161 }
162
163 #[test]
164 fn test_verify_expired_token() {
165 let keypair = KeyPair::new();
166 let public_key = keypair.public();
167
168 let expired_config = TokenTimeConfig {
169 start_time: Some(0),
170 duration: 1,
171 };
172
173 let token = HessraContext::new("agent:test".to_string(), expired_config)
174 .issue(&keypair)
175 .expect("Failed to create expired context token");
176
177 let result = ContextVerifier::new(token, public_key).verify();
178 assert!(result.is_err(), "Expired token should fail verification");
179 }
180
181 #[test]
182 fn test_verify_wrong_key() {
183 let keypair = KeyPair::new();
184 let wrong_keypair = KeyPair::new();
185 let wrong_public_key = wrong_keypair.public();
186
187 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
188 .issue(&keypair)
189 .expect("Failed to create context token");
190
191 let result = ContextVerifier::new(token, wrong_public_key).verify();
192 assert!(result.is_err(), "Token verified with wrong key should fail");
193 }
194
195 #[test]
196 fn test_verify_exposed_token() {
197 let keypair = KeyPair::new();
198 let public_key = keypair.public();
199
200 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
201 .issue(&keypair)
202 .expect("Failed to create context token");
203
204 let exposed = crate::exposure::add_exposure(
206 &token,
207 public_key,
208 &["PII:SSN".to_string()],
209 "data:user-ssn".to_string(),
210 )
211 .expect("Failed to add exposure");
212
213 ContextVerifier::new(exposed, public_key)
214 .verify()
215 .expect("Exposed token should still verify");
216 }
217
218 #[test]
219 fn test_check_precluded_exposures_blocks_matching() {
220 let keypair = KeyPair::new();
221 let public_key = keypair.public();
222
223 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
224 .issue(&keypair)
225 .expect("Failed to create context token");
226
227 let exposed = crate::exposure::add_exposure(
228 &token,
229 public_key,
230 &["PII:SSN".to_string()],
231 "data:user-ssn".to_string(),
232 )
233 .expect("Failed to add exposure");
234
235 let result = ContextVerifier::new(exposed, public_key)
236 .check_precluded_exposures(&["PII:SSN".to_string()]);
237
238 assert!(
239 result.is_err(),
240 "Should deny when precluded label is present"
241 );
242 }
243
244 #[test]
245 fn test_check_precluded_exposures_allows_non_matching() {
246 let keypair = KeyPair::new();
247 let public_key = keypair.public();
248
249 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
250 .issue(&keypair)
251 .expect("Failed to create context token");
252
253 let exposed = crate::exposure::add_exposure(
254 &token,
255 public_key,
256 &["PII:email".to_string()],
257 "data:user-profile".to_string(),
258 )
259 .expect("Failed to add exposure");
260
261 ContextVerifier::new(exposed, public_key)
262 .check_precluded_exposures(&["PII:SSN".to_string()])
263 .expect("Should allow when precluded label is not present");
264 }
265
266 #[test]
267 fn test_check_precluded_exposures_empty_list() {
268 let keypair = KeyPair::new();
269 let public_key = keypair.public();
270
271 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
272 .issue(&keypair)
273 .expect("Failed to create context token");
274
275 let exposed = crate::exposure::add_exposure(
276 &token,
277 public_key,
278 &["PII:SSN".to_string()],
279 "data:user-ssn".to_string(),
280 )
281 .expect("Failed to add exposure");
282
283 ContextVerifier::new(exposed, public_key)
284 .check_precluded_exposures(&[])
285 .expect("Empty precluded list should pass");
286 }
287
288 #[test]
289 fn test_check_precluded_exposures_expired_token_fails() {
290 let keypair = KeyPair::new();
291 let public_key = keypair.public();
292
293 let expired_config = TokenTimeConfig {
294 start_time: Some(0),
295 duration: 1,
296 };
297
298 let token = HessraContext::new("agent:test".to_string(), expired_config)
299 .issue(&keypair)
300 .expect("Failed to create expired context token");
301
302 let result = ContextVerifier::new(token, public_key)
303 .check_precluded_exposures(&["PII:SSN".to_string()]);
304
305 assert!(
306 result.is_err(),
307 "Expired token should fail even with non-matching precluded labels"
308 );
309 }
310
311 #[test]
312 fn test_check_precluded_exposures_clean_token_passes() {
313 let keypair = KeyPair::new();
314 let public_key = keypair.public();
315
316 let token = HessraContext::new("agent:test".to_string(), TokenTimeConfig::default())
317 .issue(&keypair)
318 .expect("Failed to create context token");
319
320 ContextVerifier::new(token, public_key)
321 .check_precluded_exposures(&["PII:SSN".to_string()])
322 .expect("Clean token should pass any precluded check");
323 }
324}