use std::sync::Arc;
use biscuit_auth::builder::AuthorizerBuilder;
use biscuit_auth::{Biscuit, BiscuitBuilder, BlockBuilder, KeyPair, PublicKey};
use parking_lot::RwLock;
use crate::error::{Error, Result};
struct Inner {
active: ActiveRootKey,
history: Vec<HistoryRootKey>,
}
pub struct ActiveRootKey {
pub kid: String,
pub keypair: KeyPair,
}
pub struct HistoryRootKey {
pub kid: String,
pub public_key: PublicKey,
}
#[derive(Clone)]
pub struct BiscuitConfig {
inner: Arc<RwLock<Inner>>,
}
impl BiscuitConfig {
pub fn from_active(active: ActiveRootKey, history: Vec<HistoryRootKey>) -> Self {
Self {
inner: Arc::new(RwLock::new(Inner { active, history })),
}
}
pub fn generate_ephemeral() -> Self {
let keypair = KeyPair::new();
let kid = mint_kid(&keypair.public());
Self::from_active(ActiveRootKey { kid, keypair }, Vec::new())
}
pub fn from_pem(pem: &str) -> Result<Self> {
let keypair = KeyPair::from_private_key_pem(pem)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit root key from pem: {e}")))?;
let kid = mint_kid(&keypair.public());
Ok(Self::from_active(
ActiveRootKey { kid, keypair },
Vec::new(),
))
}
pub fn active_kid(&self) -> String {
self.inner.read().active.kid.clone()
}
pub fn public_pem(&self) -> Result<String> {
self.inner
.read()
.active
.keypair
.public()
.to_pem()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public pem: {e}")))
}
pub fn active_public_key(&self) -> PublicKey {
self.inner.read().active.keypair.public()
}
pub fn issue<F>(&self, build: F) -> Result<String>
where
F: FnOnce(
BiscuitBuilder,
) -> std::result::Result<BiscuitBuilder, biscuit_auth::error::Token>,
{
let builder = build(Biscuit::builder())
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit build: {e}")))?;
let guard = self.inner.read();
let token = builder
.build(&guard.active.keypair)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit sign: {e}")))?;
token
.to_base64()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64: {e}")))
}
pub fn verify<F>(&self, token: &str, build: F) -> Result<()>
where
F: FnOnce(
AuthorizerBuilder,
) -> std::result::Result<AuthorizerBuilder, biscuit_auth::error::Token>,
{
let guard = self.inner.read();
let parsed = match Biscuit::from_base64(token, guard.active.keypair.public()) {
Ok(t) => t,
Err(active_err) => {
let mut last = active_err;
let mut found = None;
for hist in &guard.history {
match Biscuit::from_base64(token, hist.public_key) {
Ok(t) => {
found = Some(t);
break;
}
Err(e) => last = e,
}
}
match found {
Some(t) => t,
None => {
return Err(Error::Backend(anyhow::anyhow!(
"biscuit signature verify: {last}"
)));
}
}
}
};
let authorizer_builder = build(AuthorizerBuilder::new())
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer build: {e}")))?;
let mut authorizer = authorizer_builder
.build(&parsed)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorizer attach: {e}")))?;
authorizer
.authorize_with_limits(biscuit_auth::AuthorizerLimits {
max_time: std::time::Duration::from_secs(10),
..Default::default()
})
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit authorize: {e}")))?;
Ok(())
}
}
pub fn attenuate<F>(token: &str, root_public: PublicKey, build: F) -> Result<String>
where
F: FnOnce(BlockBuilder) -> std::result::Result<BlockBuilder, biscuit_auth::error::Token>,
{
let parsed = Biscuit::from_base64(token, root_public)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit parse for attenuate: {e}")))?;
let block = build(BlockBuilder::new())
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit block build: {e}")))?;
let attenuated = parsed
.append(block)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit append block: {e}")))?;
attenuated
.to_base64()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit base64 (attenuated): {e}")))
}
#[cfg(feature = "backend-postgres")]
pub async fn load_or_init_postgres(pool: &sqlx::PgPool) -> Result<BiscuitConfig> {
use sqlx::Row;
let row = sqlx::query(
"SELECT kid, private_pem
FROM auth.biscuit_root_keys
WHERE rotated_at IS NULL
ORDER BY created_at DESC
LIMIT 1",
)
.fetch_optional(pool)
.await
.map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (pg): {e}")))?;
if let Some(row) = row {
let kid: String = row.get("kid");
let pem_bytes: Vec<u8> = row.get("private_pem");
let pem = std::str::from_utf8(&pem_bytes)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
let keypair = KeyPair::from_private_key_pem(pem)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
Ok(BiscuitConfig::from_active(
ActiveRootKey { kid, keypair },
Vec::new(),
))
} else {
let keypair = KeyPair::new();
let kid = mint_kid(&keypair.public());
let private_pem = keypair
.to_private_key_pem()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
.to_string();
let public_pem = keypair
.public()
.to_pem()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
let now = now_secs();
sqlx::query(
"INSERT INTO auth.biscuit_root_keys
(kid, private_pem, public_pem, created_at, rotated_at)
VALUES ($1, $2, $3, $4, NULL)",
)
.bind(&kid)
.bind(private_pem.as_bytes())
.bind(&public_pem)
.bind(now)
.execute(pool)
.await
.map_err(|e| Error::Backend(anyhow::anyhow!("insert auth.biscuit_root_keys (pg): {e}")))?;
Ok(BiscuitConfig::from_active(
ActiveRootKey { kid, keypair },
Vec::new(),
))
}
}
#[cfg(feature = "backend-sqlite")]
pub async fn load_or_init_sqlite(pool: &sqlx::SqlitePool) -> Result<BiscuitConfig> {
use sqlx::Row;
let row = sqlx::query(
"SELECT kid, private_pem
FROM auth.biscuit_root_keys
WHERE rotated_at IS NULL
ORDER BY created_at DESC
LIMIT 1",
)
.fetch_optional(pool)
.await
.map_err(|e| Error::Backend(anyhow::anyhow!("load auth.biscuit_root_keys (sqlite): {e}")))?;
if let Some(row) = row {
let kid: String = row.get("kid");
let pem_bytes: Vec<u8> = row.get("private_pem");
let pem = std::str::from_utf8(&pem_bytes)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit private_pem utf8: {e}")))?;
let keypair = KeyPair::from_private_key_pem(pem)
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit from_private_key_pem: {e}")))?;
Ok(BiscuitConfig::from_active(
ActiveRootKey { kid, keypair },
Vec::new(),
))
} else {
let keypair = KeyPair::new();
let kid = mint_kid(&keypair.public());
let private_pem = keypair
.to_private_key_pem()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit to_private_key_pem: {e}")))?
.to_string();
let public_pem = keypair
.public()
.to_pem()
.map_err(|e| Error::Backend(anyhow::anyhow!("biscuit public to_pem: {e}")))?;
let now = now_secs();
sqlx::query(
"INSERT INTO auth.biscuit_root_keys
(kid, private_pem, public_pem, created_at, rotated_at)
VALUES (?, ?, ?, ?, NULL)",
)
.bind(&kid)
.bind(private_pem.as_bytes())
.bind(&public_pem)
.bind(now)
.execute(pool)
.await
.map_err(|e| {
Error::Backend(anyhow::anyhow!(
"insert auth.biscuit_root_keys (sqlite): {e}"
))
})?;
Ok(BiscuitConfig::from_active(
ActiveRootKey { kid, keypair },
Vec::new(),
))
}
}
fn mint_kid(public_key: &PublicKey) -> String {
let bytes = public_key.to_bytes();
let prefix = if bytes.len() >= 16 {
&bytes[..16]
} else {
&bytes
};
format!("kid_{}", data_encoding::BASE64URL_NOPAD.encode(prefix))
}
fn now_secs() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> BiscuitConfig {
BiscuitConfig::generate_ephemeral()
}
#[test]
fn issue_and_verify_round_trip() {
let cfg = cfg();
let token = cfg
.issue(|b| {
b.fact("user(\"alice\")")
.and_then(|b| b.fact("role(\"admin\")"))
})
.expect("issue");
cfg.verify(&token, |a| a.policy("allow if user(\"alice\")"))
.expect("verify");
}
#[test]
fn verify_rejects_tampered_token() {
let cfg = cfg();
let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
let mut bytes = token.into_bytes();
let half = bytes.len() / 2;
bytes[half] ^= 0x01;
let tampered = String::from_utf8_lossy(&bytes).to_string();
let result = cfg.verify(&tampered, |a| a.policy("allow if user(\"alice\")"));
assert!(matches!(result, Err(Error::Backend(_))));
}
#[test]
fn verify_with_unknown_root_key_fails() {
let cfg_a = cfg();
let cfg_b = cfg();
let token = cfg_a.issue(|b| b.fact("user(\"alice\")")).expect("issue");
let result = cfg_b.verify(&token, |a| a.policy("allow if user(\"alice\")"));
assert!(matches!(result, Err(Error::Backend(_))));
}
#[test]
fn attenuate_produces_valid_child_token() {
let cfg = cfg();
let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
let pubkey = cfg.active_public_key();
let attenuated = attenuate(&token, pubkey, |b| b.check("check if operation(\"read\")"))
.expect("attenuate");
cfg.verify(&attenuated, |a| {
a.fact("operation(\"read\")")
.and_then(|a| a.policy("allow if user(\"alice\")"))
})
.expect("read should pass");
let result = cfg.verify(&attenuated, |a| a.policy("allow if user(\"alice\")"));
assert!(matches!(result, Err(Error::Backend(_))));
}
#[test]
fn time_based_check_rejects_after_expiry() {
let cfg = cfg();
let token = cfg.issue(|b| b.fact("user(\"alice\")")).expect("issue");
let pubkey = cfg.active_public_key();
let attenuated = attenuate(&token, pubkey, |b| {
b.check("check if time($now), $now < 2000-01-01T00:00:00Z")
})
.expect("attenuate");
let result = cfg.verify(&attenuated, |a| a.time().policy("allow if user(\"alice\")"));
assert!(matches!(result, Err(Error::Backend(_))));
}
#[test]
fn from_pem_round_trips() {
let cfg = cfg();
let pem = cfg
.inner
.read()
.active
.keypair
.to_private_key_pem()
.expect("to_private_key_pem")
.to_string();
let restored = BiscuitConfig::from_pem(&pem).expect("from_pem");
assert_eq!(
restored.active_public_key().to_bytes(),
cfg.active_public_key().to_bytes(),
);
}
#[test]
fn public_pem_is_non_empty_and_pem_shaped() {
let cfg = cfg();
let pem = cfg.public_pem().expect("public_pem");
assert!(pem.contains("PUBLIC KEY"), "got: {pem}");
}
#[test]
fn active_kid_is_stable_across_clones() {
let cfg = cfg();
let kid = cfg.active_kid();
let dup = cfg.clone();
assert_eq!(kid, dup.active_kid());
}
}