Skip to main content

blindplane_access/
principal.rs

1//! Principal and issuer identities.
2
3#[cfg(feature = "client")]
4use blindplane_crypto::util::Secret;
5use blindplane_crypto::{PreparedVerifier, Sha256};
6#[cfg(feature = "client")]
7use blindplane_crypto::{SigningKey, StaticSecret};
8
9use crate::AccessError;
10use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
11use crate::signed::key_id;
12
13const PRINCIPAL_TAG: u8 = 1;
14const PRINCIPAL_KEY_ID_DOMAIN: &[u8] = b"blindplane/access/principal-key-id/v1";
15const ISSUER_KEY_ID_DOMAIN: &[u8] = b"blindplane/access/issuer-key-id/v1";
16
17pub(crate) fn issuer_key_id(public_key: &[u8; 32]) -> [u8; 32] {
18    key_id(ISSUER_KEY_ID_DOMAIN, public_key)
19}
20
21/// The endpoint represented by a principal descriptor.
22#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
23pub enum PrincipalKind {
24    /// A human tenant member.
25    User,
26    /// A tenant administrator.
27    Administrator,
28    /// A managed endpoint device.
29    Device,
30    /// A non-human service identity.
31    Service,
32}
33
34impl PrincipalKind {
35    const fn code(self) -> u8 {
36        match self {
37            Self::User => 1,
38            Self::Administrator => 2,
39            Self::Device => 3,
40            Self::Service => 4,
41        }
42    }
43
44    fn from_code(code: u8) -> Result<Self, AccessError> {
45        match code {
46            1 => Ok(Self::User),
47            2 => Ok(Self::Administrator),
48            3 => Ok(Self::Device),
49            4 => Ok(Self::Service),
50            _ => Err(AccessError::WrongObjectType),
51        }
52    }
53}
54
55/// Public identity and key descriptor for an access subject.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct Principal {
58    tenant_id: String,
59    principal_id: String,
60    kind: PrincipalKind,
61    key_epoch: u64,
62    key_id: [u8; 32],
63    wrapping_public_key: [u8; 32],
64    signing_public_key: [u8; 32],
65}
66
67impl Principal {
68    /// Tenant isolation boundary.
69    pub fn tenant_id(&self) -> &str {
70        &self.tenant_id
71    }
72
73    /// Stable subject identifier.
74    pub fn principal_id(&self) -> &str {
75        &self.principal_id
76    }
77
78    /// Principal category.
79    pub const fn kind(&self) -> PrincipalKind {
80        self.kind
81    }
82
83    /// Current public-key epoch.
84    pub const fn key_epoch(&self) -> u64 {
85        self.key_epoch
86    }
87
88    /// Domain-separated wrapping-key fingerprint.
89    pub const fn key_id(&self) -> [u8; 32] {
90        self.key_id
91    }
92
93    /// X25519 public key used for role-key grants.
94    pub const fn wrapping_public_key(&self) -> [u8; 32] {
95        self.wrapping_public_key
96    }
97
98    /// Ed25519 public key used for authored access events.
99    pub const fn signing_public_key(&self) -> [u8; 32] {
100        self.signing_public_key
101    }
102
103    /// Canonical binary encoding.
104    pub fn encode(&self) -> Vec<u8> {
105        let mut out = Vec::with_capacity(160 + self.tenant_id.len() + self.principal_id.len());
106        push_header(&mut out, PRINCIPAL_TAG);
107        push_string(&mut out, &self.tenant_id);
108        push_string(&mut out, &self.principal_id);
109        out.push(self.kind.code());
110        out.extend_from_slice(&self.key_epoch.to_be_bytes());
111        out.extend_from_slice(&self.key_id);
112        out.extend_from_slice(&self.wrapping_public_key);
113        out.extend_from_slice(&self.signing_public_key);
114        out
115    }
116
117    /// Decode and validate a canonical principal.
118    pub fn decode(bytes: &[u8], policy: &AccessValidationPolicy) -> Result<Self, AccessError> {
119        let mut cursor = Cursor::new(bytes);
120        cursor.take_header(PRINCIPAL_TAG)?;
121        let principal = Self {
122            tenant_id: cursor.take_string(policy.max_identifier_bytes)?,
123            principal_id: cursor.take_string(policy.max_identifier_bytes)?,
124            kind: PrincipalKind::from_code(cursor.take_u8()?)?,
125            key_epoch: cursor.take_u64()?,
126            key_id: cursor.take_array32()?,
127            wrapping_public_key: cursor.take_array32()?,
128            signing_public_key: cursor.take_array32()?,
129        };
130        if !cursor.is_empty() {
131            return Err(AccessError::TrailingBytes);
132        }
133        principal.validate(policy)?;
134        if principal.encode() != bytes {
135            return Err(AccessError::NonCanonicalEncoding);
136        }
137        Ok(principal)
138    }
139
140    fn validate(&self, policy: &AccessValidationPolicy) -> Result<(), AccessError> {
141        validate_identifier(&self.tenant_id, policy.max_identifier_bytes)?;
142        validate_identifier(&self.principal_id, policy.max_identifier_bytes)?;
143        if self.key_epoch == 0 {
144            return Err(AccessError::InvalidEpoch);
145        }
146        let expected = key_id(PRINCIPAL_KEY_ID_DOMAIN, &self.wrapping_public_key);
147        if expected != self.key_id
148            || PreparedVerifier::new(&self.signing_public_key).is_err()
149            || !usable_x25519_public_key(&self.wrapping_public_key)
150        {
151            return Err(AccessError::InvalidKeyIdentity);
152        }
153        Ok(())
154    }
155}
156
157fn usable_x25519_public_key(public_key: &[u8; 32]) -> bool {
158    let probe = blindplane_crypto::StaticSecret::from_bytes(Sha256::digest(
159        b"blindplane/access/public-key-validation/v1",
160    ));
161    probe.diffie_hellman(public_key).is_some()
162}
163
164/// A pinned issuer descriptor supplied through an authenticated channel.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct TrustedIssuer {
167    issuer_id: String,
168    key_id: [u8; 32],
169    public_key: [u8; 32],
170}
171
172impl TrustedIssuer {
173    /// Stable issuer identifier.
174    pub fn issuer_id(&self) -> &str {
175        &self.issuer_id
176    }
177
178    /// Domain-separated issuer-key fingerprint.
179    pub const fn key_id(&self) -> [u8; 32] {
180        self.key_id
181    }
182
183    /// Pinned Ed25519 public key.
184    pub const fn public_key(&self) -> [u8; 32] {
185        self.public_key
186    }
187}
188
189/// Client-held encryption and signing keys for one principal.
190#[cfg(feature = "client")]
191pub struct PrincipalKeypair {
192    principal: Principal,
193    wrapping_secret: StaticSecret,
194    #[allow(
195        dead_code,
196        reason = "consumed by the event implementation in a later task"
197    )]
198    signing_seed: Secret<32>,
199}
200
201#[cfg(feature = "client")]
202impl core::fmt::Debug for PrincipalKeypair {
203    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
204        f.debug_struct("PrincipalKeypair")
205            .field("principal", &self.principal)
206            .field("secrets", &"redacted")
207            .finish_non_exhaustive()
208    }
209}
210
211#[cfg(feature = "client")]
212impl PrincipalKeypair {
213    /// Generate independent X25519 and Ed25519 keys using operating-system entropy.
214    pub fn generate(
215        tenant_id: impl Into<String>,
216        principal_id: impl Into<String>,
217        kind: PrincipalKind,
218        key_epoch: u64,
219    ) -> Result<Self, AccessError> {
220        let wrapping = StaticSecret::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
221        let signing = SigningKey::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
222        Self::assemble(
223            tenant_id.into(),
224            principal_id.into(),
225            kind,
226            key_epoch,
227            wrapping,
228            signing.to_seed(),
229        )
230    }
231
232    /// Restore deterministic client keys from two 32-byte secrets.
233    pub fn from_secret_bytes(
234        tenant_id: impl Into<String>,
235        principal_id: impl Into<String>,
236        kind: PrincipalKind,
237        key_epoch: u64,
238        wrapping_secret: [u8; 32],
239        signing_seed: [u8; 32],
240    ) -> Result<Self, AccessError> {
241        Self::assemble(
242            tenant_id.into(),
243            principal_id.into(),
244            kind,
245            key_epoch,
246            StaticSecret::from_bytes(wrapping_secret),
247            signing_seed,
248        )
249    }
250
251    fn assemble(
252        tenant_id: String,
253        principal_id: String,
254        kind: PrincipalKind,
255        key_epoch: u64,
256        wrapping_secret: StaticSecret,
257        signing_seed: [u8; 32],
258    ) -> Result<Self, AccessError> {
259        let signing = SigningKey::from_seed(&signing_seed);
260        let wrapping_public_key = wrapping_secret.public_key();
261        let principal = Principal {
262            tenant_id,
263            principal_id,
264            kind,
265            key_epoch,
266            key_id: key_id(PRINCIPAL_KEY_ID_DOMAIN, &wrapping_public_key),
267            wrapping_public_key,
268            signing_public_key: signing.verifying_key(),
269        };
270        principal.validate(&AccessValidationPolicy::default())?;
271        Ok(Self {
272            principal,
273            wrapping_secret,
274            signing_seed: Secret::new(signing_seed),
275        })
276    }
277
278    /// Public principal descriptor.
279    pub const fn principal(&self) -> &Principal {
280        &self.principal
281    }
282
283    pub(crate) const fn wrapping_secret(&self) -> &StaticSecret {
284        &self.wrapping_secret
285    }
286
287    #[allow(
288        dead_code,
289        reason = "consumed by the event implementation in a later task"
290    )]
291    pub(crate) fn signing_seed(&self) -> [u8; 32] {
292        self.signing_seed.expose()
293    }
294}
295
296/// Client-held tenant issuer signing key.
297#[cfg(feature = "client")]
298pub struct AccessIssuer {
299    issuer_id: String,
300    key_id: [u8; 32],
301    signing_key: SigningKey,
302}
303
304#[cfg(feature = "client")]
305impl core::fmt::Debug for AccessIssuer {
306    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307        f.debug_struct("AccessIssuer")
308            .field("issuer_id", &self.issuer_id)
309            .field("key_id", &self.key_id)
310            .field("signing_key", &"redacted")
311            .finish()
312    }
313}
314
315#[cfg(feature = "client")]
316impl AccessIssuer {
317    /// Generate a tenant issuer key from operating-system entropy.
318    pub fn generate(issuer_id: impl Into<String>) -> Result<Self, AccessError> {
319        let signing_key = SigningKey::generate().map_err(|_| AccessError::InvalidKeyIdentity)?;
320        Self::assemble(issuer_id.into(), signing_key)
321    }
322
323    /// Restore a tenant issuer from a 32-byte Ed25519 seed.
324    pub fn from_seed(issuer_id: impl Into<String>, seed: [u8; 32]) -> Result<Self, AccessError> {
325        Self::assemble(issuer_id.into(), SigningKey::from_seed(&seed))
326    }
327
328    fn assemble(issuer_id: String, signing_key: SigningKey) -> Result<Self, AccessError> {
329        validate_identifier(
330            &issuer_id,
331            AccessValidationPolicy::default().max_identifier_bytes,
332        )?;
333        let public_key = signing_key.verifying_key();
334        Ok(Self {
335            issuer_id,
336            key_id: issuer_key_id(&public_key),
337            signing_key,
338        })
339    }
340
341    /// Pinned public descriptor for clients.
342    pub fn trusted(&self) -> TrustedIssuer {
343        TrustedIssuer {
344            issuer_id: self.issuer_id.clone(),
345            key_id: self.key_id,
346            public_key: self.public_key(),
347        }
348    }
349
350    /// Ed25519 public key.
351    pub const fn public_key(&self) -> [u8; 32] {
352        self.signing_key.verifying_key()
353    }
354
355    pub(crate) fn issuer_id(&self) -> &str {
356        &self.issuer_id
357    }
358
359    pub(crate) const fn key_id(&self) -> [u8; 32] {
360        self.key_id
361    }
362
363    pub(crate) const fn signing_key(&self) -> &SigningKey {
364        &self.signing_key
365    }
366}