use core::ops::{BitOr, BitOrAssign};
#[cfg(feature = "client")]
use blindplane_core::{Recipient, RecipientKeypair, recipient_key_id};
#[cfg(feature = "client")]
use blindplane_crypto::StaticSecret;
#[cfg(feature = "client")]
use blindplane_crypto::aead::Suite;
#[cfg(feature = "client")]
use blindplane_crypto::hpke;
#[cfg(feature = "client")]
use blindplane_crypto::util::Secret;
use crate::AccessError;
use crate::codec::{
AccessValidationPolicy, Cursor, push_bytes, push_header, push_string, validate_identifier,
};
use crate::principal::issuer_key_id;
#[cfg(feature = "client")]
use crate::principal::{AccessIssuer, Principal, PrincipalKeypair, TrustedIssuer};
#[cfg(feature = "client")]
use crate::revocation::VerifiedRevocation;
#[cfg(feature = "client")]
use crate::signed::sign;
use crate::signed::verify;
const GRANT_TAG: u8 = 4;
const GRANT_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/grant/v1";
#[cfg(feature = "client")]
const GRANT_HPKE_INFO: &[u8] = b"blindplane/access/grant-hpke/v1";
#[cfg(feature = "client")]
const ROLE_BODY_DOMAIN: &[u8] = b"blindplane/access/role-key/v1";
const KNOWN_PERMISSION_BITS: u8 = 0b0000_1111;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Permissions(u8);
impl Permissions {
pub const NONE: Self = Self(0);
pub const READ: Self = Self(1 << 0);
pub const WRITE: Self = Self(1 << 1);
pub const OBSERVE: Self = Self(1 << 2);
pub const ADMINISTER: Self = Self(1 << 3);
pub const fn contains(self, requested: Self) -> bool {
self.0 & requested.0 == requested.0
}
const fn bits(self) -> u8 {
self.0
}
fn from_bits(bits: u8) -> Result<Self, AccessError> {
if bits == 0 || bits & !KNOWN_PERMISSION_BITS != 0 {
Err(AccessError::InvalidPermissions)
} else {
Ok(Self(bits))
}
}
}
impl BitOr for Permissions {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Permissions {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GrantSpec {
pub grant_id: String,
pub scope: String,
pub permissions: Permissions,
pub authorization_epoch: u64,
pub not_before: u64,
pub not_after: u64,
}
#[cfg(feature = "client")]
pub struct RoleKeypair {
tenant_id: String,
scope: String,
role_id: String,
key_epoch: u64,
secret: StaticSecret,
}
#[cfg(feature = "client")]
impl core::fmt::Debug for RoleKeypair {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RoleKeypair")
.field("tenant_id", &self.tenant_id)
.field("scope", &self.scope)
.field("role_id", &self.role_id)
.field("key_epoch", &self.key_epoch)
.finish_non_exhaustive()
}
}
#[cfg(feature = "client")]
impl PartialEq for RoleKeypair {
fn eq(&self, other: &Self) -> bool {
self.tenant_id == other.tenant_id
&& self.scope == other.scope
&& self.role_id == other.role_id
&& self.key_epoch == other.key_epoch
&& self.secret.to_bytes() == other.secret.to_bytes()
}
}
#[cfg(feature = "client")]
impl Eq for RoleKeypair {}
#[cfg(feature = "client")]
impl RoleKeypair {
pub fn generate(
tenant_id: impl Into<String>,
scope: impl Into<String>,
role_id: impl Into<String>,
key_epoch: u64,
) -> Result<Self, AccessError> {
let secret = StaticSecret::generate().map_err(|_| AccessError::CryptographicFailure)?;
Self::assemble(
tenant_id.into(),
scope.into(),
role_id.into(),
key_epoch,
secret,
)
}
pub fn from_secret_bytes(
tenant_id: impl Into<String>,
scope: impl Into<String>,
role_id: impl Into<String>,
key_epoch: u64,
secret: [u8; 32],
) -> Result<Self, AccessError> {
Self::assemble(
tenant_id.into(),
scope.into(),
role_id.into(),
key_epoch,
StaticSecret::from_bytes(secret),
)
}
fn assemble(
tenant_id: String,
scope: String,
role_id: String,
key_epoch: u64,
secret: StaticSecret,
) -> Result<Self, AccessError> {
let limit = AccessValidationPolicy::default().max_identifier_bytes;
validate_identifier(&tenant_id, limit)?;
validate_identifier(&scope, limit)?;
validate_identifier(&role_id, limit)?;
if key_epoch == 0 {
return Err(AccessError::InvalidEpoch);
}
Ok(Self {
tenant_id,
scope,
role_id,
key_epoch,
secret,
})
}
pub fn role_id(&self) -> &str {
&self.role_id
}
pub const fn key_epoch(&self) -> u64 {
self.key_epoch
}
pub const fn public_key(&self) -> [u8; 32] {
self.secret.public_key()
}
pub(crate) fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub(crate) fn scope(&self) -> &str {
&self.scope
}
pub(crate) fn secret_bytes(&self) -> Secret<32> {
Secret::new(self.secret.to_bytes())
}
pub(crate) fn recipient(&self) -> Result<Recipient, AccessError> {
let public_key = self.public_key();
Recipient::from_verified_key(
self.role_id.clone(),
self.key_epoch,
public_key,
recipient_key_id(&public_key),
)
.map_err(|_| AccessError::InvalidKeyIdentity)
}
pub(crate) fn recipient_keypair(&self) -> Result<RecipientKeypair, AccessError> {
RecipientKeypair::from_secret_bytes(
self.role_id.clone(),
self.key_epoch,
self.secret.to_bytes(),
)
.map_err(|_| AccessError::InvalidKeyIdentity)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccessGrant {
grant_id: String,
tenant_id: String,
subject_id: String,
subject_key_id: [u8; 32],
scope: String,
permissions: Permissions,
authorization_epoch: u64,
role_key_epoch: u64,
not_before: u64,
not_after: u64,
encapsulated_key: Vec<u8>,
wrapped_role_key: Vec<u8>,
issuer_id: String,
issuer_key_id: [u8; 32],
issuer_public_key: [u8; 32],
signature: [u8; 64],
}
impl AccessGrant {
#[cfg(feature = "client")]
pub fn issue(
issuer: &AccessIssuer,
subject: &Principal,
role: &RoleKeypair,
spec: GrantSpec,
) -> Result<Self, AccessError> {
let mut grant = Self {
grant_id: spec.grant_id,
tenant_id: subject.tenant_id().to_owned(),
subject_id: subject.principal_id().to_owned(),
subject_key_id: subject.key_id(),
scope: spec.scope,
permissions: spec.permissions,
authorization_epoch: spec.authorization_epoch,
role_key_epoch: role.key_epoch,
not_before: spec.not_before,
not_after: spec.not_after,
encapsulated_key: Vec::new(),
wrapped_role_key: Vec::new(),
issuer_id: issuer.issuer_id().to_owned(),
issuer_key_id: issuer.key_id(),
issuer_public_key: issuer.public_key(),
signature: [0; 64],
};
grant.validate_metadata(&AccessValidationPolicy::default())?;
if grant.tenant_id != role.tenant_id() || grant.scope != role.scope() {
return Err(AccessError::SubjectMismatch);
}
let role_body = encode_role_body(role);
let (encapsulated_key, wrapped_role_key) = hpke::seal(
Suite::ChaCha20Poly1305,
&subject.wrapping_public_key(),
GRANT_HPKE_INFO,
&grant.hpke_aad(),
&role_body,
)
.map_err(|_| AccessError::CryptographicFailure)?;
grant.encapsulated_key = encapsulated_key;
grant.wrapped_role_key = wrapped_role_key;
grant.validate_structure(&AccessValidationPolicy::default())?;
grant.signature = sign(
GRANT_SIGNATURE_DOMAIN,
issuer.signing_key(),
&grant.unsigned_bytes(),
);
Ok(grant)
}
pub fn grant_id(&self) -> &str {
&self.grant_id
}
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 permissions(&self) -> Permissions {
self.permissions
}
pub const fn authorization_epoch(&self) -> u64 {
self.authorization_epoch
}
pub const fn role_key_epoch(&self) -> u64 {
self.role_key_epoch
}
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(GRANT_TAG)?;
let grant = Self {
grant_id: cursor.take_string(limits.max_identifier_bytes)?,
tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
subject_id: cursor.take_string(limits.max_identifier_bytes)?,
subject_key_id: cursor.take_array32()?,
scope: cursor.take_string(limits.max_identifier_bytes)?,
permissions: Permissions::from_bits(cursor.take_u8()?)?,
authorization_epoch: cursor.take_u64()?,
role_key_epoch: cursor.take_u64()?,
not_before: cursor.take_u64()?,
not_after: cursor.take_u64()?,
issuer_id: cursor.take_string(limits.max_identifier_bytes)?,
issuer_key_id: cursor.take_array32()?,
issuer_public_key: cursor.take_array32()?,
encapsulated_key: cursor.take_bytes(32)?.to_vec(),
wrapped_role_key: cursor.take_bytes(limits.max_wrapped_grant_bytes)?.to_vec(),
signature: cursor.take_array64()?,
};
if !cursor.is_empty() {
return Err(AccessError::TrailingBytes);
}
grant.validate_structure(limits)?;
grant.verify_signature()?;
if grant.encode() != bytes {
return Err(AccessError::NonCanonicalEncoding);
}
Ok(grant)
}
#[cfg(feature = "client")]
pub fn open_role(
&self,
subject: &PrincipalKeypair,
trusted: &TrustedIssuer,
now: u64,
revocation: &VerifiedRevocation<'_>,
) -> Result<RoleKeypair, AccessError> {
self.verify_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);
}
let principal = subject.principal();
if self.tenant_id != principal.tenant_id()
|| self.subject_id != principal.principal_id()
|| self.subject_key_id != principal.key_id()
{
return Err(AccessError::SubjectMismatch);
}
if now < self.not_before {
return Err(AccessError::NotYetValid);
}
if now > self.not_after {
return Err(AccessError::Expired);
}
revocation.accepts(
trusted,
&self.tenant_id,
&self.subject_id,
&self.scope,
self.authorization_epoch,
self.role_key_epoch,
)?;
let plaintext = hpke::open(
Suite::ChaCha20Poly1305,
subject.wrapping_secret(),
&self.encapsulated_key,
GRANT_HPKE_INFO,
&self.hpke_aad(),
&self.wrapped_role_key,
)
.map_err(|_| AccessError::CryptographicFailure)?;
let role = decode_role_body(&plaintext, &AccessValidationPolicy::default())?;
if role.tenant_id != self.tenant_id
|| role.scope != self.scope
|| role.key_epoch != self.role_key_epoch
{
return Err(AccessError::CryptographicFailure);
}
Ok(role)
}
fn validate_metadata(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
validate_identifier(&self.grant_id, limits.max_identifier_bytes)?;
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)?;
Permissions::from_bits(self.permissions.bits())?;
if self.authorization_epoch == 0
|| self.role_key_epoch == 0
|| self.not_before > self.not_after
{
return Err(AccessError::InvalidEpoch);
}
if self.issuer_key_id != issuer_key_id(&self.issuer_public_key) {
return Err(AccessError::InvalidKeyIdentity);
}
Ok(())
}
fn validate_structure(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
self.validate_metadata(limits)?;
if self.encapsulated_key.len() != 32
|| self.wrapped_role_key.len() <= 16
|| self.wrapped_role_key.len() > limits.max_wrapped_grant_bytes
{
return Err(AccessError::LengthLimit(self.wrapped_role_key.len()));
}
Ok(())
}
fn verify_signature(&self) -> Result<(), AccessError> {
verify(
GRANT_SIGNATURE_DOMAIN,
&self.issuer_public_key,
&self.unsigned_bytes(),
&self.signature,
)
}
fn hpke_aad(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
push_string(&mut out, &self.grant_id);
push_string(&mut out, &self.tenant_id);
push_string(&mut out, &self.subject_id);
out.extend_from_slice(&self.subject_key_id);
push_string(&mut out, &self.scope);
out.push(self.permissions.bits());
out.extend_from_slice(&self.authorization_epoch.to_be_bytes());
out.extend_from_slice(&self.role_key_epoch.to_be_bytes());
out.extend_from_slice(&self.not_before.to_be_bytes());
out.extend_from_slice(&self.not_after.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
}
fn unsigned_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(384 + self.wrapped_role_key.len());
push_header(&mut out, GRANT_TAG);
out.extend_from_slice(&self.hpke_aad());
push_bytes(&mut out, &self.encapsulated_key);
push_bytes(&mut out, &self.wrapped_role_key);
out
}
}
#[cfg(feature = "client")]
fn encode_role_body(role: &RoleKeypair) -> Vec<u8> {
let secret = role.secret_bytes();
let mut out = Vec::with_capacity(160);
push_bytes(&mut out, ROLE_BODY_DOMAIN);
push_string(&mut out, &role.tenant_id);
push_string(&mut out, &role.scope);
push_string(&mut out, &role.role_id);
out.extend_from_slice(&role.key_epoch.to_be_bytes());
out.extend_from_slice(secret.as_bytes());
out
}
#[cfg(feature = "client")]
fn decode_role_body(
bytes: &[u8],
limits: &AccessValidationPolicy,
) -> Result<RoleKeypair, AccessError> {
let mut cursor = Cursor::new(bytes);
if cursor.take_bytes(ROLE_BODY_DOMAIN.len())? != ROLE_BODY_DOMAIN {
return Err(AccessError::CryptographicFailure);
}
let tenant_id = cursor.take_string(limits.max_identifier_bytes)?;
let scope = cursor.take_string(limits.max_identifier_bytes)?;
let role_id = cursor.take_string(limits.max_identifier_bytes)?;
let key_epoch = cursor.take_u64()?;
let secret = cursor.take_array32()?;
if !cursor.is_empty() {
return Err(AccessError::CryptographicFailure);
}
RoleKeypair::from_secret_bytes(tenant_id, scope, role_id, key_epoch, secret)
}