Skip to main content

blindplane_access/
grant.rs

1//! Signed HPKE grants carrying role keys.
2
3use core::ops::{BitOr, BitOrAssign};
4
5#[cfg(feature = "client")]
6use blindplane_core::{Recipient, RecipientKeypair, recipient_key_id};
7#[cfg(feature = "client")]
8use blindplane_crypto::StaticSecret;
9#[cfg(feature = "client")]
10use blindplane_crypto::aead::Suite;
11#[cfg(feature = "client")]
12use blindplane_crypto::hpke;
13#[cfg(feature = "client")]
14use blindplane_crypto::util::Secret;
15
16use crate::AccessError;
17use crate::codec::{
18    AccessValidationPolicy, Cursor, push_bytes, push_header, push_string, validate_identifier,
19};
20use crate::principal::issuer_key_id;
21#[cfg(feature = "client")]
22use crate::principal::{AccessIssuer, Principal, PrincipalKeypair, TrustedIssuer};
23#[cfg(feature = "client")]
24use crate::revocation::VerifiedRevocation;
25#[cfg(feature = "client")]
26use crate::signed::sign;
27use crate::signed::verify;
28
29const GRANT_TAG: u8 = 4;
30const GRANT_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/grant/v1";
31#[cfg(feature = "client")]
32const GRANT_HPKE_INFO: &[u8] = b"blindplane/access/grant-hpke/v1";
33#[cfg(feature = "client")]
34const ROLE_BODY_DOMAIN: &[u8] = b"blindplane/access/role-key/v1";
35const KNOWN_PERMISSION_BITS: u8 = 0b0000_1111;
36
37/// Permissions carried by a role-key grant.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct Permissions(u8);
40
41impl Permissions {
42    /// No permissions. Invalid for an issued grant.
43    pub const NONE: Self = Self(0);
44    /// Read encrypted resources in the scope.
45    pub const READ: Self = Self(1 << 0);
46    /// Write encrypted resources in the scope.
47    pub const WRITE: Self = Self(1 << 1);
48    /// Observe audit events in the scope.
49    pub const OBSERVE: Self = Self(1 << 2);
50    /// Administer grants or policy in the scope.
51    pub const ADMINISTER: Self = Self(1 << 3);
52
53    /// Whether every requested permission is present.
54    pub const fn contains(self, requested: Self) -> bool {
55        self.0 & requested.0 == requested.0
56    }
57
58    const fn bits(self) -> u8 {
59        self.0
60    }
61
62    fn from_bits(bits: u8) -> Result<Self, AccessError> {
63        if bits == 0 || bits & !KNOWN_PERMISSION_BITS != 0 {
64            Err(AccessError::InvalidPermissions)
65        } else {
66            Ok(Self(bits))
67        }
68    }
69}
70
71impl BitOr for Permissions {
72    type Output = Self;
73
74    fn bitor(self, rhs: Self) -> Self::Output {
75        Self(self.0 | rhs.0)
76    }
77}
78
79impl BitOrAssign for Permissions {
80    fn bitor_assign(&mut self, rhs: Self) {
81        self.0 |= rhs.0;
82    }
83}
84
85/// Inputs signed into a role-key grant.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct GrantSpec {
88    /// Stable unique grant identifier.
89    pub grant_id: String,
90    /// Exact access scope.
91    pub scope: String,
92    /// Granted operations.
93    pub permissions: Permissions,
94    /// Current authorization epoch.
95    pub authorization_epoch: u64,
96    /// First valid Unix second.
97    pub not_before: u64,
98    /// Last valid Unix second.
99    pub not_after: u64,
100}
101
102/// Client-held X25519 role key used as a sealed-record recipient.
103#[cfg(feature = "client")]
104pub struct RoleKeypair {
105    tenant_id: String,
106    scope: String,
107    role_id: String,
108    key_epoch: u64,
109    secret: StaticSecret,
110}
111
112#[cfg(feature = "client")]
113impl core::fmt::Debug for RoleKeypair {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        f.debug_struct("RoleKeypair")
116            .field("tenant_id", &self.tenant_id)
117            .field("scope", &self.scope)
118            .field("role_id", &self.role_id)
119            .field("key_epoch", &self.key_epoch)
120            .finish_non_exhaustive()
121    }
122}
123
124#[cfg(feature = "client")]
125impl PartialEq for RoleKeypair {
126    fn eq(&self, other: &Self) -> bool {
127        self.tenant_id == other.tenant_id
128            && self.scope == other.scope
129            && self.role_id == other.role_id
130            && self.key_epoch == other.key_epoch
131            && self.secret.to_bytes() == other.secret.to_bytes()
132    }
133}
134
135#[cfg(feature = "client")]
136impl Eq for RoleKeypair {}
137
138#[cfg(feature = "client")]
139impl RoleKeypair {
140    /// Generate a role key from operating-system entropy.
141    pub fn generate(
142        tenant_id: impl Into<String>,
143        scope: impl Into<String>,
144        role_id: impl Into<String>,
145        key_epoch: u64,
146    ) -> Result<Self, AccessError> {
147        let secret = StaticSecret::generate().map_err(|_| AccessError::CryptographicFailure)?;
148        Self::assemble(
149            tenant_id.into(),
150            scope.into(),
151            role_id.into(),
152            key_epoch,
153            secret,
154        )
155    }
156
157    /// Restore a role key from 32 secret bytes.
158    pub fn from_secret_bytes(
159        tenant_id: impl Into<String>,
160        scope: impl Into<String>,
161        role_id: impl Into<String>,
162        key_epoch: u64,
163        secret: [u8; 32],
164    ) -> Result<Self, AccessError> {
165        Self::assemble(
166            tenant_id.into(),
167            scope.into(),
168            role_id.into(),
169            key_epoch,
170            StaticSecret::from_bytes(secret),
171        )
172    }
173
174    fn assemble(
175        tenant_id: String,
176        scope: String,
177        role_id: String,
178        key_epoch: u64,
179        secret: StaticSecret,
180    ) -> Result<Self, AccessError> {
181        let limit = AccessValidationPolicy::default().max_identifier_bytes;
182        validate_identifier(&tenant_id, limit)?;
183        validate_identifier(&scope, limit)?;
184        validate_identifier(&role_id, limit)?;
185        if key_epoch == 0 {
186            return Err(AccessError::InvalidEpoch);
187        }
188        Ok(Self {
189            tenant_id,
190            scope,
191            role_id,
192            key_epoch,
193            secret,
194        })
195    }
196
197    /// Stable role identifier used for recipient selection.
198    pub fn role_id(&self) -> &str {
199        &self.role_id
200    }
201
202    /// Current role-key epoch.
203    pub const fn key_epoch(&self) -> u64 {
204        self.key_epoch
205    }
206
207    /// X25519 public key for encrypted records.
208    pub const fn public_key(&self) -> [u8; 32] {
209        self.secret.public_key()
210    }
211
212    pub(crate) fn tenant_id(&self) -> &str {
213        &self.tenant_id
214    }
215
216    pub(crate) fn scope(&self) -> &str {
217        &self.scope
218    }
219
220    pub(crate) fn secret_bytes(&self) -> Secret<32> {
221        Secret::new(self.secret.to_bytes())
222    }
223
224    pub(crate) fn recipient(&self) -> Result<Recipient, AccessError> {
225        let public_key = self.public_key();
226        Recipient::from_verified_key(
227            self.role_id.clone(),
228            self.key_epoch,
229            public_key,
230            recipient_key_id(&public_key),
231        )
232        .map_err(|_| AccessError::InvalidKeyIdentity)
233    }
234
235    pub(crate) fn recipient_keypair(&self) -> Result<RecipientKeypair, AccessError> {
236        RecipientKeypair::from_secret_bytes(
237            self.role_id.clone(),
238            self.key_epoch,
239            self.secret.to_bytes(),
240        )
241        .map_err(|_| AccessError::InvalidKeyIdentity)
242    }
243}
244
245/// Canonical signed grant carrying one HPKE-wrapped role secret.
246#[derive(Clone, Debug, Eq, PartialEq)]
247pub struct AccessGrant {
248    grant_id: String,
249    tenant_id: String,
250    subject_id: String,
251    subject_key_id: [u8; 32],
252    scope: String,
253    permissions: Permissions,
254    authorization_epoch: u64,
255    role_key_epoch: u64,
256    not_before: u64,
257    not_after: u64,
258    encapsulated_key: Vec<u8>,
259    wrapped_role_key: Vec<u8>,
260    issuer_id: String,
261    issuer_key_id: [u8; 32],
262    issuer_public_key: [u8; 32],
263    signature: [u8; 64],
264}
265
266impl AccessGrant {
267    /// Issue a signed HPKE role-key grant to a pinned principal key.
268    #[cfg(feature = "client")]
269    pub fn issue(
270        issuer: &AccessIssuer,
271        subject: &Principal,
272        role: &RoleKeypair,
273        spec: GrantSpec,
274    ) -> Result<Self, AccessError> {
275        let mut grant = Self {
276            grant_id: spec.grant_id,
277            tenant_id: subject.tenant_id().to_owned(),
278            subject_id: subject.principal_id().to_owned(),
279            subject_key_id: subject.key_id(),
280            scope: spec.scope,
281            permissions: spec.permissions,
282            authorization_epoch: spec.authorization_epoch,
283            role_key_epoch: role.key_epoch,
284            not_before: spec.not_before,
285            not_after: spec.not_after,
286            encapsulated_key: Vec::new(),
287            wrapped_role_key: Vec::new(),
288            issuer_id: issuer.issuer_id().to_owned(),
289            issuer_key_id: issuer.key_id(),
290            issuer_public_key: issuer.public_key(),
291            signature: [0; 64],
292        };
293        grant.validate_metadata(&AccessValidationPolicy::default())?;
294        if grant.tenant_id != role.tenant_id() || grant.scope != role.scope() {
295            return Err(AccessError::SubjectMismatch);
296        }
297        let role_body = encode_role_body(role);
298        let (encapsulated_key, wrapped_role_key) = hpke::seal(
299            Suite::ChaCha20Poly1305,
300            &subject.wrapping_public_key(),
301            GRANT_HPKE_INFO,
302            &grant.hpke_aad(),
303            &role_body,
304        )
305        .map_err(|_| AccessError::CryptographicFailure)?;
306        grant.encapsulated_key = encapsulated_key;
307        grant.wrapped_role_key = wrapped_role_key;
308        grant.validate_structure(&AccessValidationPolicy::default())?;
309        grant.signature = sign(
310            GRANT_SIGNATURE_DOMAIN,
311            issuer.signing_key(),
312            &grant.unsigned_bytes(),
313        );
314        Ok(grant)
315    }
316
317    /// Stable grant identifier.
318    pub fn grant_id(&self) -> &str {
319        &self.grant_id
320    }
321
322    /// Tenant identifier authenticated by the signature.
323    pub fn tenant_id(&self) -> &str {
324        &self.tenant_id
325    }
326
327    /// Subject identifier authenticated by the signature.
328    pub fn subject_id(&self) -> &str {
329        &self.subject_id
330    }
331
332    /// Exact grant scope.
333    pub fn scope(&self) -> &str {
334        &self.scope
335    }
336
337    /// Granted operations.
338    pub const fn permissions(&self) -> Permissions {
339        self.permissions
340    }
341
342    /// Authorization epoch.
343    pub const fn authorization_epoch(&self) -> u64 {
344        self.authorization_epoch
345    }
346
347    /// Wrapped role-key epoch.
348    pub const fn role_key_epoch(&self) -> u64 {
349        self.role_key_epoch
350    }
351
352    /// Canonical encoded and signed grant.
353    pub fn encode(&self) -> Vec<u8> {
354        let mut out = self.unsigned_bytes();
355        out.extend_from_slice(&self.signature);
356        out
357    }
358
359    /// Decode, structurally validate, and verify signature math.
360    pub fn decode(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
361        let mut cursor = Cursor::new(bytes);
362        cursor.take_header(GRANT_TAG)?;
363        let grant = Self {
364            grant_id: cursor.take_string(limits.max_identifier_bytes)?,
365            tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
366            subject_id: cursor.take_string(limits.max_identifier_bytes)?,
367            subject_key_id: cursor.take_array32()?,
368            scope: cursor.take_string(limits.max_identifier_bytes)?,
369            permissions: Permissions::from_bits(cursor.take_u8()?)?,
370            authorization_epoch: cursor.take_u64()?,
371            role_key_epoch: cursor.take_u64()?,
372            not_before: cursor.take_u64()?,
373            not_after: cursor.take_u64()?,
374            issuer_id: cursor.take_string(limits.max_identifier_bytes)?,
375            issuer_key_id: cursor.take_array32()?,
376            issuer_public_key: cursor.take_array32()?,
377            encapsulated_key: cursor.take_bytes(32)?.to_vec(),
378            wrapped_role_key: cursor.take_bytes(limits.max_wrapped_grant_bytes)?.to_vec(),
379            signature: cursor.take_array64()?,
380        };
381        if !cursor.is_empty() {
382            return Err(AccessError::TrailingBytes);
383        }
384        grant.validate_structure(limits)?;
385        grant.verify_signature()?;
386        if grant.encode() != bytes {
387            return Err(AccessError::NonCanonicalEncoding);
388        }
389        Ok(grant)
390    }
391
392    /// Verify and open the role only for the named principal and current epochs.
393    #[cfg(feature = "client")]
394    pub fn open_role(
395        &self,
396        subject: &PrincipalKeypair,
397        trusted: &TrustedIssuer,
398        now: u64,
399        revocation: &VerifiedRevocation<'_>,
400    ) -> Result<RoleKeypair, AccessError> {
401        self.verify_signature()?;
402        if self.issuer_id != trusted.issuer_id()
403            || self.issuer_key_id != trusted.key_id()
404            || self.issuer_public_key != trusted.public_key()
405        {
406            return Err(AccessError::UntrustedIssuer);
407        }
408        let principal = subject.principal();
409        if self.tenant_id != principal.tenant_id()
410            || self.subject_id != principal.principal_id()
411            || self.subject_key_id != principal.key_id()
412        {
413            return Err(AccessError::SubjectMismatch);
414        }
415        if now < self.not_before {
416            return Err(AccessError::NotYetValid);
417        }
418        if now > self.not_after {
419            return Err(AccessError::Expired);
420        }
421        revocation.accepts(
422            trusted,
423            &self.tenant_id,
424            &self.subject_id,
425            &self.scope,
426            self.authorization_epoch,
427            self.role_key_epoch,
428        )?;
429        let plaintext = hpke::open(
430            Suite::ChaCha20Poly1305,
431            subject.wrapping_secret(),
432            &self.encapsulated_key,
433            GRANT_HPKE_INFO,
434            &self.hpke_aad(),
435            &self.wrapped_role_key,
436        )
437        .map_err(|_| AccessError::CryptographicFailure)?;
438        let role = decode_role_body(&plaintext, &AccessValidationPolicy::default())?;
439        if role.tenant_id != self.tenant_id
440            || role.scope != self.scope
441            || role.key_epoch != self.role_key_epoch
442        {
443            return Err(AccessError::CryptographicFailure);
444        }
445        Ok(role)
446    }
447
448    fn validate_metadata(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
449        validate_identifier(&self.grant_id, limits.max_identifier_bytes)?;
450        validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
451        validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
452        validate_identifier(&self.scope, limits.max_identifier_bytes)?;
453        validate_identifier(&self.issuer_id, limits.max_identifier_bytes)?;
454        Permissions::from_bits(self.permissions.bits())?;
455        if self.authorization_epoch == 0
456            || self.role_key_epoch == 0
457            || self.not_before > self.not_after
458        {
459            return Err(AccessError::InvalidEpoch);
460        }
461        if self.issuer_key_id != issuer_key_id(&self.issuer_public_key) {
462            return Err(AccessError::InvalidKeyIdentity);
463        }
464        Ok(())
465    }
466
467    fn validate_structure(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
468        self.validate_metadata(limits)?;
469        if self.encapsulated_key.len() != 32
470            || self.wrapped_role_key.len() <= 16
471            || self.wrapped_role_key.len() > limits.max_wrapped_grant_bytes
472        {
473            return Err(AccessError::LengthLimit(self.wrapped_role_key.len()));
474        }
475        Ok(())
476    }
477
478    fn verify_signature(&self) -> Result<(), AccessError> {
479        verify(
480            GRANT_SIGNATURE_DOMAIN,
481            &self.issuer_public_key,
482            &self.unsigned_bytes(),
483            &self.signature,
484        )
485    }
486
487    fn hpke_aad(&self) -> Vec<u8> {
488        let mut out = Vec::with_capacity(256);
489        push_string(&mut out, &self.grant_id);
490        push_string(&mut out, &self.tenant_id);
491        push_string(&mut out, &self.subject_id);
492        out.extend_from_slice(&self.subject_key_id);
493        push_string(&mut out, &self.scope);
494        out.push(self.permissions.bits());
495        out.extend_from_slice(&self.authorization_epoch.to_be_bytes());
496        out.extend_from_slice(&self.role_key_epoch.to_be_bytes());
497        out.extend_from_slice(&self.not_before.to_be_bytes());
498        out.extend_from_slice(&self.not_after.to_be_bytes());
499        push_string(&mut out, &self.issuer_id);
500        out.extend_from_slice(&self.issuer_key_id);
501        out.extend_from_slice(&self.issuer_public_key);
502        out
503    }
504
505    fn unsigned_bytes(&self) -> Vec<u8> {
506        let mut out = Vec::with_capacity(384 + self.wrapped_role_key.len());
507        push_header(&mut out, GRANT_TAG);
508        out.extend_from_slice(&self.hpke_aad());
509        push_bytes(&mut out, &self.encapsulated_key);
510        push_bytes(&mut out, &self.wrapped_role_key);
511        out
512    }
513}
514
515#[cfg(feature = "client")]
516fn encode_role_body(role: &RoleKeypair) -> Vec<u8> {
517    let secret = role.secret_bytes();
518    let mut out = Vec::with_capacity(160);
519    push_bytes(&mut out, ROLE_BODY_DOMAIN);
520    push_string(&mut out, &role.tenant_id);
521    push_string(&mut out, &role.scope);
522    push_string(&mut out, &role.role_id);
523    out.extend_from_slice(&role.key_epoch.to_be_bytes());
524    out.extend_from_slice(secret.as_bytes());
525    out
526}
527
528#[cfg(feature = "client")]
529fn decode_role_body(
530    bytes: &[u8],
531    limits: &AccessValidationPolicy,
532) -> Result<RoleKeypair, AccessError> {
533    let mut cursor = Cursor::new(bytes);
534    if cursor.take_bytes(ROLE_BODY_DOMAIN.len())? != ROLE_BODY_DOMAIN {
535        return Err(AccessError::CryptographicFailure);
536    }
537    let tenant_id = cursor.take_string(limits.max_identifier_bytes)?;
538    let scope = cursor.take_string(limits.max_identifier_bytes)?;
539    let role_id = cursor.take_string(limits.max_identifier_bytes)?;
540    let key_epoch = cursor.take_u64()?;
541    let secret = cursor.take_array32()?;
542    if !cursor.is_empty() {
543        return Err(AccessError::CryptographicFailure);
544    }
545    RoleKeypair::from_secret_bytes(tenant_id, scope, role_id, key_epoch, secret)
546}