use ed25519_dalek::{Signature, Verifier as _, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::claims::ActClaims;
use crate::error::{ActError, ActResult};
use crate::scope::Scope;
pub const FORMAT_VERSION: u64 = 1;
pub const ALGORITHM_ED25519: &str = "Ed25519";
pub const MAX_ACT_BYTES: usize = 16 * 1024;
const SIGNATURE_BYTES: usize = 64;
pub type PublicKeyBytes = [u8; 32];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActEnvelope {
pub v: u64,
pub alg: String,
pub claims: serde_bytes::ByteBuf,
pub sig: serde_bytes::ByteBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
}
impl ActEnvelope {
pub fn decode(bytes: &[u8]) -> ActResult<Self> {
if bytes.len() > MAX_ACT_BYTES {
return Err(ActError::TooLarge {
actual: bytes.len(),
limit: MAX_ACT_BYTES,
});
}
let envelope: Self =
ciborium::from_reader(bytes).map_err(|e| ActError::EnvelopeDecode(e.to_string()))?;
if envelope.v != FORMAT_VERSION {
return Err(ActError::UnsupportedVersion {
found: envelope.v,
supported: FORMAT_VERSION,
});
}
if envelope.alg != ALGORITHM_ED25519 {
return Err(ActError::UnsupportedAlgorithm(envelope.alg));
}
Ok(envelope)
}
pub fn encode(&self) -> ActResult<Vec<u8>> {
let mut buffer = Vec::new();
ciborium::into_writer(self, &mut buffer).map_err(|e| ActError::Encode(e.to_string()))?;
Ok(buffer)
}
}
#[derive(Debug, Clone)]
pub struct Verifier {
trusted_keys: Vec<PublicKeyBytes>,
expected_issuer: String,
expected_audience: String,
required_scopes: Vec<Scope>,
clock: Option<i64>,
leeway_seconds: i64,
}
impl Verifier {
pub fn new(
trusted_keys: Vec<PublicKeyBytes>,
expected_issuer: impl Into<String>,
expected_audience: impl Into<String>,
) -> ActResult<Self> {
if trusted_keys.is_empty() {
return Err(ActError::NoTrustedKeys);
}
Ok(Self {
trusted_keys,
expected_issuer: expected_issuer.into(),
expected_audience: expected_audience.into(),
required_scopes: Vec::new(),
clock: None,
leeway_seconds: 0,
})
}
#[must_use]
pub fn requiring_scopes(mut self, scopes: impl IntoIterator<Item = Scope>) -> Self {
self.required_scopes.extend(scopes);
self
}
#[must_use]
pub fn with_leeway_seconds(mut self, seconds: i64) -> Self {
self.leeway_seconds = seconds.abs();
self
}
#[must_use]
pub fn with_clock(mut self, now_unix_seconds: i64) -> Self {
self.clock = Some(now_unix_seconds);
self
}
fn now(&self) -> ActResult<i64> {
match self.clock {
Some(pinned) => Ok(pinned),
None => std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| {
let secs = d.as_secs();
i64::try_from(secs).unwrap_or(i64::MAX)
})
.map_err(|_| ActError::Encode("system clock is before the Unix epoch".to_string())),
}
}
}
pub fn verify(token_bytes: &[u8], verifier: &Verifier) -> ActResult<ActClaims> {
let envelope = ActEnvelope::decode(token_bytes)?;
if envelope.sig.is_empty() {
return Err(ActError::MissingSignature);
}
if envelope.sig.len() != SIGNATURE_BYTES {
return Err(ActError::MalformedSignature(envelope.sig.len()));
}
let signature_array: [u8; SIGNATURE_BYTES] =
<[u8; SIGNATURE_BYTES]>::try_from(envelope.sig.as_ref())
.map_err(|_| ActError::MalformedSignature(envelope.sig.len()))?;
let signature = Signature::from_bytes(&signature_array);
let candidate_keys = decode_trusted_keys(verifier)?;
let verified = candidate_keys
.iter()
.any(|key| key.verify(envelope.claims.as_ref(), &signature).is_ok());
if !verified {
return Err(ActError::SignatureInvalid {
tried: candidate_keys.len(),
});
}
let claims_bytes: &[u8] = envelope.claims.as_ref();
let claims: ActClaims =
ciborium::from_reader(claims_bytes).map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
claims.validate_structure()?;
check_policy(&claims, verifier)?;
Ok(claims)
}
fn decode_trusted_keys(verifier: &Verifier) -> ActResult<Vec<VerifyingKey>> {
verifier
.trusted_keys
.iter()
.enumerate()
.map(|(index, raw)| {
VerifyingKey::from_bytes(raw).map_err(|e| ActError::MalformedTrustedKey {
index,
reason: e.to_string(),
})
})
.collect()
}
fn check_policy(claims: &ActClaims, verifier: &Verifier) -> ActResult<()> {
let now = verifier.now()?;
if claims.exp <= now.saturating_sub(verifier.leeway_seconds) {
return Err(ActError::Expired {
expired_at: claims.exp,
now,
});
}
if claims.nbf > now.saturating_add(verifier.leeway_seconds) {
return Err(ActError::NotYetValid {
valid_from: claims.nbf,
now,
});
}
if claims.iss != verifier.expected_issuer {
return Err(ActError::IssuerMismatch {
expected: verifier.expected_issuer.clone(),
found: claims.iss.clone(),
});
}
if !claims.has_audience(&verifier.expected_audience) {
return Err(ActError::AudienceMismatch {
expected: verifier.expected_audience.clone(),
found: claims.aud.clone(),
});
}
for required in &verifier.required_scopes {
if !claims.authorizes(required) {
return Err(ActError::MissingScope(required.to_string()));
}
}
Ok(())
}
pub fn claims_to_signing_payload(claims: &ActClaims) -> ActResult<Vec<u8>> {
claims.validate_structure()?;
let mut buffer = Vec::new();
ciborium::into_writer(claims, &mut buffer).map_err(|e| ActError::Encode(e.to_string()))?;
Ok(buffer)
}
pub fn envelope_from_parts(
claims_bytes: Vec<u8>,
signature: &[u8],
kid: Option<String>,
) -> ActResult<Vec<u8>> {
if signature.len() != SIGNATURE_BYTES {
return Err(ActError::MalformedSignature(signature.len()));
}
let envelope = ActEnvelope {
v: FORMAT_VERSION,
alg: ALGORITHM_ED25519.to_string(),
claims: serde_bytes::ByteBuf::from(claims_bytes),
sig: serde_bytes::ByteBuf::from(signature.to_vec()),
kid,
};
let encoded = envelope.encode()?;
if encoded.len() > MAX_ACT_BYTES {
return Err(ActError::TooLarge {
actual: encoded.len(),
limit: MAX_ACT_BYTES,
});
}
Ok(encoded)
}
#[cfg(feature = "sign")]
pub fn sign(
claims: &ActClaims,
signing_key: &ed25519_dalek::SigningKey,
kid: Option<String>,
) -> ActResult<Vec<u8>> {
use ed25519_dalek::Signer;
let claims_bytes = claims_to_signing_payload(claims)?;
let signature = signing_key.sign(&claims_bytes);
envelope_from_parts(claims_bytes, &signature.to_bytes(), kid)
}