use blindplane_crypto::Sha256;
use crate::AccessError;
use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
use crate::principal::TrustedIssuer;
#[cfg(feature = "client")]
use crate::principal::{AccessIssuer, Principal};
#[cfg(feature = "client")]
use crate::signed::sign;
use crate::signed::verify;
const POLICY_TAG: u8 = 2;
const POLICY_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/policy/v1";
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum CapabilityKind {
Mcp,
Tool,
Skill,
Cli,
}
impl CapabilityKind {
const fn code(self) -> u8 {
match self {
Self::Mcp => 1,
Self::Tool => 2,
Self::Skill => 3,
Self::Cli => 4,
}
}
fn from_code(code: u8) -> Result<Self, AccessError> {
match code {
1 => Ok(Self::Mcp),
2 => Ok(Self::Tool),
3 => Ok(Self::Skill),
4 => Ok(Self::Cli),
_ => Err(AccessError::WrongObjectType),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Effect {
Allow,
Deny,
}
impl Effect {
const fn code(self) -> u8 {
match self {
Self::Allow => 1,
Self::Deny => 2,
}
}
fn from_code(code: u8) -> Result<Self, AccessError> {
match code {
1 => Ok(Self::Allow),
2 => Ok(Self::Deny),
_ => Err(AccessError::WrongObjectType),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Decision {
Allow,
Deny,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapabilityRule {
kind: CapabilityKind,
name: String,
effect: Effect,
}
impl CapabilityRule {
pub fn new(
kind: CapabilityKind,
name: impl Into<String>,
effect: Effect,
) -> Result<Self, AccessError> {
let name = name.into();
validate_identifier(
&name,
AccessValidationPolicy::default().max_identifier_bytes,
)?;
Ok(Self { kind, name, effect })
}
pub const fn kind(&self) -> CapabilityKind {
self.kind
}
pub fn name(&self) -> &str {
&self.name
}
pub const fn effect(&self) -> Effect {
self.effect
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicySpec {
pub revision: u64,
pub previous_hash: [u8; 32],
pub authorization_epoch: u64,
pub issued_at: u64,
pub not_before: u64,
pub not_after: u64,
pub rules: Vec<CapabilityRule>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TenantPolicy {
tenant_id: String,
subject_id: String,
subject_key_id: [u8; 32],
revision: u64,
previous_hash: [u8; 32],
authorization_epoch: u64,
issued_at: u64,
not_before: u64,
not_after: u64,
default_effect: Effect,
rules: Vec<CapabilityRule>,
issuer_id: String,
issuer_key_id: [u8; 32],
issuer_public_key: [u8; 32],
signature: [u8; 64],
}
impl TenantPolicy {
#[cfg(feature = "client")]
pub fn issue(
issuer: &AccessIssuer,
subject: &Principal,
mut spec: PolicySpec,
) -> Result<Self, AccessError> {
spec.rules.sort_by(|left, right| {
(left.kind, left.name.as_str(), left.effect).cmp(&(
right.kind,
right.name.as_str(),
right.effect,
))
});
let mut policy = Self {
tenant_id: subject.tenant_id().to_owned(),
subject_id: subject.principal_id().to_owned(),
subject_key_id: subject.key_id(),
revision: spec.revision,
previous_hash: spec.previous_hash,
authorization_epoch: spec.authorization_epoch,
issued_at: spec.issued_at,
not_before: spec.not_before,
not_after: spec.not_after,
default_effect: Effect::Deny,
rules: spec.rules,
issuer_id: issuer.issuer_id().to_owned(),
issuer_key_id: issuer.key_id(),
issuer_public_key: issuer.public_key(),
signature: [0; 64],
};
policy.validate_structure(&AccessValidationPolicy::default())?;
policy.signature = sign(
POLICY_SIGNATURE_DOMAIN,
issuer.signing_key(),
&policy.unsigned_bytes(),
);
Ok(policy)
}
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn subject_id(&self) -> &str {
&self.subject_id
}
pub const fn revision(&self) -> u64 {
self.revision
}
pub const fn authorization_epoch(&self) -> u64 {
self.authorization_epoch
}
pub fn encode(&self) -> Vec<u8> {
let mut out = self.unsigned_bytes();
out.extend_from_slice(&self.signature);
out
}
pub fn policy_hash(&self) -> [u8; 32] {
let encoded = self.encode();
let mut input = Vec::with_capacity(POLICY_SIGNATURE_DOMAIN.len() + encoded.len());
input.extend_from_slice(POLICY_SIGNATURE_DOMAIN);
input.extend_from_slice(&encoded);
Sha256::digest(&input)
}
pub fn decode(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
let mut cursor = Cursor::new(bytes);
cursor.take_header(POLICY_TAG)?;
let tenant_id = cursor.take_string(limits.max_identifier_bytes)?;
let subject_id = cursor.take_string(limits.max_identifier_bytes)?;
let subject_key_id = cursor.take_array32()?;
let revision = cursor.take_u64()?;
let previous_hash = cursor.take_array32()?;
let authorization_epoch = cursor.take_u64()?;
let issued_at = cursor.take_u64()?;
let not_before = cursor.take_u64()?;
let not_after = cursor.take_u64()?;
let default_effect = Effect::from_code(cursor.take_u8()?)?;
let rule_count = cursor.take_len(limits.max_rules)?;
let mut rules = Vec::with_capacity(rule_count);
for _ in 0..rule_count {
rules.push(CapabilityRule {
kind: CapabilityKind::from_code(cursor.take_u8()?)?,
name: cursor.take_string(limits.max_identifier_bytes)?,
effect: Effect::from_code(cursor.take_u8()?)?,
});
}
let issuer_id = cursor.take_string(limits.max_identifier_bytes)?;
let issuer_key_id = cursor.take_array32()?;
let issuer_public_key = cursor.take_array32()?;
let signature = cursor.take_array64()?;
if !cursor.is_empty() {
return Err(AccessError::TrailingBytes);
}
let policy = Self {
tenant_id,
subject_id,
subject_key_id,
revision,
previous_hash,
authorization_epoch,
issued_at,
not_before,
not_after,
default_effect,
rules,
issuer_id,
issuer_key_id,
issuer_public_key,
signature,
};
policy.validate_structure(limits)?;
verify(
POLICY_SIGNATURE_DOMAIN,
&policy.issuer_public_key,
&policy.unsigned_bytes(),
&policy.signature,
)?;
if policy.encode() != bytes {
return Err(AccessError::NonCanonicalEncoding);
}
Ok(policy)
}
pub fn verify<'a>(
&'a self,
trusted: &TrustedIssuer,
expected_tenant: &str,
expected_subject: &str,
now: u64,
minimum_revision: u64,
minimum_authorization_epoch: u64,
) -> Result<VerifiedPolicy<'a>, AccessError> {
verify(
POLICY_SIGNATURE_DOMAIN,
&self.issuer_public_key,
&self.unsigned_bytes(),
&self.signature,
)?;
if self.issuer_id != trusted.issuer_id()
|| self.issuer_key_id != trusted.key_id()
|| self.issuer_public_key != trusted.public_key()
{
return Err(AccessError::UntrustedIssuer);
}
if self.tenant_id != expected_tenant || self.subject_id != expected_subject {
return Err(AccessError::SubjectMismatch);
}
if now < self.not_before {
return Err(AccessError::NotYetValid);
}
if now > self.not_after {
return Err(AccessError::Expired);
}
if self.revision < minimum_revision {
return Err(AccessError::StaleRevision);
}
if self.authorization_epoch < minimum_authorization_epoch {
return Err(AccessError::Revoked);
}
Ok(VerifiedPolicy { policy: self })
}
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.issuer_id, limits.max_identifier_bytes)?;
if self.revision == 0
|| self.authorization_epoch == 0
|| self.not_before > self.not_after
|| self.issued_at > self.not_after
|| (self.revision == 1 && self.previous_hash != [0; 32])
|| (self.revision > 1 && self.previous_hash == [0; 32])
{
return Err(AccessError::InvalidEpoch);
}
if self.default_effect != Effect::Deny || self.rules.len() > limits.max_rules {
return Err(AccessError::LengthLimit(self.rules.len()));
}
for rule in &self.rules {
validate_identifier(&rule.name, limits.max_identifier_bytes)?;
}
for pair in self.rules.windows(2) {
let left = (pair[0].kind, pair[0].name.as_str());
let right = (pair[1].kind, pair[1].name.as_str());
if left >= right {
return Err(AccessError::DuplicateRule);
}
}
Ok(())
}
fn unsigned_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(256 + self.rules.len() * 32);
push_header(&mut out, POLICY_TAG);
push_string(&mut out, &self.tenant_id);
push_string(&mut out, &self.subject_id);
out.extend_from_slice(&self.subject_key_id);
out.extend_from_slice(&self.revision.to_be_bytes());
out.extend_from_slice(&self.previous_hash);
out.extend_from_slice(&self.authorization_epoch.to_be_bytes());
out.extend_from_slice(&self.issued_at.to_be_bytes());
out.extend_from_slice(&self.not_before.to_be_bytes());
out.extend_from_slice(&self.not_after.to_be_bytes());
out.push(self.default_effect.code());
out.extend_from_slice(
&u32::try_from(self.rules.len())
.expect("policy rules are bounded")
.to_be_bytes(),
);
for rule in &self.rules {
out.push(rule.kind.code());
push_string(&mut out, &rule.name);
out.push(rule.effect.code());
}
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 VerifiedPolicy<'a> {
policy: &'a TenantPolicy,
}
impl VerifiedPolicy<'_> {
pub fn decision(&self, kind: CapabilityKind, name: &str) -> Decision {
self.policy
.rules
.iter()
.find(|rule| rule.kind == kind && rule.name == name)
.map_or(Decision::Deny, |rule| match rule.effect {
Effect::Allow => Decision::Allow,
Effect::Deny => Decision::Deny,
})
}
}