cat_gateway/service/common/auth/rbac/
token.rsuse std::{
fmt::{Display, Formatter},
time::{Duration, SystemTime},
};
use anyhow::{bail, Ok};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use pallas::codec::minicbor;
use tracing::error;
use ulid::Ulid;
use crate::utils::blake2b_hash::blake2b_128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Kid(pub [u8; 16]);
impl From<&VerifyingKey> for Kid {
fn from(vk: &VerifyingKey) -> Self {
Self(blake2b_128(vk.as_bytes()))
}
}
impl PartialEq<VerifyingKey> for Kid {
fn eq(&self, other: &VerifyingKey) -> bool {
self == &Kid::from(other)
}
}
#[derive(Debug, Clone, Copy)]
struct UlidBytes(pub [u8; 16]);
#[derive(Debug, Clone)]
pub struct SignatureEd25519(pub [u8; 64]);
#[derive(Debug, Clone)]
pub(crate) struct CatalystRBACTokenV1 {
pub(crate) kid: Kid,
pub(crate) ulid: Ulid,
pub(crate) sig: SignatureEd25519,
raw: Vec<u8>,
}
impl CatalystRBACTokenV1 {
const AUTH_TOKEN_PREFIX: &str = "catv1";
const KID_ULID_CBOR_ENCODED_BYTES: u8 = 34;
#[allow(dead_code, clippy::expect_used)]
pub(crate) fn new(sk: &SigningKey) -> Self {
let vk: ed25519_dalek::VerifyingKey = sk.verifying_key();
let kid = Kid::from(&vk);
let ulid = Ulid::new();
let out: Vec<u8> = Vec::new();
let mut encoder = minicbor::Encoder::new(out);
encoder.bytes(&kid.0).expect("This should never fail.");
encoder
.bytes(&ulid.to_bytes())
.expect("This should never fail");
let sig = SignatureEd25519(sk.sign(encoder.writer()).to_bytes());
encoder.bytes(&sig.0).expect("This should never fail");
Self {
kid,
ulid,
sig,
raw: encoder.writer().clone(),
}
}
pub(crate) fn decode(auth_token: &str) -> anyhow::Result<Self> {
let token = auth_token.split('.').collect::<Vec<&str>>();
let prefix = token.first().ok_or(anyhow::anyhow!("No valid prefix"))?;
if *prefix != Self::AUTH_TOKEN_PREFIX {
return Err(anyhow::anyhow!("Corrupt token, invalid prefix"));
}
let token_base64 = token.get(1).ok_or(anyhow::anyhow!("No valid token"))?;
let token_cbor_encoded = BASE64_URL_SAFE_NO_PAD.decode(token_base64)?;
let mut cbor_decoder = minicbor::Decoder::new(&token_cbor_encoded);
let kid = Kid(cbor_decoder
.bytes()
.map_err(|e| anyhow::anyhow!(format!("Invalid cbor for kid : {e}")))?
.try_into()?);
let ulid_raw: UlidBytes = UlidBytes(
cbor_decoder
.bytes()
.map_err(|e| anyhow::anyhow!(format!("Invalid cbor for ulid : {e}")))?
.try_into()?,
);
let ulid = Ulid::from_bytes(ulid_raw.0);
let signature = SignatureEd25519(
cbor_decoder
.bytes()
.map_err(|e| anyhow::anyhow!(format!("Invalid cbor for signature : {e}")))?
.try_into()?,
);
Ok(CatalystRBACTokenV1 {
kid,
ulid,
sig: signature,
raw: token_cbor_encoded,
})
}
pub(crate) fn verify(&self, public_key: &VerifyingKey) -> anyhow::Result<()> {
if self.kid != *public_key {
error!(token=%self, public_key=?public_key,
"Tokens Kid did not match verifying Public Key",
);
bail!("Kid does not match PublicKey.")
}
let message_cbor_encoded = self
.raw
.get(0..Self::KID_ULID_CBOR_ENCODED_BYTES.into())
.ok_or(anyhow::anyhow!("No valid token"))?;
if let Err(error) =
public_key.verify_strict(message_cbor_encoded, &Signature::from_bytes(&self.sig.0))
{
error!(error=%error, token=%self, public_key=?public_key,
"Token was not signed by the expected Public Key",
);
bail!("Token Not Validated");
}
Ok(())
}
pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
let now = SystemTime::now();
let token_age = self.ulid.datetime();
let Some(min_time) = now.checked_sub(max_age) else {
return false;
};
let Some(max_time) = now.checked_add(max_skew) else {
return false;
};
(min_time < token_age) && (max_time > token_age)
}
}
impl Display for CatalystRBACTokenV1 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}.{}",
CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
BASE64_URL_SAFE_NO_PAD.encode(self.raw.clone())
)
}
}
#[cfg(test)]
mod tests {
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use super::*;
#[test]
fn test_token_generation_and_decoding() {
let mut random_seed = OsRng;
let signing_key: SigningKey = SigningKey::generate(&mut random_seed);
let verifying_key = signing_key.verifying_key();
let signing_key2: SigningKey = SigningKey::generate(&mut random_seed);
let verifying_key2 = signing_key2.verifying_key();
let kid = Kid::from(&verifying_key);
assert!(kid == verifying_key);
assert!(kid != verifying_key2);
let token = CatalystRBACTokenV1::new(&signing_key);
assert!(token.verify(&verifying_key).is_ok());
assert!(token.verify(&verifying_key2).is_err());
let decoded_token = format!("{token}");
let re_encoded_token = CatalystRBACTokenV1::decode(&decoded_token)
.expect("Failed to decode a token we encoded.");
assert!(re_encoded_token.verify(&verifying_key).is_ok());
assert!(re_encoded_token.verify(&verifying_key2).is_err());
}
#[test]
fn is_young() {
let mut random_seed = OsRng;
let key = SigningKey::generate(&mut random_seed);
let mut token = CatalystRBACTokenV1::new(&key);
let now = SystemTime::now();
token.ulid = Ulid::from_datetime(now - Duration::from_secs(2));
let max_age = Duration::from_secs(1);
let max_skew = Duration::from_secs(1);
assert!(!token.is_young(max_age, max_skew));
let max_age = Duration::from_secs(3);
assert!(token.is_young(max_age, max_skew));
token.ulid = Ulid::from_datetime(now + Duration::from_secs(2));
let max_skew = Duration::from_secs(1);
assert!(!token.is_young(max_age, max_skew));
let max_skew = Duration::from_secs(3);
assert!(token.is_young(max_age, max_skew));
}
}