1use std::sync::Arc;
28
29use biscuit_auth::builder::AuthorizerBuilder;
30use biscuit_auth::{Biscuit, BiscuitBuilder, BlockBuilder, KeyPair, PublicKey};
31use parking_lot::RwLock;
32
33use crate::error::{Error, Result};
34
35struct Inner {
39 active: ActiveRootKey,
40 history: Vec<HistoryRootKey>,
41}
42
43pub struct ActiveRootKey {
45 pub kid: String,
46 pub keypair: KeyPair,
47}
48
49pub struct HistoryRootKey {
54 pub kid: String,
55 pub public_key: PublicKey,
56}
57
58#[derive(Clone)]
62pub struct BiscuitConfig {
63 inner: Arc<RwLock<Inner>>,
64}
65
66impl BiscuitConfig {
67 pub fn from_active(active: ActiveRootKey, history: Vec<HistoryRootKey>) -> Self {
70 Self {
71 inner: Arc::new(RwLock::new(Inner { active, history })),
72 }
73 }
74
75 pub fn generate_ephemeral() -> Self {
81 let keypair = KeyPair::new();
82 let kid = mint_kid(&keypair.public());
83 Self::from_active(ActiveRootKey { kid, keypair }, Vec::new())
84 }
85
86 pub fn from_pem(pem: &str) -> Result<Self> {
91 let keypair = KeyPair::from_private_key_pem(pem)
92 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit root key from pem: {e}")))?;
93 let kid = mint_kid(&keypair.public());
94 Ok(Self::from_active(
95 ActiveRootKey { kid, keypair },
96 Vec::new(),
97 ))
98 }
99
100 pub fn active_kid(&self) -> String {
103 self.inner.read().active.kid.clone()
104 }
105
106 pub fn public_pem(&self) -> Result<String> {
111 self.inner
112 .read()
113 .active
114 .keypair
115 .public()
116 .to_pem()
117 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public pem: {e}")))
118 }
119
120 pub fn active_public_key(&self) -> PublicKey {
123 self.inner.read().active.keypair.public()
124 }
125
126 pub fn issue<F>(&self, build: F) -> Result<String>
135 where
136 F: FnOnce(BiscuitBuilder) -> std::result::Result<BiscuitBuilder, biscuit_auth::error::Token>,
137 {
138 let builder = build(Biscuit::builder())
139 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit build: {e}")))?;
140 let guard = self.inner.read();
141 let token = builder
142 .build(&guard.active.keypair)
143 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit sign: {e}")))?;
144 token
145 .to_base64()
146 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64: {e}")))
147 }
148
149 pub fn verify<F>(&self, token: &str, build: F) -> Result<()>
159 where
160 F: FnOnce(AuthorizerBuilder) -> std::result::Result<AuthorizerBuilder, biscuit_auth::error::Token>,
161 {
162 let guard = self.inner.read();
163 let parsed = match Biscuit::from_base64(token, guard.active.keypair.public()) {
166 Ok(t) => t,
167 Err(active_err) => {
168 let mut last = active_err;
169 let mut found = None;
170 for hist in &guard.history {
171 match Biscuit::from_base64(token, hist.public_key) {
172 Ok(t) => {
173 found = Some(t);
174 break;
175 }
176 Err(e) => last = e,
177 }
178 }
179 match found {
180 Some(t) => t,
181 None => {
182 return Err(Error::Backend(anyhow::anyhow!(
183 "biscuit signature verify: {last}"
184 )))
185 }
186 }
187 }
188 };
189 let authorizer_builder = build(AuthorizerBuilder::new())
190 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer build: {e}")))?;
191 let mut authorizer = authorizer_builder
192 .build(&parsed)
193 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer attach: {e}")))?;
194 authorizer
195 .authorize()
196 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorize: {e}")))?;
197 Ok(())
198 }
199}
200
201pub fn attenuate<F>(token: &str, root_public: PublicKey, build: F) -> Result<String>
210where
211 F: FnOnce(BlockBuilder) -> std::result::Result<BlockBuilder, biscuit_auth::error::Token>,
212{
213 let parsed = Biscuit::from_base64(token, root_public)
214 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit parse for attenuate: {e}")))?;
215 let block = build(BlockBuilder::new())
216 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit block build: {e}")))?;
217 let attenuated = parsed
218 .append(block)
219 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit append block: {e}")))?;
220 attenuated
221 .to_base64()
222 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64 (attenuated): {e}")))
223}
224
225#[cfg(feature = "backend-postgres")]
232pub async fn load_or_init_postgres(pool: &sqlx::PgPool) -> Result<BiscuitConfig> {
233 use sqlx::Row;
234 let row = sqlx::query(
235 "SELECT kid, private_pem
236 FROM auth.biscuit_root_keys
237 WHERE rotated_at IS NULL
238 ORDER BY created_at DESC
239 LIMIT 1",
240 )
241 .fetch_optional(pool)
242 .await
243 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (pg): {e}")))?;
244
245 if let Some(row) = row {
246 let kid: String = row.get("kid");
247 let pem_bytes: Vec<u8> = row.get("private_pem");
248 let pem = std::str::from_utf8(&pem_bytes)
249 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
250 let keypair = KeyPair::from_private_key_pem(pem)
251 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
252 Ok(BiscuitConfig::from_active(
253 ActiveRootKey { kid, keypair },
254 Vec::new(),
255 ))
256 } else {
257 let keypair = KeyPair::new();
258 let kid = mint_kid(&keypair.public());
259 let private_pem = keypair
260 .to_private_key_pem()
261 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
262 .to_string();
263 let public_pem = keypair
264 .public()
265 .to_pem()
266 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
267 let now = now_secs();
268 sqlx::query(
269 "INSERT INTO auth.biscuit_root_keys
270 (kid, private_pem, public_pem, created_at, rotated_at)
271 VALUES ($1, $2, $3, $4, NULL)",
272 )
273 .bind(&kid)
274 .bind(private_pem.as_bytes())
275 .bind(&public_pem)
276 .bind(now)
277 .execute(pool)
278 .await
279 .map_err(|e| Error::Backend(anyhow::anyhow!("insert auth.biscuit_root_keys (pg): {e}")))?;
280 Ok(BiscuitConfig::from_active(
281 ActiveRootKey { kid, keypair },
282 Vec::new(),
283 ))
284 }
285}
286
287#[cfg(feature = "backend-sqlite")]
289pub async fn load_or_init_sqlite(pool: &sqlx::SqlitePool) -> Result<BiscuitConfig> {
290 use sqlx::Row;
291 let row = sqlx::query(
292 "SELECT kid, private_pem
293 FROM auth.biscuit_root_keys
294 WHERE rotated_at IS NULL
295 ORDER BY created_at DESC
296 LIMIT 1",
297 )
298 .fetch_optional(pool)
299 .await
300 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (sqlite): {e}")))?;
301
302 if let Some(row) = row {
303 let kid: String = row.get("kid");
304 let pem_bytes: Vec<u8> = row.get("private_pem");
305 let pem = std::str::from_utf8(&pem_bytes)
306 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
307 let keypair = KeyPair::from_private_key_pem(pem)
308 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
309 Ok(BiscuitConfig::from_active(
310 ActiveRootKey { kid, keypair },
311 Vec::new(),
312 ))
313 } else {
314 let keypair = KeyPair::new();
315 let kid = mint_kid(&keypair.public());
316 let private_pem = keypair
317 .to_private_key_pem()
318 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
319 .to_string();
320 let public_pem = keypair
321 .public()
322 .to_pem()
323 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
324 let now = now_secs();
325 sqlx::query(
326 "INSERT INTO auth.biscuit_root_keys
327 (kid, private_pem, public_pem, created_at, rotated_at)
328 VALUES (?, ?, ?, ?, NULL)",
329 )
330 .bind(&kid)
331 .bind(private_pem.as_bytes())
332 .bind(&public_pem)
333 .bind(now)
334 .execute(pool)
335 .await
336 .map_err(|e| Error::Backend(anyhow::anyhow!("insert auth.biscuit_root_keys (sqlite): {e}")))?;
337 Ok(BiscuitConfig::from_active(
338 ActiveRootKey { kid, keypair },
339 Vec::new(),
340 ))
341 }
342}
343
344fn mint_kid(public_key: &PublicKey) -> String {
349 let bytes = public_key.to_bytes();
350 let prefix = if bytes.len() >= 16 { &bytes[..16] } else { &bytes };
351 format!("kid_{}", data_encoding::BASE64URL_NOPAD.encode(prefix))
352}
353
354fn now_secs() -> f64 {
355 std::time::SystemTime::now()
356 .duration_since(std::time::UNIX_EPOCH)
357 .unwrap_or_default()
358 .as_secs_f64()
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn cfg() -> BiscuitConfig {
366 BiscuitConfig::generate_ephemeral()
367 }
368
369 #[test]
370 fn issue_and_verify_round_trip() {
371 let cfg = cfg();
372 let token = cfg
373 .issue(|b| {
374 b.fact("user(\"alice\")")
375 .and_then(|b| b.fact("role(\"admin\")"))
376 })
377 .expect("issue");
378 cfg.verify(&token, |a| a.policy("allow if user(\"alice\")"))
379 .expect("verify");
380 }
381
382 #[test]
383 fn verify_rejects_tampered_token() {
384 let cfg = cfg();
385 let token = cfg
386 .issue(|b| b.fact("user(\"alice\")"))
387 .expect("issue");
388 let mut bytes = token.into_bytes();
390 let half = bytes.len() / 2;
391 bytes[half] ^= 0x01;
392 let tampered = String::from_utf8_lossy(&bytes).to_string();
393 let result = cfg.verify(&tampered, |a| a.policy("allow if user(\"alice\")"));
394 assert!(matches!(result, Err(Error::Backend(_))));
395 }
396
397 #[test]
398 fn verify_with_unknown_root_key_fails() {
399 let cfg_a = cfg();
401 let cfg_b = cfg();
402 let token = cfg_a
403 .issue(|b| b.fact("user(\"alice\")"))
404 .expect("issue");
405 let result = cfg_b.verify(&token, |a| a.policy("allow if user(\"alice\")"));
406 assert!(matches!(result, Err(Error::Backend(_))));
407 }
408
409 #[test]
410 fn attenuate_produces_valid_child_token() {
411 let cfg = cfg();
412 let token = cfg
413 .issue(|b| b.fact("user(\"alice\")"))
414 .expect("issue");
415 let pubkey = cfg.active_public_key();
416 let attenuated = attenuate(&token, pubkey, |b| {
419 b.check("check if operation(\"read\")")
420 })
421 .expect("attenuate");
422 cfg.verify(&attenuated, |a| {
424 a.fact("operation(\"read\")")
425 .and_then(|a| a.policy("allow if user(\"alice\")"))
426 })
427 .expect("read should pass");
428 let result = cfg.verify(&attenuated, |a| a.policy("allow if user(\"alice\")"));
431 assert!(matches!(result, Err(Error::Backend(_))));
432 }
433
434 #[test]
435 fn time_based_check_rejects_after_expiry() {
436 let cfg = cfg();
437 let token = cfg
439 .issue(|b| b.fact("user(\"alice\")"))
440 .expect("issue");
441 let pubkey = cfg.active_public_key();
442 let attenuated = attenuate(&token, pubkey, |b| {
443 b.check("check if time($now), $now < 2000-01-01T00:00:00Z")
446 })
447 .expect("attenuate");
448 let result = cfg.verify(&attenuated, |a| {
449 a.time().policy("allow if user(\"alice\")")
450 });
451 assert!(matches!(result, Err(Error::Backend(_))));
452 }
453
454 #[test]
455 fn from_pem_round_trips() {
456 let cfg = cfg();
457 let pem = cfg
458 .inner
459 .read()
460 .active
461 .keypair
462 .to_private_key_pem()
463 .expect("to_private_key_pem")
464 .to_string();
465 let restored = BiscuitConfig::from_pem(&pem).expect("from_pem");
466 assert_eq!(
468 restored.active_public_key().to_bytes(),
469 cfg.active_public_key().to_bytes(),
470 );
471 }
472
473 #[test]
474 fn public_pem_is_non_empty_and_pem_shaped() {
475 let cfg = cfg();
476 let pem = cfg.public_pem().expect("public_pem");
477 assert!(pem.contains("PUBLIC KEY"), "got: {pem}");
478 }
479
480 #[test]
481 fn active_kid_is_stable_across_clones() {
482 let cfg = cfg();
483 let kid = cfg.active_kid();
484 let dup = cfg.clone();
485 assert_eq!(kid, dup.active_kid());
486 }
487}