#[cfg(feature = "client")]
use blindplane_crypto::util::Secret;
use blindplane_crypto::{PreparedVerifier, Sha256};
#[cfg(feature = "client")]
use blindplane_crypto::{SigningKey, StaticSecret};
use crate::AccessError;
use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
use crate::signed::key_id;
const PRINCIPAL_TAG: u8 = 1;
const PRINCIPAL_KEY_ID_DOMAIN: &[u8] = b"blindplane/access/principal-key-id/v1";
const ISSUER_KEY_ID_DOMAIN: &[u8] = b"blindplane/access/issuer-key-id/v1";
pub(crate) fn issuer_key_id(public_key: &[u8; 32]) -> [u8; 32] {
key_id(ISSUER_KEY_ID_DOMAIN, public_key)
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PrincipalKind {
User,
Administrator,
Device,
Service,
}
impl PrincipalKind {
const fn code(self) -> u8 {
match self {
Self::User => 1,
Self::Administrator => 2,
Self::Device => 3,
Self::Service => 4,
}
}
fn from_code(code: u8) -> Result<Self, AccessError> {
match code {
1 => Ok(Self::User),
2 => Ok(Self::Administrator),
3 => Ok(Self::Device),
4 => Ok(Self::Service),
_ => Err(AccessError::WrongObjectType),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Principal {
tenant_id: String,
principal_id: String,
kind: PrincipalKind,
key_epoch: u64,
key_id: [u8; 32],
wrapping_public_key: [u8; 32],
signing_public_key: [u8; 32],
}
impl Principal {
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn principal_id(&self) -> &str {
&self.principal_id
}
pub const fn kind(&self) -> PrincipalKind {
self.kind
}
pub const fn key_epoch(&self) -> u64 {
self.key_epoch
}
pub const fn key_id(&self) -> [u8; 32] {
self.key_id
}
pub const fn wrapping_public_key(&self) -> [u8; 32] {
self.wrapping_public_key
}
pub const fn signing_public_key(&self) -> [u8; 32] {
self.signing_public_key
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(160 + self.tenant_id.len() + self.principal_id.len());
push_header(&mut out, PRINCIPAL_TAG);
push_string(&mut out, &self.tenant_id);
push_string(&mut out, &self.principal_id);
out.push(self.kind.code());
out.extend_from_slice(&self.key_epoch.to_be_bytes());
out.extend_from_slice(&self.key_id);
out.extend_from_slice(&self.wrapping_public_key);
out.extend_from_slice(&self.signing_public_key);
out
}
pub fn decode(bytes: &[u8], policy: &AccessValidationPolicy) -> Result<Self, AccessError> {
let mut cursor = Cursor::new(bytes);
cursor.take_header(PRINCIPAL_TAG)?;
let principal = Self {
tenant_id: cursor.take_string(policy.max_identifier_bytes)?,
principal_id: cursor.take_string(policy.max_identifier_bytes)?,
kind: PrincipalKind::from_code(cursor.take_u8()?)?,
key_epoch: cursor.take_u64()?,
key_id: cursor.take_array32()?,
wrapping_public_key: cursor.take_array32()?,
signing_public_key: cursor.take_array32()?,
};
if !cursor.is_empty() {
return Err(AccessError::TrailingBytes);
}
principal.validate(policy)?;
if principal.encode() != bytes {
return Err(AccessError::NonCanonicalEncoding);
}
Ok(principal)
}
fn validate(&self, policy: &AccessValidationPolicy) -> Result<(), AccessError> {
validate_identifier(&self.tenant_id, policy.max_identifier_bytes)?;
validate_identifier(&self.principal_id, policy.max_identifier_bytes)?;
if self.key_epoch == 0 {
return Err(AccessError::InvalidEpoch);
}
let expected = key_id(PRINCIPAL_KEY_ID_DOMAIN, &self.wrapping_public_key);
if expected != self.key_id
|| PreparedVerifier::new(&self.signing_public_key).is_err()
|| !usable_x25519_public_key(&self.wrapping_public_key)
{
return Err(AccessError::InvalidKeyIdentity);
}
Ok(())
}
}
fn usable_x25519_public_key(public_key: &[u8; 32]) -> bool {
let probe = blindplane_crypto::StaticSecret::from_bytes(Sha256::digest(
b"blindplane/access/public-key-validation/v1",
));
probe.diffie_hellman(public_key).is_some()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrustedIssuer {
issuer_id: String,
key_id: [u8; 32],
public_key: [u8; 32],
}
impl TrustedIssuer {
pub fn issuer_id(&self) -> &str {
&self.issuer_id
}
pub const fn key_id(&self) -> [u8; 32] {
self.key_id
}
pub const fn public_key(&self) -> [u8; 32] {
self.public_key
}
}
#[cfg(feature = "client")]
pub struct PrincipalKeypair {
principal: Principal,
wrapping_secret: StaticSecret,
#[allow(
dead_code,
reason = "consumed by the event implementation in a later task"
)]
signing_seed: Secret<32>,
}
#[cfg(feature = "client")]
impl core::fmt::Debug for PrincipalKeypair {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PrincipalKeypair")
.field("principal", &self.principal)
.field("secrets", &"redacted")
.finish_non_exhaustive()
}
}
#[cfg(feature = "client")]
impl PrincipalKeypair {
pub fn generate(
tenant_id: impl Into<String>,
principal_id: impl Into<String>,
kind: PrincipalKind,
key_epoch: u64,
) -> Result<Self, AccessError> {
let wrapping = StaticSecret::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
let signing = SigningKey::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
Self::assemble(
tenant_id.into(),
principal_id.into(),
kind,
key_epoch,
wrapping,
signing.to_seed(),
)
}
pub fn from_secret_bytes(
tenant_id: impl Into<String>,
principal_id: impl Into<String>,
kind: PrincipalKind,
key_epoch: u64,
wrapping_secret: [u8; 32],
signing_seed: [u8; 32],
) -> Result<Self, AccessError> {
Self::assemble(
tenant_id.into(),
principal_id.into(),
kind,
key_epoch,
StaticSecret::from_bytes(wrapping_secret),
signing_seed,
)
}
fn assemble(
tenant_id: String,
principal_id: String,
kind: PrincipalKind,
key_epoch: u64,
wrapping_secret: StaticSecret,
signing_seed: [u8; 32],
) -> Result<Self, AccessError> {
let signing = SigningKey::from_seed(&signing_seed);
let wrapping_public_key = wrapping_secret.public_key();
let principal = Principal {
tenant_id,
principal_id,
kind,
key_epoch,
key_id: key_id(PRINCIPAL_KEY_ID_DOMAIN, &wrapping_public_key),
wrapping_public_key,
signing_public_key: signing.verifying_key(),
};
principal.validate(&AccessValidationPolicy::default())?;
Ok(Self {
principal,
wrapping_secret,
signing_seed: Secret::new(signing_seed),
})
}
pub const fn principal(&self) -> &Principal {
&self.principal
}
pub(crate) const fn wrapping_secret(&self) -> &StaticSecret {
&self.wrapping_secret
}
#[allow(
dead_code,
reason = "consumed by the event implementation in a later task"
)]
pub(crate) fn signing_seed(&self) -> [u8; 32] {
self.signing_seed.expose()
}
}
#[cfg(feature = "client")]
pub struct AccessIssuer {
issuer_id: String,
key_id: [u8; 32],
signing_key: SigningKey,
}
#[cfg(feature = "client")]
impl core::fmt::Debug for AccessIssuer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AccessIssuer")
.field("issuer_id", &self.issuer_id)
.field("key_id", &self.key_id)
.field("signing_key", &"redacted")
.finish()
}
}
#[cfg(feature = "client")]
impl AccessIssuer {
pub fn generate(issuer_id: impl Into<String>) -> Result<Self, AccessError> {
let signing_key = SigningKey::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
Self::assemble(issuer_id.into(), signing_key)
}
pub fn from_seed(issuer_id: impl Into<String>, seed: [u8; 32]) -> Result<Self, AccessError> {
Self::assemble(issuer_id.into(), SigningKey::from_seed(&seed))
}
fn assemble(issuer_id: String, signing_key: SigningKey) -> Result<Self, AccessError> {
validate_identifier(
&issuer_id,
AccessValidationPolicy::default().max_identifier_bytes,
)?;
let public_key = signing_key.verifying_key();
Ok(Self {
issuer_id,
key_id: issuer_key_id(&public_key),
signing_key,
})
}
pub fn trusted(&self) -> TrustedIssuer {
TrustedIssuer {
issuer_id: self.issuer_id.clone(),
key_id: self.key_id,
public_key: self.public_key(),
}
}
pub const fn public_key(&self) -> [u8; 32] {
self.signing_key.verifying_key()
}
pub(crate) fn issuer_id(&self) -> &str {
&self.issuer_id
}
pub(crate) const fn key_id(&self) -> [u8; 32] {
self.key_id
}
pub(crate) const fn signing_key(&self) -> &SigningKey {
&self.signing_key
}
}