1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct RevocationSpec {
18 pub revision: u64,
20 pub minimum_authorization_epoch: u64,
22 pub minimum_role_key_epoch: u64,
24 pub issued_at: u64,
26}
27
28#[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 #[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 pub fn tenant_id(&self) -> &str {
78 &self.tenant_id
79 }
80
81 pub fn subject_id(&self) -> &str {
83 &self.subject_id
84 }
85
86 pub fn scope(&self) -> &str {
88 &self.scope
89 }
90
91 pub const fn revision(&self) -> u64 {
93 self.revision
94 }
95
96 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 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 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#[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}