blindplane-access 0.1.0

Signed enterprise access grants, capability policies, revocation and encrypted audit events for Blindplane
Documentation
//! Principal and issuer identities.

#[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)
}

/// The endpoint represented by a principal descriptor.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PrincipalKind {
    /// A human tenant member.
    User,
    /// A tenant administrator.
    Administrator,
    /// A managed endpoint device.
    Device,
    /// A non-human service identity.
    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),
        }
    }
}

/// Public identity and key descriptor for an access subject.
#[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 {
    /// Tenant isolation boundary.
    pub fn tenant_id(&self) -> &str {
        &self.tenant_id
    }

    /// Stable subject identifier.
    pub fn principal_id(&self) -> &str {
        &self.principal_id
    }

    /// Principal category.
    pub const fn kind(&self) -> PrincipalKind {
        self.kind
    }

    /// Current public-key epoch.
    pub const fn key_epoch(&self) -> u64 {
        self.key_epoch
    }

    /// Domain-separated wrapping-key fingerprint.
    pub const fn key_id(&self) -> [u8; 32] {
        self.key_id
    }

    /// X25519 public key used for role-key grants.
    pub const fn wrapping_public_key(&self) -> [u8; 32] {
        self.wrapping_public_key
    }

    /// Ed25519 public key used for authored access events.
    pub const fn signing_public_key(&self) -> [u8; 32] {
        self.signing_public_key
    }

    /// Canonical binary encoding.
    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
    }

    /// Decode and validate a canonical principal.
    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()
}

/// A pinned issuer descriptor supplied through an authenticated channel.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrustedIssuer {
    issuer_id: String,
    key_id: [u8; 32],
    public_key: [u8; 32],
}

impl TrustedIssuer {
    /// Stable issuer identifier.
    pub fn issuer_id(&self) -> &str {
        &self.issuer_id
    }

    /// Domain-separated issuer-key fingerprint.
    pub const fn key_id(&self) -> [u8; 32] {
        self.key_id
    }

    /// Pinned Ed25519 public key.
    pub const fn public_key(&self) -> [u8; 32] {
        self.public_key
    }
}

/// Client-held encryption and signing keys for one principal.
#[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 {
    /// Generate independent X25519 and Ed25519 keys using operating-system entropy.
    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(),
        )
    }

    /// Restore deterministic client keys from two 32-byte secrets.
    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),
        })
    }

    /// Public principal descriptor.
    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()
    }
}

/// Client-held tenant issuer signing key.
#[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 {
    /// Generate a tenant issuer key from operating-system entropy.
    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)
    }

    /// Restore a tenant issuer from a 32-byte Ed25519 seed.
    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,
        })
    }

    /// Pinned public descriptor for clients.
    pub fn trusted(&self) -> TrustedIssuer {
        TrustedIssuer {
            issuer_id: self.issuer_id.clone(),
            key_id: self.key_id,
            public_key: self.public_key(),
        }
    }

    /// Ed25519 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
    }
}