1use crate::{ContextClaimProof, ServicePrincipal};
2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
3use jsonwebtoken::{
4 Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, decode_header, encode,
5 errors::ErrorKind,
6};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use spiffe::{
10 JwtSource, JwtSvid, JwtSvidError, SpiffeId, TrustDomain, X509Source, X509Svid,
11 x509_source::SvidPicker,
12};
13use std::{
14 collections::BTreeMap,
15 future::Future,
16 pin::Pin,
17 sync::{
18 RwLock,
19 atomic::{AtomicU64, Ordering},
20 },
21};
22
23const DEVELOPMENT_ISSUER: &str = "lenso-system-sandbox-development-only";
24const CREDENTIAL_KIND: &str = "lenso.workload-identity.v1";
25const MAX_SANDBOX_CREDENTIAL_TTL_MS: u64 = 5 * 60 * 1_000;
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct AuthenticatedTransportBinding(pub String);
31
32impl AuthenticatedTransportBinding {
33 #[must_use]
34 pub fn new(proof: impl Into<String>) -> Self {
35 Self(proof.into())
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum WorkloadIdentityErrorCode {
42 DevelopmentProviderForbidden,
43 InvalidRequest,
44 InvalidProof,
45 IssuerMismatch,
46 AudienceMismatch,
47 CredentialExpired,
48 TransportBindingMismatch,
49 StaleCredential,
50 RotationFailed,
51 ProviderUnavailable,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct WorkloadIdentityEvidence {
57 pub outcome: String,
58 pub service_principal: Option<String>,
59 pub credential_id: Option<String>,
60 pub key_id: Option<String>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct WorkloadIdentityError {
65 pub code: WorkloadIdentityErrorCode,
66 pub message: String,
67 pub evidence: WorkloadIdentityEvidence,
68}
69
70impl std::fmt::Display for WorkloadIdentityError {
71 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 formatter.write_str(&self.message)
73 }
74}
75
76impl std::error::Error for WorkloadIdentityError {}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct WorkloadCredentialRequest {
80 pub service_principal: String,
81 pub audience: String,
82 pub authenticated_transport_binding: String,
83 pub issued_at_unix_ms: u64,
84 pub ttl_ms: u64,
85}
86
87impl WorkloadCredentialRequest {
88 #[must_use]
89 pub fn new(
90 service_principal: impl Into<String>,
91 audience: impl Into<String>,
92 authenticated_transport_binding: impl Into<String>,
93 issued_at_unix_ms: u64,
94 ttl_ms: u64,
95 ) -> Self {
96 Self {
97 service_principal: service_principal.into(),
98 audience: audience.into(),
99 authenticated_transport_binding: authenticated_transport_binding.into(),
100 issued_at_unix_ms,
101 ttl_ms,
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct WorkloadCredential {
109 pub token: String,
110 pub issuer: String,
111 pub service_principal: String,
112 pub audience: String,
113 pub expires_at_unix_ms: u64,
114 pub credential_id: String,
115 pub key_id: String,
116 pub algorithm: String,
117}
118
119impl WorkloadCredential {
120 #[must_use]
121 pub fn service_principal_context(&self) -> ServicePrincipal {
122 ServicePrincipal {
123 issuer: self.issuer.clone(),
124 subject: self.service_principal.clone(),
125 audiences: vec![self.audience.clone()],
126 expires_at_unix_ms: self.expires_at_unix_ms,
127 credential_id: self.credential_id.clone(),
128 proof: ContextClaimProof {
129 verification_method: self.key_id.clone(),
130 algorithm: self.algorithm.clone(),
131 signature: self.token.clone(),
132 },
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct WorkloadIdentityVerification {
139 pub audience: String,
140 pub authenticated_transport_binding: String,
141 pub now_unix_ms: u64,
142}
143
144impl WorkloadIdentityVerification {
145 #[must_use]
146 pub fn new(
147 audience: impl Into<String>,
148 authenticated_transport_binding: impl Into<String>,
149 now_unix_ms: u64,
150 ) -> Self {
151 Self {
152 audience: audience.into(),
153 authenticated_transport_binding: authenticated_transport_binding.into(),
154 now_unix_ms,
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct AuthenticatedServicePrincipal {
162 pub service_principal: String,
163 pub credential_id: String,
164 pub issuer: String,
165 pub audience: String,
166 pub expires_at_unix_ms: u64,
167 pub key_id: String,
168 pub algorithm: String,
169 pub evidence: WorkloadIdentityEvidence,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct WorkloadIdentityRotationEvidence {
175 pub outcome: String,
176 pub previous_key_id: String,
177 pub active_key_id: String,
178}
179
180pub trait WorkloadIdentityProvider: std::fmt::Debug + Send + Sync {
181 fn issue(
182 &self,
183 request: WorkloadCredentialRequest,
184 ) -> Result<WorkloadCredential, WorkloadIdentityError>;
185
186 fn issue_async(
190 &self,
191 request: WorkloadCredentialRequest,
192 ) -> Pin<Box<dyn Future<Output = Result<WorkloadCredential, WorkloadIdentityError>> + Send + '_>>
193 {
194 Box::pin(async move { self.issue(request) })
195 }
196
197 fn verify(
198 &self,
199 token: &str,
200 verification: &WorkloadIdentityVerification,
201 ) -> Result<AuthenticatedServicePrincipal, WorkloadIdentityError>;
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct SpiffeWorkloadIdentityConfig {
209 endpoint: String,
210 trust_domain: TrustDomain,
211 service_principal: String,
212 spiffe_id: SpiffeId,
213}
214
215impl SpiffeWorkloadIdentityConfig {
216 pub fn new(
217 endpoint: impl Into<String>,
218 trust_domain: impl AsRef<str>,
219 service_principal: impl Into<String>,
220 ) -> Result<Self, WorkloadIdentityError> {
221 let endpoint = endpoint.into();
222 let service_principal = service_principal.into();
223 if endpoint.trim().is_empty() {
224 return Err(identity_error(
225 WorkloadIdentityErrorCode::InvalidRequest,
226 "invalid_spiffe_endpoint",
227 "SPIFFE Workload Identity requires a Workload API endpoint",
228 Some(service_principal),
229 None,
230 None,
231 ));
232 }
233 let Some(service_id) = service_principal.strip_prefix("service:") else {
234 return Err(invalid_service_principal());
235 };
236 if !valid_service_principal(&service_principal) {
237 return Err(invalid_service_principal());
238 }
239 let trust_domain = TrustDomain::new(trust_domain.as_ref()).map_err(|_| {
240 identity_error(
241 WorkloadIdentityErrorCode::InvalidRequest,
242 "invalid_spiffe_trust_domain",
243 "SPIFFE Workload Identity requires a valid trust domain",
244 Some(service_principal.clone()),
245 None,
246 None,
247 )
248 })?;
249 let spiffe_id = SpiffeId::from_segments(trust_domain.clone(), &["service", service_id])
250 .map_err(|_| invalid_service_principal())?;
251 Ok(Self {
252 endpoint,
253 trust_domain,
254 service_principal,
255 spiffe_id,
256 })
257 }
258
259 #[must_use]
260 pub fn endpoint(&self) -> &str {
261 &self.endpoint
262 }
263
264 #[must_use]
265 pub const fn trust_domain(&self) -> &TrustDomain {
266 &self.trust_domain
267 }
268
269 #[must_use]
270 pub fn service_principal(&self) -> &str {
271 &self.service_principal
272 }
273
274 #[must_use]
275 pub const fn spiffe_id(&self) -> &SpiffeId {
276 &self.spiffe_id
277 }
278}
279
280#[derive(Debug)]
281struct ExactSpiffeIdPicker(SpiffeId);
282
283impl SvidPicker for ExactSpiffeIdPicker {
284 fn pick_svid(&self, svids: &[std::sync::Arc<X509Svid>]) -> Option<usize> {
285 svids.iter().position(|svid| svid.spiffe_id() == &self.0)
286 }
287}
288
289#[derive(Debug)]
293pub struct SpiffeWorkloadIdentityProvider {
294 config: SpiffeWorkloadIdentityConfig,
295 jwt_source: JwtSource,
296 x509_source: X509Source,
297}
298
299impl SpiffeWorkloadIdentityProvider {
300 pub async fn connect(
301 config: SpiffeWorkloadIdentityConfig,
302 ) -> Result<Self, WorkloadIdentityError> {
303 let x509_source = X509Source::builder()
304 .endpoint(config.endpoint())
305 .picker(ExactSpiffeIdPicker(config.spiffe_id().clone()))
306 .build()
307 .await
308 .map_err(|_| provider_unavailable("spiffe_x509_source_unavailable"))?;
309 let selected = x509_source
310 .svid()
311 .map_err(|_| provider_unavailable("spiffe_x509_source_unavailable"))?;
312 if selected.spiffe_id() != config.spiffe_id() {
313 x509_source.shutdown().await;
314 return Err(identity_error(
315 WorkloadIdentityErrorCode::IssuerMismatch,
316 "spiffe_identity_mismatch",
317 "The SPIFFE Workload API did not provide the configured Service identity",
318 Some(config.service_principal().to_owned()),
319 None,
320 None,
321 ));
322 }
323 let jwt_source = match JwtSource::builder()
324 .endpoint(config.endpoint())
325 .build()
326 .await
327 {
328 Ok(source) => source,
329 Err(_) => {
330 x509_source.shutdown().await;
331 return Err(provider_unavailable("spiffe_jwt_source_unavailable"));
332 }
333 };
334 Ok(Self {
335 config,
336 jwt_source,
337 x509_source,
338 })
339 }
340
341 #[must_use]
342 pub fn config(&self) -> &SpiffeWorkloadIdentityConfig {
343 &self.config
344 }
345
346 #[must_use]
349 pub fn x509_source(&self) -> X509Source {
350 self.x509_source.clone()
351 }
352
353 #[must_use]
354 pub fn authenticated_transport_binding(
355 peer_spiffe_id: &SpiffeId,
356 ) -> AuthenticatedTransportBinding {
357 AuthenticatedTransportBinding::new(peer_spiffe_id.to_string())
358 }
359
360 pub async fn shutdown(&self) {
361 self.jwt_source.shutdown().await;
362 self.x509_source.shutdown().await;
363 }
364}
365
366impl WorkloadIdentityProvider for SpiffeWorkloadIdentityProvider {
367 fn issue(
368 &self,
369 request: WorkloadCredentialRequest,
370 ) -> Result<WorkloadCredential, WorkloadIdentityError> {
371 Err(identity_error(
372 WorkloadIdentityErrorCode::ProviderUnavailable,
373 "spiffe_async_issuance_required",
374 "SPIFFE credential issuance requires the asynchronous provider method",
375 Some(request.service_principal),
376 None,
377 None,
378 ))
379 }
380
381 fn issue_async(
382 &self,
383 request: WorkloadCredentialRequest,
384 ) -> Pin<Box<dyn Future<Output = Result<WorkloadCredential, WorkloadIdentityError>> + Send + '_>>
385 {
386 Box::pin(async move {
387 if request.service_principal != self.config.service_principal
388 || request.audience.trim().is_empty()
389 || request.authenticated_transport_binding != self.config.spiffe_id.to_string()
390 || request.ttl_ms == 0
391 {
392 return Err(identity_error(
393 WorkloadIdentityErrorCode::InvalidRequest,
394 "invalid_spiffe_credential_request",
395 "SPIFFE credentials require the configured Service Principal, audience, mTLS peer identity, and positive lifetime",
396 Some(request.service_principal),
397 None,
398 None,
399 ));
400 }
401 let svid = self
402 .jwt_source
403 .fetch_jwt_svid_with_id([&request.audience], Some(&self.config.spiffe_id))
404 .await
405 .map_err(|_| provider_unavailable("spiffe_credential_issuance_failed"))?;
406 if svid.spiffe_id() != &self.config.spiffe_id {
407 return Err(identity_error(
408 WorkloadIdentityErrorCode::IssuerMismatch,
409 "spiffe_identity_mismatch",
410 "The SPIFFE Workload API issued an unexpected Service identity",
411 Some(request.service_principal),
412 None,
413 Some(svid.key_id().to_owned()),
414 ));
415 }
416 let expires_at_unix_ms = expiry_unix_ms(&svid)?;
417 let maximum_expiry = request
418 .issued_at_unix_ms
419 .checked_add(request.ttl_ms)
420 .and_then(|value| value.checked_add(999))
421 .ok_or_else(|| {
422 identity_error(
423 WorkloadIdentityErrorCode::InvalidRequest,
424 "invalid_credential_lifetime",
425 "Workload credential lifetime exceeds the supported timestamp range",
426 Some(request.service_principal.clone()),
427 None,
428 Some(svid.key_id().to_owned()),
429 )
430 })?;
431 if expires_at_unix_ms <= request.issued_at_unix_ms
432 || expires_at_unix_ms > maximum_expiry
433 {
434 return Err(identity_error(
435 WorkloadIdentityErrorCode::InvalidRequest,
436 "credential_lifetime_out_of_policy",
437 "The SPIFFE issuer credential lifetime exceeds the requested maximum",
438 Some(request.service_principal),
439 None,
440 Some(svid.key_id().to_owned()),
441 ));
442 }
443 let token = svid.token().to_owned();
444 let algorithm = jwt_algorithm(&token)?;
445 Ok(WorkloadCredential {
446 credential_id: credential_id(&token),
447 token,
448 issuer: format!("spiffe://{}", self.config.trust_domain),
449 service_principal: self.config.service_principal.clone(),
450 audience: request.audience,
451 expires_at_unix_ms,
452 key_id: svid.key_id().to_owned(),
453 algorithm,
454 })
455 })
456 }
457
458 fn verify(
459 &self,
460 token: &str,
461 verification: &WorkloadIdentityVerification,
462 ) -> Result<AuthenticatedServicePrincipal, WorkloadIdentityError> {
463 let svid = JwtSvid::parse_and_validate(token, &self.jwt_source, &[&verification.audience])
464 .map_err(spiffe_verification_error)?;
465 if svid.spiffe_id().trust_domain() != &self.config.trust_domain {
466 return Err(identity_error(
467 WorkloadIdentityErrorCode::IssuerMismatch,
468 "issuer_mismatch",
469 "SPIFFE Workload Identity belongs to an untrusted domain",
470 None,
471 Some(credential_id(token)),
472 Some(svid.key_id().to_owned()),
473 ));
474 }
475 let service_principal =
476 service_principal_from_spiffe_id(svid.spiffe_id()).ok_or_else(invalid_proof)?;
477 let expires_at_unix_ms = expiry_unix_ms(&svid)?;
478 if verification.now_unix_ms >= expires_at_unix_ms {
479 return Err(identity_error(
480 WorkloadIdentityErrorCode::CredentialExpired,
481 "credential_expired",
482 "Workload Identity credential has expired",
483 Some(service_principal),
484 Some(credential_id(token)),
485 Some(svid.key_id().to_owned()),
486 ));
487 }
488 if verification.authenticated_transport_binding != svid.spiffe_id().to_string() {
489 return Err(identity_error(
490 WorkloadIdentityErrorCode::TransportBindingMismatch,
491 "transport_binding_mismatch",
492 "JWT-SVID identity is not bound to the authenticated X.509-SVID mTLS peer",
493 Some(service_principal),
494 Some(credential_id(token)),
495 Some(svid.key_id().to_owned()),
496 ));
497 }
498 let credential_id = credential_id(token);
499 let key_id = svid.key_id().to_owned();
500 Ok(AuthenticatedServicePrincipal {
501 service_principal: service_principal.clone(),
502 credential_id: credential_id.clone(),
503 issuer: format!("spiffe://{}", self.config.trust_domain),
504 audience: verification.audience.clone(),
505 expires_at_unix_ms,
506 key_id: key_id.clone(),
507 algorithm: jwt_algorithm(token)?,
508 evidence: WorkloadIdentityEvidence {
509 outcome: "authenticated".to_owned(),
510 service_principal: Some(service_principal),
511 credential_id: Some(credential_id),
512 key_id: Some(key_id),
513 },
514 })
515 }
516}
517
518#[derive(Debug)]
519pub struct SystemSandboxWorkloadIdentityProvider {
520 state: RwLock<SystemSandboxProviderState>,
521 next_credential: AtomicU64,
522}
523
524#[derive(Debug)]
525struct SystemSandboxProviderState {
526 secret: String,
527 generation: u64,
528 retired_secrets: BTreeMap<String, String>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
532struct WorkloadClaims {
533 iss: String,
534 sub: String,
535 aud: Vec<String>,
536 exp: u64,
537 iat: u64,
538 jti: String,
539 credential_kind: String,
540 authenticated_transport_binding: String,
541 expires_at_unix_ms: u64,
542}
543
544impl SystemSandboxWorkloadIdentityProvider {
545 pub fn new(
546 environment: &str,
547 secret: impl Into<String>,
548 ) -> Result<Self, WorkloadIdentityError> {
549 if !matches!(
550 environment.trim().to_ascii_lowercase().as_str(),
551 "local" | "dev" | "development" | "test"
552 ) {
553 return Err(identity_error(
554 WorkloadIdentityErrorCode::DevelopmentProviderForbidden,
555 "development_provider_forbidden",
556 "The System Sandbox Workload Identity provider is development-only",
557 None,
558 None,
559 None,
560 ));
561 }
562 let secret = secret.into();
563 if secret.is_empty() {
564 return Err(identity_error(
565 WorkloadIdentityErrorCode::InvalidRequest,
566 "invalid_provider_secret",
567 "The System Sandbox signing secret must not be empty",
568 None,
569 None,
570 None,
571 ));
572 }
573 Ok(Self {
574 state: RwLock::new(SystemSandboxProviderState {
575 secret,
576 generation: 1,
577 retired_secrets: BTreeMap::new(),
578 }),
579 next_credential: AtomicU64::new(1),
580 })
581 }
582
583 pub fn rotate(
584 &self,
585 new_secret: impl Into<String>,
586 ) -> Result<WorkloadIdentityRotationEvidence, WorkloadIdentityError> {
587 let new_secret = new_secret.into();
588 let mut state = self.state.write().expect("identity provider lock poisoned");
589 let active_key_id = key_id(state.generation);
590 if new_secret.is_empty() || new_secret == state.secret {
591 return Err(identity_error(
592 WorkloadIdentityErrorCode::RotationFailed,
593 "rotation_failed",
594 "The System Sandbox credential rotation did not provide a new signing secret",
595 None,
596 None,
597 Some(active_key_id),
598 ));
599 }
600 let previous_key_id = active_key_id;
601 let previous_secret = std::mem::replace(&mut state.secret, new_secret);
602 state
603 .retired_secrets
604 .insert(previous_key_id.clone(), previous_secret);
605 state.generation += 1;
606 Ok(WorkloadIdentityRotationEvidence {
607 outcome: "rotated".to_owned(),
608 previous_key_id,
609 active_key_id: key_id(state.generation),
610 })
611 }
612}
613
614impl WorkloadIdentityProvider for SystemSandboxWorkloadIdentityProvider {
615 fn issue(
616 &self,
617 request: WorkloadCredentialRequest,
618 ) -> Result<WorkloadCredential, WorkloadIdentityError> {
619 if !valid_service_principal(&request.service_principal)
620 || request.audience.trim().is_empty()
621 || request.authenticated_transport_binding.trim().is_empty()
622 || request.ttl_ms == 0
623 || request.ttl_ms > MAX_SANDBOX_CREDENTIAL_TTL_MS
624 {
625 return Err(identity_error(
626 WorkloadIdentityErrorCode::InvalidRequest,
627 "invalid_credential_request",
628 "Workload credentials require a Service Principal, audience, authenticated transport binding, and positive lifetime",
629 None,
630 None,
631 None,
632 ));
633 }
634 let expires_at_unix_ms = request
635 .issued_at_unix_ms
636 .checked_add(request.ttl_ms)
637 .ok_or_else(|| {
638 identity_error(
639 WorkloadIdentityErrorCode::InvalidRequest,
640 "invalid_credential_lifetime",
641 "Workload credential lifetime exceeds the supported timestamp range",
642 Some(request.service_principal.clone()),
643 None,
644 None,
645 )
646 })?;
647 let sequence = self.next_credential.fetch_add(1, Ordering::SeqCst);
648 let state = self.state.read().expect("identity provider lock poisoned");
649 let active_key_id = key_id(state.generation);
650 let credential_id = format!(
651 "sandbox:{}:{}:{sequence}",
652 state.generation, request.service_principal
653 );
654 let claims = WorkloadClaims {
655 iss: DEVELOPMENT_ISSUER.to_owned(),
656 sub: request.service_principal.clone(),
657 aud: vec![request.audience.clone()],
658 exp: expires_at_unix_ms.div_ceil(1_000),
659 iat: request.issued_at_unix_ms / 1_000,
660 jti: credential_id.clone(),
661 credential_kind: CREDENTIAL_KIND.to_owned(),
662 authenticated_transport_binding: request.authenticated_transport_binding,
663 expires_at_unix_ms,
664 };
665 let mut header = Header::new(Algorithm::HS256);
666 header.kid = Some(active_key_id.clone());
667 let token = encode(
668 &header,
669 &claims,
670 &EncodingKey::from_secret(state.secret.as_bytes()),
671 )
672 .map_err(|_| {
673 identity_error(
674 WorkloadIdentityErrorCode::InvalidProof,
675 "credential_signing_failed",
676 "The System Sandbox provider could not sign the Workload Identity credential",
677 Some(request.service_principal.clone()),
678 Some(credential_id.clone()),
679 Some(active_key_id.clone()),
680 )
681 })?;
682 Ok(WorkloadCredential {
683 token,
684 issuer: DEVELOPMENT_ISSUER.to_owned(),
685 service_principal: request.service_principal,
686 audience: request.audience,
687 expires_at_unix_ms,
688 credential_id,
689 key_id: active_key_id,
690 algorithm: "HS256-development-only".to_owned(),
691 })
692 }
693
694 fn verify(
695 &self,
696 token: &str,
697 verification: &WorkloadIdentityVerification,
698 ) -> Result<AuthenticatedServicePrincipal, WorkloadIdentityError> {
699 let header = decode_header(token).map_err(|_| invalid_proof())?;
700 let state = self.state.read().expect("identity provider lock poisoned");
701 let active_key_id = key_id(state.generation);
702 let credential_key_id = header.kid.ok_or_else(invalid_proof)?;
703 let (secret, stale) = if credential_key_id == active_key_id {
704 (state.secret.as_str(), false)
705 } else if let Some(secret) = state.retired_secrets.get(&credential_key_id) {
706 (secret.as_str(), true)
707 } else {
708 return Err(invalid_proof());
709 };
710 let mut validation = Validation::new(Algorithm::HS256);
711 validation.validate_exp = false;
712 validation.validate_aud = false;
713 validation.required_spec_claims.clear();
714 let claims = decode::<WorkloadClaims>(
715 token,
716 &DecodingKey::from_secret(secret.as_bytes()),
717 &validation,
718 )
719 .map_err(|_| invalid_proof())?
720 .claims;
721 let evidence = |outcome: &str| WorkloadIdentityEvidence {
722 outcome: outcome.to_owned(),
723 service_principal: Some(claims.sub.clone()),
724 credential_id: Some(claims.jti.clone()),
725 key_id: Some(credential_key_id.clone()),
726 };
727 if claims.credential_kind != CREDENTIAL_KIND {
728 return Err(WorkloadIdentityError {
729 code: WorkloadIdentityErrorCode::InvalidProof,
730 message: "Provider host tokens cannot be used as Workload Identity credentials"
731 .to_owned(),
732 evidence: evidence("credential_kind_mismatch"),
733 });
734 }
735 if claims.iss != DEVELOPMENT_ISSUER {
736 return Err(WorkloadIdentityError {
737 code: WorkloadIdentityErrorCode::IssuerMismatch,
738 message: "Workload Identity issuer does not match the configured provider"
739 .to_owned(),
740 evidence: evidence("issuer_mismatch"),
741 });
742 }
743 if stale {
744 return Err(WorkloadIdentityError {
745 code: WorkloadIdentityErrorCode::StaleCredential,
746 message:
747 "The Workload Identity credential was issued by a superseded System Sandbox key"
748 .to_owned(),
749 evidence: evidence("stale_credential"),
750 });
751 }
752 if !claims
753 .aud
754 .iter()
755 .any(|audience| audience == &verification.audience)
756 {
757 return Err(WorkloadIdentityError {
758 code: WorkloadIdentityErrorCode::AudienceMismatch,
759 message: "Workload Identity credential is not intended for this receiver"
760 .to_owned(),
761 evidence: evidence("audience_mismatch"),
762 });
763 }
764 if verification.now_unix_ms >= claims.expires_at_unix_ms {
765 return Err(WorkloadIdentityError {
766 code: WorkloadIdentityErrorCode::CredentialExpired,
767 message: "Workload Identity credential has expired".to_owned(),
768 evidence: evidence("credential_expired"),
769 });
770 }
771 if claims.authenticated_transport_binding != verification.authenticated_transport_binding {
772 return Err(WorkloadIdentityError {
773 code: WorkloadIdentityErrorCode::TransportBindingMismatch,
774 message: "Workload Identity proof is not bound to the authenticated transport"
775 .to_owned(),
776 evidence: evidence("transport_binding_mismatch"),
777 });
778 }
779 let authenticated_evidence = evidence("authenticated");
780 Ok(AuthenticatedServicePrincipal {
781 service_principal: claims.sub,
782 credential_id: claims.jti,
783 issuer: claims.iss,
784 audience: verification.audience.clone(),
785 expires_at_unix_ms: claims.expires_at_unix_ms,
786 key_id: credential_key_id,
787 algorithm: "HS256-development-only".to_owned(),
788 evidence: authenticated_evidence,
789 })
790 }
791}
792
793fn key_id(generation: u64) -> String {
794 format!("system-sandbox-key-{generation}")
795}
796
797fn valid_service_principal(value: &str) -> bool {
798 value.strip_prefix("service:").is_some_and(|service_id| {
799 !service_id.is_empty()
800 && service_id
801 .bytes()
802 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
803 })
804}
805
806fn invalid_service_principal() -> WorkloadIdentityError {
807 identity_error(
808 WorkloadIdentityErrorCode::InvalidRequest,
809 "invalid_service_principal",
810 "Workload Identity requires a stable Service Principal",
811 None,
812 None,
813 None,
814 )
815}
816
817fn service_principal_from_spiffe_id(spiffe_id: &SpiffeId) -> Option<String> {
818 let service_id = spiffe_id.path().strip_prefix("/service/")?;
819 let service_principal = format!("service:{service_id}");
820 (spiffe_id.path().split('/').count() == 3 && valid_service_principal(&service_principal))
821 .then_some(service_principal)
822}
823
824fn expiry_unix_ms(svid: &JwtSvid) -> Result<u64, WorkloadIdentityError> {
825 u64::try_from(svid.expiry().unix_timestamp())
826 .ok()
827 .and_then(|seconds| seconds.checked_mul(1_000))
828 .ok_or_else(|| {
829 identity_error(
830 WorkloadIdentityErrorCode::InvalidProof,
831 "invalid_credential_expiry",
832 "SPIFFE credential expiration is outside the supported timestamp range",
833 None,
834 None,
835 Some(svid.key_id().to_owned()),
836 )
837 })
838}
839
840fn credential_id(token: &str) -> String {
841 let digest = Sha256::digest(token.as_bytes());
842 format!("spiffe-jwt:{}", URL_SAFE_NO_PAD.encode(digest))
843}
844
845fn jwt_algorithm(token: &str) -> Result<String, WorkloadIdentityError> {
846 decode_header(token)
847 .map(|header| format!("{:?}", header.alg))
848 .map_err(|_| invalid_proof())
849}
850
851fn spiffe_verification_error(error: JwtSvidError) -> WorkloadIdentityError {
852 let (code, outcome, message) = match &error {
853 JwtSvidError::InvalidAudience(_, _) => (
854 WorkloadIdentityErrorCode::AudienceMismatch,
855 "audience_mismatch",
856 "Workload Identity credential is not intended for this receiver",
857 ),
858 JwtSvidError::InvalidToken(error) if matches!(error.kind(), ErrorKind::InvalidAudience) => {
859 (
860 WorkloadIdentityErrorCode::AudienceMismatch,
861 "audience_mismatch",
862 "Workload Identity credential is not intended for this receiver",
863 )
864 }
865 JwtSvidError::InvalidToken(error)
866 if matches!(error.kind(), ErrorKind::ExpiredSignature) =>
867 {
868 (
869 WorkloadIdentityErrorCode::CredentialExpired,
870 "credential_expired",
871 "Workload Identity credential has expired",
872 )
873 }
874 _ => (
875 WorkloadIdentityErrorCode::InvalidProof,
876 "invalid_proof",
877 "SPIFFE Workload Identity proof could not be verified",
878 ),
879 };
880 identity_error(code, outcome, message, None, None, None)
881}
882
883fn provider_unavailable(outcome: &str) -> WorkloadIdentityError {
884 identity_error(
885 WorkloadIdentityErrorCode::ProviderUnavailable,
886 outcome,
887 "SPIFFE Workload Identity material is unavailable",
888 None,
889 None,
890 None,
891 )
892}
893
894fn invalid_proof() -> WorkloadIdentityError {
895 identity_error(
896 WorkloadIdentityErrorCode::InvalidProof,
897 "invalid_proof",
898 "Workload Identity proof could not be verified",
899 None,
900 None,
901 None,
902 )
903}
904
905fn identity_error(
906 code: WorkloadIdentityErrorCode,
907 outcome: &str,
908 message: &str,
909 service_principal: Option<String>,
910 credential_id: Option<String>,
911 key_id: Option<String>,
912) -> WorkloadIdentityError {
913 WorkloadIdentityError {
914 code,
915 message: message.to_owned(),
916 evidence: WorkloadIdentityEvidence {
917 outcome: outcome.to_owned(),
918 service_principal,
919 credential_id,
920 key_id,
921 },
922 }
923}