1use blindplane_crypto::Sha256;
4
5use crate::AccessError;
6use crate::codec::{AccessValidationPolicy, Cursor, push_header, push_string, validate_identifier};
7use crate::principal::TrustedIssuer;
8#[cfg(feature = "client")]
9use crate::principal::{AccessIssuer, Principal};
10#[cfg(feature = "client")]
11use crate::signed::sign;
12use crate::signed::verify;
13
14const POLICY_TAG: u8 = 2;
15const POLICY_SIGNATURE_DOMAIN: &[u8] = b"blindplane/access/policy/v1";
16
17#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
19pub enum CapabilityKind {
20 Mcp,
22 Tool,
24 Skill,
26 Cli,
28}
29
30impl CapabilityKind {
31 const fn code(self) -> u8 {
32 match self {
33 Self::Mcp => 1,
34 Self::Tool => 2,
35 Self::Skill => 3,
36 Self::Cli => 4,
37 }
38 }
39
40 fn from_code(code: u8) -> Result<Self, AccessError> {
41 match code {
42 1 => Ok(Self::Mcp),
43 2 => Ok(Self::Tool),
44 3 => Ok(Self::Skill),
45 4 => Ok(Self::Cli),
46 _ => Err(AccessError::WrongObjectType),
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
53pub enum Effect {
54 Allow,
56 Deny,
58}
59
60impl Effect {
61 const fn code(self) -> u8 {
62 match self {
63 Self::Allow => 1,
64 Self::Deny => 2,
65 }
66 }
67
68 fn from_code(code: u8) -> Result<Self, AccessError> {
69 match code {
70 1 => Ok(Self::Allow),
71 2 => Ok(Self::Deny),
72 _ => Err(AccessError::WrongObjectType),
73 }
74 }
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum Decision {
80 Allow,
82 Deny,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct CapabilityRule {
89 kind: CapabilityKind,
90 name: String,
91 effect: Effect,
92}
93
94impl CapabilityRule {
95 pub fn new(
97 kind: CapabilityKind,
98 name: impl Into<String>,
99 effect: Effect,
100 ) -> Result<Self, AccessError> {
101 let name = name.into();
102 validate_identifier(
103 &name,
104 AccessValidationPolicy::default().max_identifier_bytes,
105 )?;
106 Ok(Self { kind, name, effect })
107 }
108
109 pub const fn kind(&self) -> CapabilityKind {
111 self.kind
112 }
113
114 pub fn name(&self) -> &str {
116 &self.name
117 }
118
119 pub const fn effect(&self) -> Effect {
121 self.effect
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct PolicySpec {
128 pub revision: u64,
130 pub previous_hash: [u8; 32],
132 pub authorization_epoch: u64,
134 pub issued_at: u64,
136 pub not_before: u64,
138 pub not_after: u64,
140 pub rules: Vec<CapabilityRule>,
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct TenantPolicy {
147 tenant_id: String,
148 subject_id: String,
149 subject_key_id: [u8; 32],
150 revision: u64,
151 previous_hash: [u8; 32],
152 authorization_epoch: u64,
153 issued_at: u64,
154 not_before: u64,
155 not_after: u64,
156 default_effect: Effect,
157 rules: Vec<CapabilityRule>,
158 issuer_id: String,
159 issuer_key_id: [u8; 32],
160 issuer_public_key: [u8; 32],
161 signature: [u8; 64],
162}
163
164impl TenantPolicy {
165 #[cfg(feature = "client")]
167 pub fn issue(
168 issuer: &AccessIssuer,
169 subject: &Principal,
170 mut spec: PolicySpec,
171 ) -> Result<Self, AccessError> {
172 spec.rules.sort_by(|left, right| {
173 (left.kind, left.name.as_str(), left.effect).cmp(&(
174 right.kind,
175 right.name.as_str(),
176 right.effect,
177 ))
178 });
179 let mut policy = Self {
180 tenant_id: subject.tenant_id().to_owned(),
181 subject_id: subject.principal_id().to_owned(),
182 subject_key_id: subject.key_id(),
183 revision: spec.revision,
184 previous_hash: spec.previous_hash,
185 authorization_epoch: spec.authorization_epoch,
186 issued_at: spec.issued_at,
187 not_before: spec.not_before,
188 not_after: spec.not_after,
189 default_effect: Effect::Deny,
190 rules: spec.rules,
191 issuer_id: issuer.issuer_id().to_owned(),
192 issuer_key_id: issuer.key_id(),
193 issuer_public_key: issuer.public_key(),
194 signature: [0; 64],
195 };
196 policy.validate_structure(&AccessValidationPolicy::default())?;
197 policy.signature = sign(
198 POLICY_SIGNATURE_DOMAIN,
199 issuer.signing_key(),
200 &policy.unsigned_bytes(),
201 );
202 Ok(policy)
203 }
204
205 pub fn tenant_id(&self) -> &str {
207 &self.tenant_id
208 }
209
210 pub fn subject_id(&self) -> &str {
212 &self.subject_id
213 }
214
215 pub const fn revision(&self) -> u64 {
217 self.revision
218 }
219
220 pub const fn authorization_epoch(&self) -> u64 {
222 self.authorization_epoch
223 }
224
225 pub fn encode(&self) -> Vec<u8> {
227 let mut out = self.unsigned_bytes();
228 out.extend_from_slice(&self.signature);
229 out
230 }
231
232 pub fn policy_hash(&self) -> [u8; 32] {
234 let encoded = self.encode();
235 let mut input = Vec::with_capacity(POLICY_SIGNATURE_DOMAIN.len() + encoded.len());
236 input.extend_from_slice(POLICY_SIGNATURE_DOMAIN);
237 input.extend_from_slice(&encoded);
238 Sha256::digest(&input)
239 }
240
241 pub fn decode(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
243 let mut cursor = Cursor::new(bytes);
244 cursor.take_header(POLICY_TAG)?;
245 let tenant_id = cursor.take_string(limits.max_identifier_bytes)?;
246 let subject_id = cursor.take_string(limits.max_identifier_bytes)?;
247 let subject_key_id = cursor.take_array32()?;
248 let revision = cursor.take_u64()?;
249 let previous_hash = cursor.take_array32()?;
250 let authorization_epoch = cursor.take_u64()?;
251 let issued_at = cursor.take_u64()?;
252 let not_before = cursor.take_u64()?;
253 let not_after = cursor.take_u64()?;
254 let default_effect = Effect::from_code(cursor.take_u8()?)?;
255 let rule_count = cursor.take_len(limits.max_rules)?;
256 let mut rules = Vec::with_capacity(rule_count);
257 for _ in 0..rule_count {
258 rules.push(CapabilityRule {
259 kind: CapabilityKind::from_code(cursor.take_u8()?)?,
260 name: cursor.take_string(limits.max_identifier_bytes)?,
261 effect: Effect::from_code(cursor.take_u8()?)?,
262 });
263 }
264 let issuer_id = cursor.take_string(limits.max_identifier_bytes)?;
265 let issuer_key_id = cursor.take_array32()?;
266 let issuer_public_key = cursor.take_array32()?;
267 let signature = cursor.take_array64()?;
268 if !cursor.is_empty() {
269 return Err(AccessError::TrailingBytes);
270 }
271 let policy = Self {
272 tenant_id,
273 subject_id,
274 subject_key_id,
275 revision,
276 previous_hash,
277 authorization_epoch,
278 issued_at,
279 not_before,
280 not_after,
281 default_effect,
282 rules,
283 issuer_id,
284 issuer_key_id,
285 issuer_public_key,
286 signature,
287 };
288 policy.validate_structure(limits)?;
289 verify(
290 POLICY_SIGNATURE_DOMAIN,
291 &policy.issuer_public_key,
292 &policy.unsigned_bytes(),
293 &policy.signature,
294 )?;
295 if policy.encode() != bytes {
296 return Err(AccessError::NonCanonicalEncoding);
297 }
298 Ok(policy)
299 }
300
301 pub fn verify<'a>(
303 &'a self,
304 trusted: &TrustedIssuer,
305 expected_tenant: &str,
306 expected_subject: &str,
307 now: u64,
308 minimum_revision: u64,
309 minimum_authorization_epoch: u64,
310 ) -> Result<VerifiedPolicy<'a>, AccessError> {
311 verify(
312 POLICY_SIGNATURE_DOMAIN,
313 &self.issuer_public_key,
314 &self.unsigned_bytes(),
315 &self.signature,
316 )?;
317 if self.issuer_id != trusted.issuer_id()
318 || self.issuer_key_id != trusted.key_id()
319 || self.issuer_public_key != trusted.public_key()
320 {
321 return Err(AccessError::UntrustedIssuer);
322 }
323 if self.tenant_id != expected_tenant || self.subject_id != expected_subject {
324 return Err(AccessError::SubjectMismatch);
325 }
326 if now < self.not_before {
327 return Err(AccessError::NotYetValid);
328 }
329 if now > self.not_after {
330 return Err(AccessError::Expired);
331 }
332 if self.revision < minimum_revision {
333 return Err(AccessError::StaleRevision);
334 }
335 if self.authorization_epoch < minimum_authorization_epoch {
336 return Err(AccessError::Revoked);
337 }
338 Ok(VerifiedPolicy { policy: self })
339 }
340
341 fn validate_structure(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
342 validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
343 validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
344 validate_identifier(&self.issuer_id, limits.max_identifier_bytes)?;
345 if self.revision == 0
346 || self.authorization_epoch == 0
347 || self.not_before > self.not_after
348 || self.issued_at > self.not_after
349 || (self.revision == 1 && self.previous_hash != [0; 32])
350 || (self.revision > 1 && self.previous_hash == [0; 32])
351 {
352 return Err(AccessError::InvalidEpoch);
353 }
354 if self.default_effect != Effect::Deny || self.rules.len() > limits.max_rules {
355 return Err(AccessError::LengthLimit(self.rules.len()));
356 }
357 for rule in &self.rules {
358 validate_identifier(&rule.name, limits.max_identifier_bytes)?;
359 }
360 for pair in self.rules.windows(2) {
361 let left = (pair[0].kind, pair[0].name.as_str());
362 let right = (pair[1].kind, pair[1].name.as_str());
363 if left >= right {
364 return Err(AccessError::DuplicateRule);
365 }
366 }
367 Ok(())
368 }
369
370 fn unsigned_bytes(&self) -> Vec<u8> {
371 let mut out = Vec::with_capacity(256 + self.rules.len() * 32);
372 push_header(&mut out, POLICY_TAG);
373 push_string(&mut out, &self.tenant_id);
374 push_string(&mut out, &self.subject_id);
375 out.extend_from_slice(&self.subject_key_id);
376 out.extend_from_slice(&self.revision.to_be_bytes());
377 out.extend_from_slice(&self.previous_hash);
378 out.extend_from_slice(&self.authorization_epoch.to_be_bytes());
379 out.extend_from_slice(&self.issued_at.to_be_bytes());
380 out.extend_from_slice(&self.not_before.to_be_bytes());
381 out.extend_from_slice(&self.not_after.to_be_bytes());
382 out.push(self.default_effect.code());
383 out.extend_from_slice(
384 &u32::try_from(self.rules.len())
385 .expect("policy rules are bounded")
386 .to_be_bytes(),
387 );
388 for rule in &self.rules {
389 out.push(rule.kind.code());
390 push_string(&mut out, &rule.name);
391 out.push(rule.effect.code());
392 }
393 push_string(&mut out, &self.issuer_id);
394 out.extend_from_slice(&self.issuer_key_id);
395 out.extend_from_slice(&self.issuer_public_key);
396 out
397 }
398}
399
400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
402pub struct VerifiedPolicy<'a> {
403 policy: &'a TenantPolicy,
404}
405
406impl VerifiedPolicy<'_> {
407 pub fn decision(&self, kind: CapabilityKind, name: &str) -> Decision {
409 self.policy
410 .rules
411 .iter()
412 .find(|rule| rule.kind == kind && rule.name == name)
413 .map_or(Decision::Deny, |rule| match rule.effect {
414 Effect::Allow => Decision::Allow,
415 Effect::Deny => Decision::Deny,
416 })
417 }
418}