1use crate::{ContextClaimProof, DelegatedActorContext, ServiceTenancyMode, TenantContext};
2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
3use ed25519_dalek::{Signature, Verifier as _, VerifyingKey};
4use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
5use serde::{Deserialize, Serialize};
6use std::{
7 collections::{BTreeMap, BTreeSet},
8 fs::OpenOptions,
9 io::Write as _,
10 path::Path,
11 sync::atomic::{AtomicU64, Ordering},
12 sync::{Arc, Mutex},
13};
14
15const DEVELOPMENT_ISSUER: &str = "lenso-system-sandbox-development-only";
16const CREDENTIAL_KIND: &str = "lenso.delegated-context.v1";
17const MAX_SANDBOX_CREDENTIAL_TTL_MS: u64 = 5 * 60 * 1_000;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum DelegatedContextErrorCode {
22 DevelopmentProviderForbidden,
23 InvalidRequest,
24 InvalidProof,
25 CredentialExpired,
26 AudienceMismatch,
27 DelegationRequired,
28 IntentMismatch,
29 PermissionRequired,
30 OverbroadPermissions,
31 TenantRequired,
32 TenantIncompatible,
33 PolicyRequired,
34 EvidencePersistenceFailed,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct IdentityDecisionEvidence {
40 pub outcome: String,
41 pub actor_subject: Option<String>,
42 pub delegation_id: Option<String>,
43 pub tenant_id: Option<String>,
44 pub tenant_claim_id: Option<String>,
45 pub audience: String,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DelegatedContextError {
50 pub code: DelegatedContextErrorCode,
51 pub message: String,
52 pub evidence: IdentityDecisionEvidence,
53}
54
55impl std::fmt::Display for DelegatedContextError {
56 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 formatter.write_str(&self.message)
58 }
59}
60
61impl std::error::Error for DelegatedContextError {}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct DelegatedActorCredentialRequest {
65 pub subject: String,
66 pub audience: String,
67 pub intent: String,
68 pub permissions: Vec<String>,
69 pub issued_at_unix_ms: u64,
70 pub ttl_ms: u64,
71}
72
73impl DelegatedActorCredentialRequest {
74 #[must_use]
75 pub fn new<I, S>(
76 subject: impl Into<String>,
77 audience: impl Into<String>,
78 intent: impl Into<String>,
79 permissions: I,
80 issued_at_unix_ms: u64,
81 ttl_ms: u64,
82 ) -> Self
83 where
84 I: IntoIterator<Item = S>,
85 S: Into<String>,
86 {
87 Self {
88 subject: subject.into(),
89 audience: audience.into(),
90 intent: intent.into(),
91 permissions: permissions.into_iter().map(Into::into).collect(),
92 issued_at_unix_ms,
93 ttl_ms,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct TenantCredentialRequest {
100 pub tenant_id: String,
101 pub actor_subject: String,
102 pub delegation_id: String,
103 pub audience: String,
104 pub issued_at_unix_ms: u64,
105 pub ttl_ms: u64,
106}
107
108impl TenantCredentialRequest {
109 #[must_use]
110 pub fn new(
111 tenant_id: impl Into<String>,
112 actor_subject: impl Into<String>,
113 delegation_id: impl Into<String>,
114 audience: impl Into<String>,
115 issued_at_unix_ms: u64,
116 ttl_ms: u64,
117 ) -> Self {
118 Self {
119 tenant_id: tenant_id.into(),
120 actor_subject: actor_subject.into(),
121 delegation_id: delegation_id.into(),
122 audience: audience.into(),
123 issued_at_unix_ms,
124 ttl_ms,
125 }
126 }
127}
128
129pub trait DelegatedContextIssuer: std::fmt::Debug + Send + Sync {
132 fn issue_actor(
133 &self,
134 request: DelegatedActorCredentialRequest,
135 ) -> Result<DelegatedActorContext, DelegatedContextError>;
136
137 fn issue_tenant(
138 &self,
139 request: TenantCredentialRequest,
140 ) -> Result<TenantContext, DelegatedContextError>;
141}
142
143pub trait DelegatedContextVerifier: std::fmt::Debug + Send + Sync {
146 fn verify_actor(
147 &self,
148 context: &DelegatedActorContext,
149 audience: &str,
150 now_unix_ms: u64,
151 ) -> Result<(), DelegatedContextError>;
152
153 fn verify_tenant(
154 &self,
155 context: &TenantContext,
156 audience: &str,
157 now_unix_ms: u64,
158 ) -> Result<(), DelegatedContextError>;
159}
160
161pub trait DelegatedContextProvider: DelegatedContextIssuer + DelegatedContextVerifier {}
164
165impl<T> DelegatedContextProvider for T where T: DelegatedContextIssuer + DelegatedContextVerifier {}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ServiceContext {
170 pub actor: DelegatedActorContext,
171 pub tenant: Option<TenantContext>,
172}
173
174impl ServiceContext {
175 #[must_use]
176 pub const fn new(actor: DelegatedActorContext, tenant: Option<TenantContext>) -> Self {
177 Self { actor, tenant }
178 }
179}
180
181pub trait IdentityDecisionRecorder: std::fmt::Debug + Send + Sync {
182 fn record(&self, evidence: &IdentityDecisionEvidence) -> Result<(), String>;
183}
184
185#[derive(Debug, Default)]
186pub struct MemoryIdentityDecisionRecorder {
187 evidence: Mutex<Vec<IdentityDecisionEvidence>>,
188}
189
190impl MemoryIdentityDecisionRecorder {
191 #[must_use]
192 pub fn evidence(&self) -> Vec<IdentityDecisionEvidence> {
193 self.evidence
194 .lock()
195 .expect("evidence lock poisoned")
196 .clone()
197 }
198}
199
200impl IdentityDecisionRecorder for MemoryIdentityDecisionRecorder {
201 fn record(&self, evidence: &IdentityDecisionEvidence) -> Result<(), String> {
202 self.evidence
203 .lock()
204 .map_err(|_| "identity evidence lock poisoned".to_owned())?
205 .push(evidence.clone());
206 Ok(())
207 }
208}
209
210#[derive(Debug)]
211pub struct JsonlIdentityDecisionRecorder {
212 file: Mutex<std::fs::File>,
213}
214
215impl JsonlIdentityDecisionRecorder {
216 pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
217 OpenOptions::new()
218 .create(true)
219 .append(true)
220 .open(path)
221 .map(|file| Self {
222 file: Mutex::new(file),
223 })
224 }
225}
226
227impl IdentityDecisionRecorder for JsonlIdentityDecisionRecorder {
228 fn record(&self, evidence: &IdentityDecisionEvidence) -> Result<(), String> {
229 let mut line = serde_json::to_vec(evidence).map_err(|error| error.to_string())?;
230 line.push(b'\n');
231 self.file
232 .lock()
233 .map_err(|_| "identity evidence journal lock poisoned".to_owned())?
234 .write_all(&line)
235 .map_err(|error| error.to_string())
236 }
237}
238
239#[derive(Debug, Clone)]
240pub struct ServiceContextAdmission {
241 provider: Arc<dyn DelegatedContextVerifier>,
242 policies: std::collections::BTreeMap<String, ServiceContextPolicy>,
243}
244
245impl ServiceContextAdmission {
246 #[must_use]
247 pub fn new<I, S>(provider: Arc<dyn DelegatedContextVerifier>, policies: I) -> Self
248 where
249 I: IntoIterator<Item = (S, ServiceContextPolicy)>,
250 S: Into<String>,
251 {
252 Self {
253 provider,
254 policies: policies
255 .into_iter()
256 .map(|(boundary, policy)| (boundary.into(), policy))
257 .collect(),
258 }
259 }
260
261 pub fn admit(
262 &self,
263 boundary: &str,
264 context: Option<&ServiceContext>,
265 now_unix_ms: u64,
266 ) -> Result<AuthenticatedServiceContext, DelegatedContextError> {
267 let policy = self.policies.get(boundary).ok_or_else(|| {
268 bare_error(
269 DelegatedContextErrorCode::PolicyRequired,
270 "Service Context policy is required for this boundary",
271 boundary,
272 )
273 })?;
274 policy.verify(
275 self.provider.as_ref(),
276 context.map(|item| &item.actor),
277 context.and_then(|item| item.tenant.as_ref()),
278 now_unix_ms,
279 )
280 }
281
282 pub fn invalid_proof(&self, boundary: &str) -> DelegatedContextError {
283 let audience = self
284 .policies
285 .get(boundary)
286 .map_or(boundary, |policy| policy.audience.as_str());
287 invalid_proof(audience)
288 }
289
290 #[must_use]
291 pub fn tenancy_mode(&self, boundary: &str) -> Option<ServiceTenancyMode> {
292 self.policies
293 .get(boundary)
294 .map(|policy| policy.tenancy_mode.clone())
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct ServiceContextPolicy {
300 pub audience: String,
301 pub intent: String,
302 pub required_permissions: BTreeSet<String>,
303 pub allowed_permissions: BTreeSet<String>,
304 pub tenancy_mode: ServiceTenancyMode,
305}
306
307impl ServiceContextPolicy {
308 #[must_use]
309 pub fn new<R, RS, A, AS>(
310 audience: impl Into<String>,
311 intent: impl Into<String>,
312 required_permissions: R,
313 allowed_permissions: A,
314 tenancy_mode: ServiceTenancyMode,
315 ) -> Self
316 where
317 R: IntoIterator<Item = RS>,
318 RS: Into<String>,
319 A: IntoIterator<Item = AS>,
320 AS: Into<String>,
321 {
322 Self {
323 audience: audience.into(),
324 intent: intent.into(),
325 required_permissions: required_permissions.into_iter().map(Into::into).collect(),
326 allowed_permissions: allowed_permissions.into_iter().map(Into::into).collect(),
327 tenancy_mode,
328 }
329 }
330
331 pub fn verify(
332 &self,
333 provider: &dyn DelegatedContextVerifier,
334 actor: Option<&DelegatedActorContext>,
335 tenant: Option<&TenantContext>,
336 now_unix_ms: u64,
337 ) -> Result<AuthenticatedServiceContext, DelegatedContextError> {
338 let actor = actor.ok_or_else(|| {
339 self.error(
340 DelegatedContextErrorCode::DelegationRequired,
341 "Delegated Actor Context is required",
342 None,
343 tenant,
344 )
345 })?;
346 provider.verify_actor(actor, &self.audience, now_unix_ms)?;
347 if actor.intent != self.intent {
348 return Err(self.error(
349 DelegatedContextErrorCode::IntentMismatch,
350 "delegated intent is not authorized for this operation",
351 Some(actor),
352 tenant,
353 ));
354 }
355 let permissions = actor.permissions.iter().cloned().collect::<BTreeSet<_>>();
356 if !self.required_permissions.is_subset(&permissions) {
357 return Err(self.error(
358 DelegatedContextErrorCode::PermissionRequired,
359 "delegation does not grant every required permission",
360 Some(actor),
361 tenant,
362 ));
363 }
364 if !permissions.is_subset(&self.allowed_permissions) {
365 return Err(self.error(
366 DelegatedContextErrorCode::OverbroadPermissions,
367 "delegation contains permissions broader than this operation allows",
368 Some(actor),
369 tenant,
370 ));
371 }
372
373 let tenant = match (&self.tenancy_mode, tenant) {
374 (ServiceTenancyMode::Required, None) => {
375 return Err(self.error(
376 DelegatedContextErrorCode::TenantRequired,
377 "Tenant Context is required",
378 Some(actor),
379 None,
380 ));
381 }
382 (ServiceTenancyMode::None, Some(_)) => {
383 return Err(self.error(
384 DelegatedContextErrorCode::TenantIncompatible,
385 "Tenant Context is incompatible with none tenancy mode",
386 Some(actor),
387 None,
388 ));
389 }
390 (_, Some(tenant)) => {
391 provider.verify_tenant(tenant, &self.audience, now_unix_ms)?;
392 if tenant.issuer != actor.issuer
393 || tenant.expires_at_unix_ms > actor.expires_at_unix_ms
394 || tenant.actor_subject != actor.subject
395 || tenant.delegation_id != actor.delegation_id
396 {
397 return Err(self.error(
398 DelegatedContextErrorCode::TenantIncompatible,
399 "Tenant Context must be bound to this actor delegation and cannot outlive it",
400 Some(actor),
401 None,
402 ));
403 }
404 Some(tenant.clone())
405 }
406 (_, None) => None,
407 };
408
409 Ok(AuthenticatedServiceContext {
410 actor: actor.clone(),
411 tenant: tenant.clone(),
412 evidence: IdentityDecisionEvidence {
413 outcome: "identity_context_accepted".to_owned(),
414 actor_subject: Some(actor.subject.clone()),
415 delegation_id: Some(actor.delegation_id.clone()),
416 tenant_id: tenant.as_ref().map(|item| item.tenant_id.clone()),
417 tenant_claim_id: tenant.as_ref().map(|item| item.claim_id.clone()),
418 audience: self.audience.clone(),
419 },
420 })
421 }
422
423 fn error(
424 &self,
425 code: DelegatedContextErrorCode,
426 message: impl Into<String>,
427 actor: Option<&DelegatedActorContext>,
428 tenant: Option<&TenantContext>,
429 ) -> DelegatedContextError {
430 DelegatedContextError {
431 code,
432 message: message.into(),
433 evidence: IdentityDecisionEvidence {
434 outcome: error_outcome(code).to_owned(),
435 actor_subject: actor.map(|item| item.subject.clone()),
436 delegation_id: actor.map(|item| item.delegation_id.clone()),
437 tenant_id: tenant.map(|item| item.tenant_id.clone()),
438 tenant_claim_id: tenant.map(|item| item.claim_id.clone()),
439 audience: self.audience.clone(),
440 },
441 }
442 }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct AuthenticatedServiceContext {
448 pub actor: DelegatedActorContext,
449 pub tenant: Option<TenantContext>,
450 pub evidence: IdentityDecisionEvidence,
451}
452
453const ED25519_DELEGATED_CONTEXT_ALGORITHM: &str = "Ed25519";
454
455#[derive(Serialize)]
456#[serde(rename_all = "camelCase")]
457struct ActorSigningPayload<'a> {
458 protocol: &'static str,
459 issuer: &'a str,
460 subject: &'a str,
461 audiences: &'a [String],
462 intent: &'a str,
463 permissions: &'a [String],
464 expires_at_unix_ms: u64,
465 delegation_id: &'a str,
466}
467
468#[derive(Serialize)]
469#[serde(rename_all = "camelCase")]
470struct TenantSigningPayload<'a> {
471 protocol: &'static str,
472 issuer: &'a str,
473 tenant_id: &'a str,
474 actor_subject: &'a str,
475 delegation_id: &'a str,
476 audiences: &'a [String],
477 expires_at_unix_ms: u64,
478 claim_id: &'a str,
479}
480
481pub fn delegated_actor_signing_bytes(
484 context: &DelegatedActorContext,
485) -> serde_json::Result<Vec<u8>> {
486 lenso_contracts::canonical_json(&ActorSigningPayload {
487 protocol: "lenso.delegated-actor-context.ed25519.v1",
488 issuer: &context.issuer,
489 subject: &context.subject,
490 audiences: &context.audiences,
491 intent: &context.intent,
492 permissions: &context.permissions,
493 expires_at_unix_ms: context.expires_at_unix_ms,
494 delegation_id: &context.delegation_id,
495 })
496}
497
498pub fn tenant_context_signing_bytes(context: &TenantContext) -> serde_json::Result<Vec<u8>> {
501 lenso_contracts::canonical_json(&TenantSigningPayload {
502 protocol: "lenso.tenant-context.ed25519.v1",
503 issuer: &context.issuer,
504 tenant_id: &context.tenant_id,
505 actor_subject: &context.actor_subject,
506 delegation_id: &context.delegation_id,
507 audiences: &context.audiences,
508 expires_at_unix_ms: context.expires_at_unix_ms,
509 claim_id: &context.claim_id,
510 })
511}
512
513#[derive(Debug, Clone, Default)]
517pub struct Ed25519DelegatedContextVerifier {
518 keys: BTreeMap<(String, String), VerifyingKey>,
519}
520
521impl Ed25519DelegatedContextVerifier {
522 pub fn from_base64_public_keys<I, IS, MS, KS>(keys: I) -> Result<Self, DelegatedContextError>
523 where
524 I: IntoIterator<Item = (IS, MS, KS)>,
525 IS: Into<String>,
526 MS: Into<String>,
527 KS: AsRef<str>,
528 {
529 let mut parsed = BTreeMap::new();
530 for (issuer, method, encoded) in keys {
531 let issuer = issuer.into();
532 let method = method.into();
533 if issuer.trim().is_empty() || method.trim().is_empty() {
534 return Err(bare_error(
535 DelegatedContextErrorCode::InvalidRequest,
536 "delegated context issuer and verification method are required",
537 "",
538 ));
539 }
540 let bytes = URL_SAFE_NO_PAD.decode(encoded.as_ref()).map_err(|_| {
541 bare_error(
542 DelegatedContextErrorCode::InvalidRequest,
543 "delegated context Ed25519 public key encoding is invalid",
544 "",
545 )
546 })?;
547 let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
548 bare_error(
549 DelegatedContextErrorCode::InvalidRequest,
550 "delegated context Ed25519 public keys must contain exactly 32 bytes",
551 "",
552 )
553 })?;
554 let key = VerifyingKey::from_bytes(&bytes).map_err(|_| {
555 bare_error(
556 DelegatedContextErrorCode::InvalidRequest,
557 "delegated context Ed25519 public key is invalid",
558 "",
559 )
560 })?;
561 if parsed.insert((issuer, method), key).is_some() {
562 return Err(bare_error(
563 DelegatedContextErrorCode::InvalidRequest,
564 "delegated context issuer and verification method must be unique",
565 "",
566 ));
567 }
568 }
569 if parsed.is_empty() {
570 return Err(bare_error(
571 DelegatedContextErrorCode::InvalidRequest,
572 "at least one delegated context verification key is required",
573 "",
574 ));
575 }
576 Ok(Self { keys: parsed })
577 }
578
579 fn verify(
580 &self,
581 issuer: &str,
582 proof: &ContextClaimProof,
583 payload: &[u8],
584 audience: &str,
585 ) -> Result<(), DelegatedContextError> {
586 if proof.algorithm != ED25519_DELEGATED_CONTEXT_ALGORITHM {
587 return Err(invalid_proof(audience));
588 }
589 let key = self
590 .keys
591 .get(&(issuer.to_owned(), proof.verification_method.clone()))
592 .ok_or_else(|| invalid_proof(audience))?;
593 let signature = URL_SAFE_NO_PAD
594 .decode(&proof.signature)
595 .ok()
596 .and_then(|bytes| Signature::from_slice(&bytes).ok())
597 .ok_or_else(|| invalid_proof(audience))?;
598 key.verify(payload, &signature)
599 .map_err(|_| invalid_proof(audience))
600 }
601}
602
603impl DelegatedContextVerifier for Ed25519DelegatedContextVerifier {
604 fn verify_actor(
605 &self,
606 context: &DelegatedActorContext,
607 audience: &str,
608 now_unix_ms: u64,
609 ) -> Result<(), DelegatedContextError> {
610 let payload =
611 delegated_actor_signing_bytes(context).map_err(|_| invalid_proof(audience))?;
612 self.verify(&context.issuer, &context.proof, &payload, audience)?;
613 verify_audience_and_expiry(
614 &context.audiences,
615 context.expires_at_unix_ms,
616 audience,
617 now_unix_ms,
618 )
619 }
620
621 fn verify_tenant(
622 &self,
623 context: &TenantContext,
624 audience: &str,
625 now_unix_ms: u64,
626 ) -> Result<(), DelegatedContextError> {
627 let payload = tenant_context_signing_bytes(context).map_err(|_| invalid_proof(audience))?;
628 self.verify(&context.issuer, &context.proof, &payload, audience)?;
629 verify_audience_and_expiry(
630 &context.audiences,
631 context.expires_at_unix_ms,
632 audience,
633 now_unix_ms,
634 )
635 }
636}
637
638fn verify_audience_and_expiry(
639 audiences: &[String],
640 expires_at_unix_ms: u64,
641 audience: &str,
642 now_unix_ms: u64,
643) -> Result<(), DelegatedContextError> {
644 if audiences.len() != 1 || audiences[0] != audience {
645 return Err(bare_error(
646 DelegatedContextErrorCode::AudienceMismatch,
647 "delegated context is not intended for this audience",
648 audience,
649 ));
650 }
651 if expires_at_unix_ms <= now_unix_ms {
652 return Err(bare_error(
653 DelegatedContextErrorCode::CredentialExpired,
654 "delegated context has expired",
655 audience,
656 ));
657 }
658 Ok(())
659}
660
661#[derive(Debug)]
662pub struct SystemSandboxDelegatedContextProvider {
663 secret: String,
664 next_actor: AtomicU64,
665 next_tenant: AtomicU64,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
669struct DelegatedClaims {
670 iss: String,
671 sub: String,
672 aud: Vec<String>,
673 exp: u64,
674 iat: u64,
675 jti: String,
676 credential_kind: String,
677 claim_kind: String,
678 intent: Option<String>,
679 permissions: Vec<String>,
680 tenant_id: Option<String>,
681 actor_subject: Option<String>,
682 delegation_id: Option<String>,
683 expires_at_unix_ms: u64,
684}
685
686impl SystemSandboxDelegatedContextProvider {
687 pub fn new(
688 environment: &str,
689 secret: impl Into<String>,
690 ) -> Result<Self, DelegatedContextError> {
691 if !matches!(environment, "local" | "development" | "test") {
692 return Err(bare_error(
693 DelegatedContextErrorCode::DevelopmentProviderForbidden,
694 "sandbox delegated context provider is development-only",
695 "",
696 ));
697 }
698 let secret = secret.into();
699 if secret.trim().is_empty() {
700 return Err(bare_error(
701 DelegatedContextErrorCode::InvalidRequest,
702 "sandbox delegated context secret is required",
703 "",
704 ));
705 }
706 Ok(Self {
707 secret,
708 next_actor: AtomicU64::new(1),
709 next_tenant: AtomicU64::new(1),
710 })
711 }
712
713 fn issue(
714 &self,
715 mut claims: DelegatedClaims,
716 ) -> Result<(DelegatedClaims, ContextClaimProof), DelegatedContextError> {
717 validate_issue_claims(&claims)?;
718 claims.exp = claims.expires_at_unix_ms.div_ceil(1_000);
719 claims.iat /= 1_000;
720 let mut header = Header::new(Algorithm::HS256);
721 header.kid = Some("sandbox-delegated-context-1".to_owned());
722 let token = encode(
723 &header,
724 &claims,
725 &EncodingKey::from_secret(self.secret.as_bytes()),
726 )
727 .map_err(|_| invalid_proof(&claims.aud[0]))?;
728 Ok((
729 claims,
730 ContextClaimProof {
731 verification_method: "sandbox-delegated-context-1".to_owned(),
732 algorithm: "HS256".to_owned(),
733 signature: token,
734 },
735 ))
736 }
737
738 fn verify(
739 &self,
740 token: &str,
741 audience: &str,
742 now_unix_ms: u64,
743 expected_kind: &str,
744 ) -> Result<DelegatedClaims, DelegatedContextError> {
745 let mut validation = Validation::new(Algorithm::HS256);
746 validation.validate_exp = false;
747 validation.validate_aud = false;
748 validation.required_spec_claims.clear();
749 let claims = decode::<DelegatedClaims>(
750 token,
751 &DecodingKey::from_secret(self.secret.as_bytes()),
752 &validation,
753 )
754 .map_err(|_| invalid_proof(audience))?
755 .claims;
756 if claims.credential_kind != CREDENTIAL_KIND || claims.claim_kind != expected_kind {
757 return Err(invalid_proof(audience));
758 }
759 if !claims.aud.iter().any(|item| item == audience) {
760 return Err(bare_error(
761 DelegatedContextErrorCode::AudienceMismatch,
762 "delegated context is not intended for this audience",
763 audience,
764 ));
765 }
766 if claims.expires_at_unix_ms <= now_unix_ms {
767 return Err(bare_error(
768 DelegatedContextErrorCode::CredentialExpired,
769 "delegated context has expired",
770 audience,
771 ));
772 }
773 Ok(claims)
774 }
775}
776
777impl DelegatedContextIssuer for SystemSandboxDelegatedContextProvider {
778 fn issue_actor(
779 &self,
780 request: DelegatedActorCredentialRequest,
781 ) -> Result<DelegatedActorContext, DelegatedContextError> {
782 let delegation_id = format!(
783 "delegation_{}",
784 self.next_actor.fetch_add(1, Ordering::SeqCst)
785 );
786 let expires_at_unix_ms = request.issued_at_unix_ms.saturating_add(request.ttl_ms);
787 let (claims, proof) = self.issue(DelegatedClaims {
788 iss: DEVELOPMENT_ISSUER.to_owned(),
789 sub: request.subject,
790 aud: vec![request.audience],
791 exp: 0,
792 iat: request.issued_at_unix_ms,
793 jti: delegation_id.clone(),
794 credential_kind: CREDENTIAL_KIND.to_owned(),
795 claim_kind: "actor".to_owned(),
796 intent: Some(request.intent),
797 permissions: request.permissions,
798 tenant_id: None,
799 actor_subject: None,
800 delegation_id: None,
801 expires_at_unix_ms,
802 })?;
803 Ok(DelegatedActorContext {
804 issuer: claims.iss,
805 subject: claims.sub,
806 audiences: claims.aud,
807 intent: claims.intent.expect("actor claims include intent"),
808 permissions: claims.permissions,
809 expires_at_unix_ms: claims.expires_at_unix_ms,
810 delegation_id,
811 proof,
812 })
813 }
814
815 fn issue_tenant(
816 &self,
817 request: TenantCredentialRequest,
818 ) -> Result<TenantContext, DelegatedContextError> {
819 let claim_id = format!(
820 "tenant_claim_{}",
821 self.next_tenant.fetch_add(1, Ordering::SeqCst)
822 );
823 let expires_at_unix_ms = request.issued_at_unix_ms.saturating_add(request.ttl_ms);
824 let (claims, proof) = self.issue(DelegatedClaims {
825 iss: DEVELOPMENT_ISSUER.to_owned(),
826 sub: request.actor_subject.clone(),
827 aud: vec![request.audience],
828 exp: 0,
829 iat: request.issued_at_unix_ms,
830 jti: claim_id.clone(),
831 credential_kind: CREDENTIAL_KIND.to_owned(),
832 claim_kind: "tenant".to_owned(),
833 intent: None,
834 permissions: Vec::new(),
835 tenant_id: Some(request.tenant_id),
836 actor_subject: Some(request.actor_subject),
837 delegation_id: Some(request.delegation_id),
838 expires_at_unix_ms,
839 })?;
840 Ok(TenantContext {
841 issuer: claims.iss,
842 tenant_id: claims.tenant_id.expect("tenant claims include tenant id"),
843 actor_subject: claims
844 .actor_subject
845 .expect("tenant claims include actor subject"),
846 delegation_id: claims
847 .delegation_id
848 .expect("tenant claims include delegation id"),
849 audiences: claims.aud,
850 expires_at_unix_ms: claims.expires_at_unix_ms,
851 claim_id,
852 proof,
853 })
854 }
855}
856
857impl DelegatedContextVerifier for SystemSandboxDelegatedContextProvider {
858 fn verify_actor(
859 &self,
860 context: &DelegatedActorContext,
861 audience: &str,
862 now_unix_ms: u64,
863 ) -> Result<(), DelegatedContextError> {
864 let claims = self.verify(&context.proof.signature, audience, now_unix_ms, "actor")?;
865 if context.proof.algorithm != "HS256"
866 || context.proof.verification_method != "sandbox-delegated-context-1"
867 || context.issuer != claims.iss
868 || context.subject != claims.sub
869 || context.audiences != claims.aud
870 || context.intent != claims.intent.as_deref().unwrap_or_default()
871 || context.permissions != claims.permissions
872 || context.expires_at_unix_ms != claims.expires_at_unix_ms
873 || context.delegation_id != claims.jti
874 {
875 return Err(invalid_proof(audience));
876 }
877 Ok(())
878 }
879
880 fn verify_tenant(
881 &self,
882 context: &TenantContext,
883 audience: &str,
884 now_unix_ms: u64,
885 ) -> Result<(), DelegatedContextError> {
886 let claims = self.verify(&context.proof.signature, audience, now_unix_ms, "tenant")?;
887 if context.proof.algorithm != "HS256"
888 || context.proof.verification_method != "sandbox-delegated-context-1"
889 || context.issuer != claims.iss
890 || Some(context.tenant_id.as_str()) != claims.tenant_id.as_deref()
891 || Some(context.actor_subject.as_str()) != claims.actor_subject.as_deref()
892 || Some(context.delegation_id.as_str()) != claims.delegation_id.as_deref()
893 || context.audiences != claims.aud
894 || context.expires_at_unix_ms != claims.expires_at_unix_ms
895 || context.claim_id != claims.jti
896 {
897 return Err(invalid_proof(audience));
898 }
899 Ok(())
900 }
901}
902
903fn validate_issue_claims(claims: &DelegatedClaims) -> Result<(), DelegatedContextError> {
904 let ttl_ms = claims.expires_at_unix_ms.saturating_sub(claims.iat);
905 let actor_invalid = claims.claim_kind == "actor"
906 && (claims.intent.as_deref().is_none_or(str::is_empty)
907 || claims.permissions.is_empty()
908 || claims.permissions.iter().any(String::is_empty));
909 if claims.sub.trim().is_empty()
910 || claims.aud.len() != 1
911 || claims.aud[0].trim().is_empty()
912 || ttl_ms == 0
913 || ttl_ms > MAX_SANDBOX_CREDENTIAL_TTL_MS
914 || actor_invalid
915 || (claims.claim_kind == "tenant"
916 && (claims.tenant_id.as_deref().is_none_or(str::is_empty)
917 || claims.actor_subject.as_deref().is_none_or(str::is_empty)
918 || claims.delegation_id.as_deref().is_none_or(str::is_empty)))
919 {
920 return Err(bare_error(
921 DelegatedContextErrorCode::InvalidRequest,
922 "delegated context request must be bounded and non-empty",
923 claims.aud.first().map_or("", String::as_str),
924 ));
925 }
926 Ok(())
927}
928
929fn invalid_proof(audience: &str) -> DelegatedContextError {
930 bare_error(
931 DelegatedContextErrorCode::InvalidProof,
932 "delegated context proof is invalid",
933 audience,
934 )
935}
936
937fn bare_error(
938 code: DelegatedContextErrorCode,
939 message: impl Into<String>,
940 audience: &str,
941) -> DelegatedContextError {
942 DelegatedContextError {
943 code,
944 message: message.into(),
945 evidence: IdentityDecisionEvidence {
946 outcome: error_outcome(code).to_owned(),
947 actor_subject: None,
948 delegation_id: None,
949 tenant_id: None,
950 tenant_claim_id: None,
951 audience: audience.to_owned(),
952 },
953 }
954}
955
956const fn error_outcome(code: DelegatedContextErrorCode) -> &'static str {
957 match code {
958 DelegatedContextErrorCode::DevelopmentProviderForbidden => "development_provider_forbidden",
959 DelegatedContextErrorCode::InvalidRequest => "delegated_context_invalid_request",
960 DelegatedContextErrorCode::InvalidProof => "delegated_context_invalid_proof",
961 DelegatedContextErrorCode::CredentialExpired => "delegated_context_expired",
962 DelegatedContextErrorCode::AudienceMismatch => "delegated_context_audience_mismatch",
963 DelegatedContextErrorCode::DelegationRequired => "delegation_required",
964 DelegatedContextErrorCode::IntentMismatch => "delegated_intent_mismatch",
965 DelegatedContextErrorCode::PermissionRequired => "delegated_permission_required",
966 DelegatedContextErrorCode::OverbroadPermissions => "delegated_permissions_overbroad",
967 DelegatedContextErrorCode::TenantRequired => "tenant_context_required",
968 DelegatedContextErrorCode::TenantIncompatible => "tenant_context_incompatible",
969 DelegatedContextErrorCode::PolicyRequired => "service_context_policy_required",
970 DelegatedContextErrorCode::EvidencePersistenceFailed => {
971 "identity_evidence_persistence_failed"
972 }
973 }
974}