use lys_core::keys::Ed25519Identity;
use serde_json::{Value, json};
use zeroize::Zeroizing;
pub struct Issuer<'a> {
pub url: &'a str,
pub key_id: &'a str,
pub identity: &'a Ed25519Identity,
}
pub struct Options<'a> {
pub holder: &'a str,
pub expires_at: u64,
pub issued_at: u64,
}
impl<'a> From<&'a str> for Options<'a> {
fn from(holder: &'a str) -> Self {
Self {
holder,
expires_at: u64::MAX,
issued_at: 0,
}
}
}
pub fn mint<'a>(
options: impl Into<Options<'a>>,
rights: &[Value],
issuer: &Issuer<'_>,
) -> Result<Zeroizing<String>, Box<dyn std::error::Error>> {
let Options {
holder,
expires_at,
issued_at,
} = options.into();
let claims = json!({
"iss": issuer.url, "sub": holder, "aud": "liminal", "iat": issued_at, "exp": expires_at,
"holder": {"id": holder, "kind": "agent", "responsible": "owner"},
"rights": rights,
});
let header = base64url(&serde_json::to_vec(&json!({
"alg": "EdDSA", "typ": "JWT", "kid": issuer.key_id,
}))?);
let signed = format!("{header}.{}", base64url(&serde_json::to_vec(&claims)?));
Ok(Zeroizing::new(format!(
"{signed}.{}",
base64url(&issuer.identity.sign(signed.as_bytes()))
)))
}
#[must_use]
pub fn base64url(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut encoded = String::new();
for chunk in bytes.chunks(3) {
let first = chunk[0];
let second = chunk.get(1).copied().unwrap_or(0);
let third = chunk.get(2).copied().unwrap_or(0);
encoded.push(char::from(ALPHABET[usize::from(first >> 2)]));
encoded.push(char::from(
ALPHABET[usize::from((first & 3) << 4 | second >> 4)],
));
if chunk.len() > 1 {
encoded.push(char::from(
ALPHABET[usize::from((second & 15) << 2 | third >> 6)],
));
}
if chunk.len() > 2 {
encoded.push(char::from(ALPHABET[usize::from(third & 63)]));
}
}
encoded
}