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(
137 BiscuitBuilder,
138 ) -> std::result::Result<BiscuitBuilder, biscuit_auth::error::Token>,
139 {
140 let builder = build(Biscuit::builder())
141 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit build: {e}")))?;
142 let guard = self.inner.read();
143 let token = builder
144 .build(&guard.active.keypair)
145 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit sign: {e}")))?;
146 token
147 .to_base64()
148 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64: {e}")))
149 }
150
151 pub fn verify<F>(&self, token: &str, build: F) -> Result<()>
161 where
162 F: FnOnce(
163 AuthorizerBuilder,
164 ) -> std::result::Result<AuthorizerBuilder, biscuit_auth::error::Token>,
165 {
166 let guard = self.inner.read();
167 let parsed = match Biscuit::from_base64(token, guard.active.keypair.public()) {
170 Ok(t) => t,
171 Err(active_err) => {
172 let mut last = active_err;
173 let mut found = None;
174 for hist in &guard.history {
175 match Biscuit::from_base64(token, hist.public_key) {
176 Ok(t) => {
177 found = Some(t);
178 break;
179 }
180 Err(e) => last = e,
181 }
182 }
183 match found {
184 Some(t) => t,
185 None => {
186 return Err(Error::Backend(anyhow::anyhow!(
187 "biscuit signature verify: {last}"
188 )));
189 }
190 }
191 }
192 };
193 let authorizer_builder = build(AuthorizerBuilder::new())
194 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer build: {e}")))?;
195 let mut authorizer = authorizer_builder
196 .build(&parsed)
197 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer attach: {e}")))?;
198 authorizer
199 .authorize()
200 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorize: {e}")))?;
201 Ok(())
202 }
203}
204
205pub fn attenuate<F>(token: &str, root_public: PublicKey, build: F) -> Result<String>
214where
215 F: FnOnce(BlockBuilder) -> std::result::Result<BlockBuilder, biscuit_auth::error::Token>,
216{
217 let parsed = Biscuit::from_base64(token, root_public)
218 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit parse for attenuate: {e}")))?;
219 let block = build(BlockBuilder::new())
220 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit block build: {e}")))?;
221 let attenuated = parsed
222 .append(block)
223 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit append block: {e}")))?;
224 attenuated
225 .to_base64()
226 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64 (attenuated): {e}")))
227}
228
229#[cfg(feature = "backend-postgres")]
236pub async fn load_or_init_postgres(pool: &sqlx::PgPool) -> Result<BiscuitConfig> {
237 use sqlx::Row;
238 let row = sqlx::query(
239 "SELECT kid, private_pem
240 FROM auth.biscuit_root_keys
241 WHERE rotated_at IS NULL
242 ORDER BY created_at DESC
243 LIMIT 1",
244 )
245 .fetch_optional(pool)
246 .await
247 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (pg): {e}")))?;
248
249 if let Some(row) = row {
250 let kid: String = row.get("kid");
251 let pem_bytes: Vec<u8> = row.get("private_pem");
252 let pem = std::str::from_utf8(&pem_bytes)
253 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
254 let keypair = KeyPair::from_private_key_pem(pem)
255 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
256 Ok(BiscuitConfig::from_active(
257 ActiveRootKey { kid, keypair },
258 Vec::new(),
259 ))
260 } else {
261 let keypair = KeyPair::new();
262 let kid = mint_kid(&keypair.public());
263 let private_pem = keypair
264 .to_private_key_pem()
265 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
266 .to_string();
267 let public_pem = keypair
268 .public()
269 .to_pem()
270 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
271 let now = now_secs();
272 sqlx::query(
273 "INSERT INTO auth.biscuit_root_keys
274 (kid, private_pem, public_pem, created_at, rotated_at)
275 VALUES ($1, $2, $3, $4, NULL)",
276 )
277 .bind(&kid)
278 .bind(private_pem.as_bytes())
279 .bind(&public_pem)
280 .bind(now)
281 .execute(pool)
282 .await
283 .map_err(|e| Error::Backend(anyhow::anyhow!("insert auth.biscuit_root_keys (pg): {e}")))?;
284 Ok(BiscuitConfig::from_active(
285 ActiveRootKey { kid, keypair },
286 Vec::new(),
287 ))
288 }
289}
290
291#[cfg(feature = "backend-sqlite")]
293pub async fn load_or_init_sqlite(pool: &sqlx::SqlitePool) -> Result<BiscuitConfig> {
294 use sqlx::Row;
295 let row = sqlx::query(
296 "SELECT kid, private_pem
297 FROM auth.biscuit_root_keys
298 WHERE rotated_at IS NULL
299 ORDER BY created_at DESC
300 LIMIT 1",
301 )
302 .fetch_optional(pool)
303 .await
304 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (sqlite): {e}")))?;
305
306 if let Some(row) = row {
307 let kid: String = row.get("kid");
308 let pem_bytes: Vec<u8> = row.get("private_pem");
309 let pem = std::str::from_utf8(&pem_bytes)
310 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
311 let keypair = KeyPair::from_private_key_pem(pem)
312 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
313 Ok(BiscuitConfig::from_active(
314 ActiveRootKey { kid, keypair },
315 Vec::new(),
316 ))
317 } else {
318 let keypair = KeyPair::new();
319 let kid = mint_kid(&keypair.public());
320 let private_pem = keypair
321 .to_private_key_pem()
322 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
323 .to_string();
324 let public_pem = keypair
325 .public()
326 .to_pem()
327 .map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
328 let now = now_secs();
329 sqlx::query(
330 "INSERT INTO auth.biscuit_root_keys
331 (kid, private_pem, public_pem, created_at, rotated_at)
332 VALUES (?, ?, ?, ?, NULL)",
333 )
334 .bind(&kid)
335 .bind(private_pem.as_bytes())
336 .bind(&public_pem)
337 .bind(now)
338 .execute(pool)
339 .await
340 .map_err(|e| {
341 Error::Backend(anyhow::anyhow!(
342 "insert auth.biscuit_root_keys (sqlite): {e}"
343 ))
344 })?;
345 Ok(BiscuitConfig::from_active(
346 ActiveRootKey { kid, keypair },
347 Vec::new(),
348 ))
349 }
350}
351
352fn mint_kid(public_key: &PublicKey) -> String {
357 let bytes = public_key.to_bytes();
358 let prefix = if bytes.len() >= 16 {
359 &bytes[..16]
360 } else {
361 &bytes
362 };
363 format!("kid_{}", data_encoding::BASE64URL_NOPAD.encode(prefix))
364}
365
366fn now_secs() -> f64 {
367 std::time::SystemTime::now()
368 .duration_since(std::time::UNIX_EPOCH)
369 .unwrap_or_default()
370 .as_secs_f64()
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn cfg() -> BiscuitConfig {
378 BiscuitConfig::generate_ephemeral()
379 }
380
381 #[test]
382 fn issue_and_verify_round_trip() {
383 let cfg = cfg();
384 let token = cfg
385 .issue(|b| {
386 b.fact("user(\"alice\")")
387 .and_then(|b| b.fact("role(\"admin\")"))
388 })
389 .expect("issue");
390 cfg.verify(&token, |a| a.policy("allow if user(\"alice\")"))
391 .expect("verify");
392 }
393
394 #[test]
395 fn verify_rejects_tampered_token() {
396 let cfg = cfg();
397 let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
398 let mut bytes = token.into_bytes();
400 let half = bytes.len() / 2;
401 bytes[half] ^= 0x01;
402 let tampered = String::from_utf8_lossy(&bytes).to_string();
403 let result = cfg.verify(&tampered, |a| a.policy("allow if user(\"alice\")"));
404 assert!(matches!(result, Err(Error::Backend(_))));
405 }
406
407 #[test]
408 fn verify_with_unknown_root_key_fails() {
409 let cfg_a = cfg();
411 let cfg_b = cfg();
412 let token = cfg_a.issue(|b| b.fact("user(\"alice\")")).expect("issue");
413 let result = cfg_b.verify(&token, |a| a.policy("allow if user(\"alice\")"));
414 assert!(matches!(result, Err(Error::Backend(_))));
415 }
416
417 #[test]
418 fn attenuate_produces_valid_child_token() {
419 let cfg = cfg();
420 let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
421 let pubkey = cfg.active_public_key();
422 let attenuated = attenuate(&token, pubkey, |b| b.check("check if operation(\"read\")"))
425 .expect("attenuate");
426 cfg.verify(&attenuated, |a| {
428 a.fact("operation(\"read\")")
429 .and_then(|a| a.policy("allow if user(\"alice\")"))
430 })
431 .expect("read should pass");
432 let result = cfg.verify(&attenuated, |a| a.policy("allow if user(\"alice\")"));
435 assert!(matches!(result, Err(Error::Backend(_))));
436 }
437
438 #[test]
439 fn time_based_check_rejects_after_expiry() {
440 let cfg = cfg();
441 let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
443 let pubkey = cfg.active_public_key();
444 let attenuated = attenuate(&token, pubkey, |b| {
445 b.check("check if time($now), $now < 2000-01-01T00:00:00Z")
448 })
449 .expect("attenuate");
450 let result = cfg.verify(&attenuated, |a| a.time().policy("allow if user(\"alice\")"));
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}