use crate::AccessError;
use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
#[cfg(feature = "client")]
use crate::principal::AccessIssuer;
use crate::principal::{TrustedIssuer, issuer_key_id};
#[cfg(feature = "client")]
use crate::signed::sign;
use crate::signed::verify;
const REVOCATION_TAG: u8 = 3;
const REVOCATION_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/revocation/v1";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RevocationSpec {
pub revision: u64,
pub minimum_authorization_epoch: u64,
pub minimum_role_key_epoch: u64,
pub issued_at: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RevocationState {
tenant_id: String,
subject_id: String,
scope: String,
revision: u64,
minimum_authorization_epoch: u64,
minimum_role_key_epoch: u64,
issued_at: u64,
issuer_id: String,
issuer_key_id: [u8; 32],
issuer_public_key: [u8; 32],
signature: [u8; 64],
}
impl RevocationState {
#[cfg(feature = "client")]
pub fn issue(
issuer: &AccessIssuer,
tenant_id: impl Into<String>,
subject_id: impl Into<String>,
scope: impl Into<String>,
spec: RevocationSpec,
) -> Result<Self, AccessError> {
let mut state = Self {
tenant_id: tenant_id.into(),
subject_id: subject_id.into(),
scope: scope.into(),
revision: spec.revision,
minimum_authorization_epoch: spec.minimum_authorization_epoch,
minimum_role_key_epoch: spec.minimum_role_key_epoch,
issued_at: spec.issued_at,
issuer_id: issuer.issuer_id().to_owned(),
issuer_key_id: issuer.key_id(),
issuer_public_key: issuer.public_key(),
signature: [0; 64],
};
state.validate_structure(&AccessValidationPolicy::default())?;
state.signature = sign(
REVOCATION_SIGNATURE_DOMAIN,
issuer.signing_key(),
&state.unsigned_bytes(),
);
Ok(state)
}
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn subject_id(&self) -> &str {
&self.subject_id
}
pub fn scope(&self) -> &str {
&self.scope
}
pub const fn revision(&self) -> u64 {
self.revision
}
pub fn encode(&self) -> Vec<u8> {
let mut out = self.unsigned_bytes();
out.extend_from_slice(&self.signature);
out
}
pub fn decode(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
let mut cursor = Cursor::new(bytes);
cursor.take_header(REVOCATION_TAG)?;
let state = Self {
tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
subject_id: cursor.take_string(limits.max_identifier_bytes)?,
scope: cursor.take_string(limits.max_identifier_bytes)?,
revision: cursor.take_u64()?,
minimum_authorization_epoch: cursor.take_u64()?,
minimum_role_key_epoch: cursor.take_u64()?,
issued_at: cursor.take_u64()?,
issuer_id: cursor.take_string(limits.max_identifier_bytes)?,
issuer_key_id: cursor.take_array32()?,
issuer_public_key: cursor.take_array32()?,
signature: cursor.take_array64()?,
};
if !cursor.is_empty() {
return Err(AccessError::TrailingBytes);
}
state.validate_structure(limits)?;
state.verify_signature()?;
if state.encode() != bytes {
return Err(AccessError::NonCanonicalEncoding);
}
Ok(state)
}
pub fn verify<'a>(
&'a self,
trusted: &TrustedIssuer,
expected_tenant: &str,
expected_subject: &str,
expected_scope: &str,
) -> Result<VerifiedRevocation<'a>, AccessError> {
self.verify_signature()?;
if !self.issuer_matches(trusted) {
return Err(AccessError::UntrustedIssuer);
}
if self.tenant_id != expected_tenant
|| self.subject_id != expected_subject
|| self.scope != expected_scope
{
return Err(AccessError::SubjectMismatch);
}
Ok(VerifiedRevocation { state: self })
}
fn issuer_matches(&self, trusted: &TrustedIssuer) -> bool {
self.issuer_id == trusted.issuer_id()
&& self.issuer_key_id == trusted.key_id()
&& self.issuer_public_key == trusted.public_key()
}
fn validate_structure(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
validate_identifier(&self.scope, limits.max_identifier_bytes)?;
validate_identifier(&self.issuer_id, limits.max_identifier_bytes)?;
if self.revision == 0
|| self.minimum_authorization_epoch == 0
|| self.minimum_role_key_epoch == 0
{
return Err(AccessError::InvalidEpoch);
}
if self.issuer_key_id != issuer_key_id(&self.issuer_public_key) {
return Err(AccessError::InvalidKeyIdentity);
}
Ok(())
}
fn verify_signature(&self) -> Result<(), AccessError> {
verify(
REVOCATION_SIGNATURE_DOMAIN,
&self.issuer_public_key,
&self.unsigned_bytes(),
&self.signature,
)
}
fn unsigned_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
push_header(&mut out, REVOCATION_TAG);
push_string(&mut out, &self.tenant_id);
push_string(&mut out, &self.subject_id);
push_string(&mut out, &self.scope);
out.extend_from_slice(&self.revision.to_be_bytes());
out.extend_from_slice(&self.minimum_authorization_epoch.to_be_bytes());
out.extend_from_slice(&self.minimum_role_key_epoch.to_be_bytes());
out.extend_from_slice(&self.issued_at.to_be_bytes());
push_string(&mut out, &self.issuer_id);
out.extend_from_slice(&self.issuer_key_id);
out.extend_from_slice(&self.issuer_public_key);
out
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VerifiedRevocation<'a> {
state: &'a RevocationState,
}
impl VerifiedRevocation<'_> {
#[cfg(feature = "client")]
pub(crate) fn accepts(
self,
trusted: &TrustedIssuer,
tenant_id: &str,
subject_id: &str,
scope: &str,
authorization_epoch: u64,
role_key_epoch: u64,
) -> Result<(), AccessError> {
if !self.state.issuer_matches(trusted)
|| self.state.tenant_id != tenant_id
|| self.state.subject_id != subject_id
|| self.state.scope != scope
{
return Err(AccessError::SubjectMismatch);
}
if authorization_epoch < self.state.minimum_authorization_epoch
|| role_key_epoch < self.state.minimum_role_key_epoch
{
return Err(AccessError::Revoked);
}
Ok(())
}
}