Skip to main content

blindplane_access/
revocation.rs

1//! Signed per-subject and scope revocation state.
2
3use crate::AccessError;
4use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
5#[cfg(feature = "client")]
6use crate::principal::AccessIssuer;
7use crate::principal::{TrustedIssuer, issuer_key_id};
8#[cfg(feature = "client")]
9use crate::signed::sign;
10use crate::signed::verify;
11
12const REVOCATION_TAG: u8 = 3;
13const REVOCATION_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/revocation/v1";
14
15/// Monotonic floors signed into a revocation state.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct RevocationSpec {
18    /// Monotonic state revision.
19    pub revision: u64,
20    /// Grants below this authorization epoch are invalid.
21    pub minimum_authorization_epoch: u64,
22    /// Grants below this role-key epoch are invalid.
23    pub minimum_role_key_epoch: u64,
24    /// Issuance time in Unix seconds.
25    pub issued_at: u64,
26}
27
28/// Signed epoch floors for one `(tenant, subject, scope)` tuple.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct RevocationState {
31    tenant_id: String,
32    subject_id: String,
33    scope: String,
34    revision: u64,
35    minimum_authorization_epoch: u64,
36    minimum_role_key_epoch: u64,
37    issued_at: u64,
38    issuer_id: String,
39    issuer_key_id: [u8; 32],
40    issuer_public_key: [u8; 32],
41    signature: [u8; 64],
42}
43
44impl RevocationState {
45    /// Issue signed revocation floors for one subject and scope.
46    #[cfg(feature = "client")]
47    pub fn issue(
48        issuer: &AccessIssuer,
49        tenant_id: impl Into<String>,
50        subject_id: impl Into<String>,
51        scope: impl Into<String>,
52        spec: RevocationSpec,
53    ) -> Result<Self, AccessError> {
54        let mut state = Self {
55            tenant_id: tenant_id.into(),
56            subject_id: subject_id.into(),
57            scope: scope.into(),
58            revision: spec.revision,
59            minimum_authorization_epoch: spec.minimum_authorization_epoch,
60            minimum_role_key_epoch: spec.minimum_role_key_epoch,
61            issued_at: spec.issued_at,
62            issuer_id: issuer.issuer_id().to_owned(),
63            issuer_key_id: issuer.key_id(),
64            issuer_public_key: issuer.public_key(),
65            signature: [0; 64],
66        };
67        state.validate_structure(&AccessValidationPolicy::default())?;
68        state.signature = sign(
69            REVOCATION_SIGNATURE_DOMAIN,
70            issuer.signing_key(),
71            &state.unsigned_bytes(),
72        );
73        Ok(state)
74    }
75
76    /// Tenant identifier authenticated by the signature.
77    pub fn tenant_id(&self) -> &str {
78        &self.tenant_id
79    }
80
81    /// Subject identifier authenticated by the signature.
82    pub fn subject_id(&self) -> &str {
83        &self.subject_id
84    }
85
86    /// Scope identifier authenticated by the signature.
87    pub fn scope(&self) -> &str {
88        &self.scope
89    }
90
91    /// Monotonic revocation-state revision.
92    pub const fn revision(&self) -> u64 {
93        self.revision
94    }
95
96    /// Canonical encoded and signed state.
97    pub fn encode(&self) -> Vec<u8> {
98        let mut out = self.unsigned_bytes();
99        out.extend_from_slice(&self.signature);
100        out
101    }
102
103    /// Decode, structurally validate, and verify signature math.
104    pub fn decode(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
105        let mut cursor = Cursor::new(bytes);
106        cursor.take_header(REVOCATION_TAG)?;
107        let state = Self {
108            tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
109            subject_id: cursor.take_string(limits.max_identifier_bytes)?,
110            scope: cursor.take_string(limits.max_identifier_bytes)?,
111            revision: cursor.take_u64()?,
112            minimum_authorization_epoch: cursor.take_u64()?,
113            minimum_role_key_epoch: cursor.take_u64()?,
114            issued_at: cursor.take_u64()?,
115            issuer_id: cursor.take_string(limits.max_identifier_bytes)?,
116            issuer_key_id: cursor.take_array32()?,
117            issuer_public_key: cursor.take_array32()?,
118            signature: cursor.take_array64()?,
119        };
120        if !cursor.is_empty() {
121            return Err(AccessError::TrailingBytes);
122        }
123        state.validate_structure(limits)?;
124        state.verify_signature()?;
125        if state.encode() != bytes {
126            return Err(AccessError::NonCanonicalEncoding);
127        }
128        Ok(state)
129    }
130
131    /// Verify trust and the exact tuple before exposing epoch floors.
132    pub fn verify<'a>(
133        &'a self,
134        trusted: &TrustedIssuer,
135        expected_tenant: &str,
136        expected_subject: &str,
137        expected_scope: &str,
138    ) -> Result<VerifiedRevocation<'a>, AccessError> {
139        self.verify_signature()?;
140        if !self.issuer_matches(trusted) {
141            return Err(AccessError::UntrustedIssuer);
142        }
143        if self.tenant_id != expected_tenant
144            || self.subject_id != expected_subject
145            || self.scope != expected_scope
146        {
147            return Err(AccessError::SubjectMismatch);
148        }
149        Ok(VerifiedRevocation { state: self })
150    }
151
152    fn issuer_matches(&self, trusted: &TrustedIssuer) -> bool {
153        self.issuer_id == trusted.issuer_id()
154            && self.issuer_key_id == trusted.key_id()
155            && self.issuer_public_key == trusted.public_key()
156    }
157
158    fn validate_structure(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
159        validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
160        validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
161        validate_identifier(&self.scope, limits.max_identifier_bytes)?;
162        validate_identifier(&self.issuer_id, limits.max_identifier_bytes)?;
163        if self.revision == 0
164            || self.minimum_authorization_epoch == 0
165            || self.minimum_role_key_epoch == 0
166        {
167            return Err(AccessError::InvalidEpoch);
168        }
169        if self.issuer_key_id != issuer_key_id(&self.issuer_public_key) {
170            return Err(AccessError::InvalidKeyIdentity);
171        }
172        Ok(())
173    }
174
175    fn verify_signature(&self) -> Result<(), AccessError> {
176        verify(
177            REVOCATION_SIGNATURE_DOMAIN,
178            &self.issuer_public_key,
179            &self.unsigned_bytes(),
180            &self.signature,
181        )
182    }
183
184    fn unsigned_bytes(&self) -> Vec<u8> {
185        let mut out = Vec::with_capacity(256);
186        push_header(&mut out, REVOCATION_TAG);
187        push_string(&mut out, &self.tenant_id);
188        push_string(&mut out, &self.subject_id);
189        push_string(&mut out, &self.scope);
190        out.extend_from_slice(&self.revision.to_be_bytes());
191        out.extend_from_slice(&self.minimum_authorization_epoch.to_be_bytes());
192        out.extend_from_slice(&self.minimum_role_key_epoch.to_be_bytes());
193        out.extend_from_slice(&self.issued_at.to_be_bytes());
194        push_string(&mut out, &self.issuer_id);
195        out.extend_from_slice(&self.issuer_key_id);
196        out.extend_from_slice(&self.issuer_public_key);
197        out
198    }
199}
200
201/// Revocation state that passed issuer and tuple verification.
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub struct VerifiedRevocation<'a> {
204    state: &'a RevocationState,
205}
206
207impl VerifiedRevocation<'_> {
208    #[cfg(feature = "client")]
209    pub(crate) fn accepts(
210        self,
211        trusted: &TrustedIssuer,
212        tenant_id: &str,
213        subject_id: &str,
214        scope: &str,
215        authorization_epoch: u64,
216        role_key_epoch: u64,
217    ) -> Result<(), AccessError> {
218        if !self.state.issuer_matches(trusted)
219            || self.state.tenant_id != tenant_id
220            || self.state.subject_id != subject_id
221            || self.state.scope != scope
222        {
223            return Err(AccessError::SubjectMismatch);
224        }
225        if authorization_epoch < self.state.minimum_authorization_epoch
226            || role_key_epoch < self.state.minimum_role_key_epoch
227        {
228            return Err(AccessError::Revoked);
229        }
230        Ok(())
231    }
232}