1#![allow(clippy::result_large_err)]
11
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15
16use base64::Engine as _;
17use base64::engine::general_purpose::URL_SAFE_NO_PAD;
18use basil_proto::spiffe::spiffe_workload_api_server::SpiffeWorkloadApi;
19use basil_proto::spiffe::{
20 JwtBundlesRequest, JwtBundlesResponse, Jwtsvid, JwtsvidRequest, JwtsvidResponse,
21 ValidateJwtsvidRequest, ValidateJwtsvidResponse, X509BundlesRequest, X509BundlesResponse,
22 X509svid, X509svidRequest, X509svidResponse,
23};
24use futures::Stream;
25use std::collections::HashMap;
26use tonic::{Code, Request, Response, Status};
27
28use crate::catalog::policy::Op;
29use crate::catalog::{Class, Decision, DenyReason, KeyAlgorithm, KeyEntry};
30use crate::decision::DecisionRecord;
31use crate::event::BrokerEventKind;
32use crate::state::{BrokerState, Generation};
33use crate::transport::peer_from_request;
34
35type WorkloadResult<T> = Result<Response<T>, Status>;
36type BoxStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send + 'static>>;
37
38#[derive(Debug, Clone)]
40pub struct SpiffeWorkloadGrpc {
41 state: Arc<BrokerState>,
42}
43
44impl SpiffeWorkloadGrpc {
45 #[must_use]
47 pub const fn new(state: Arc<BrokerState>) -> Self {
48 Self { state }
49 }
50}
51
52#[tonic::async_trait]
53impl SpiffeWorkloadApi for SpiffeWorkloadGrpc {
54 type FetchX509SVIDStream = BoxStream<X509svidResponse>;
55 type FetchX509BundlesStream = BoxStream<X509BundlesResponse>;
56 type FetchJWTBundlesStream = BoxStream<JwtBundlesResponse>;
57
58 async fn fetch_x509svid(
59 &self,
60 request: Request<X509svidRequest>,
61 ) -> WorkloadResult<Self::FetchX509SVIDStream> {
62 require_workload_header(&request)?;
63 let peer = peer_from_request(&request);
64 let uid = peer.uid.ok_or_else(|| {
65 Status::new(
66 Code::Unauthenticated,
67 "missing peer credentials for FetchX509SVID",
68 )
69 })?;
70 let plan = self.x509_issue_plan(uid)?;
71 let state = Arc::clone(&self.state);
72 let rx = state.events().subscribe();
73 let stream = futures::stream::unfold(
74 (state, rx, plan, uid, false),
75 |(state, mut rx, plan, uid, emitted)| async move {
76 if !emitted {
77 let response = issue_x509_response(&state, uid, &plan).await;
78 return Some((response, (state, rx, plan, uid, true)));
79 }
80
81 let refresh =
82 tokio::time::sleep(Duration::from_secs(x509_refresh_after_secs(&plan)));
83 tokio::pin!(refresh);
84 loop {
85 tokio::select! {
86 () = &mut refresh => break,
87 event = rx.recv() => match event {
88 Ok(event) if x509_refresh_event(&plan, &event.kind) => break,
89 Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
90 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
91 }
92 }
93 }
94
95 let response = issue_x509_response(&state, uid, &plan).await;
96 Some((response, (state, rx, plan, uid, true)))
97 },
98 );
99 Ok(Response::new(Box::pin(stream)))
100 }
101
102 async fn fetch_x509_bundles(
103 &self,
104 request: Request<X509BundlesRequest>,
105 ) -> WorkloadResult<Self::FetchX509BundlesStream> {
106 require_workload_header(&request)?;
107 let plan = self.x509_bundle_plan()?;
108 let state = Arc::clone(&self.state);
109 let rx = state.events().subscribe();
110 let stream = futures::stream::unfold(
111 (state, rx, plan, false),
112 |(state, mut rx, plan, emitted)| async move {
113 if !emitted {
114 let response = x509_bundles_response(&state, &plan).await;
115 return Some((response, (state, rx, plan, true)));
116 }
117
118 loop {
119 match rx.recv().await {
120 Ok(event) if x509_bundle_refresh_event(&plan, &event.kind) => break,
121 Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
122 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
123 }
124 }
125
126 let response = x509_bundles_response(&state, &plan).await;
127 Some((response, (state, rx, plan, true)))
128 },
129 );
130 Ok(Response::new(Box::pin(stream)))
131 }
132
133 async fn fetch_jwtsvid(
134 &self,
135 request: Request<JwtsvidRequest>,
136 ) -> WorkloadResult<JwtsvidResponse> {
137 require_workload_header(&request)?;
138 let peer = peer_from_request(&request);
139 let uid = peer.uid.ok_or_else(|| {
140 Status::new(
141 Code::Unauthenticated,
142 "missing peer credentials for FetchJWTSVID",
143 )
144 })?;
145 let body = request.get_ref();
146 if body.audience.is_empty() || body.audience.iter().any(|aud| aud.trim().is_empty()) {
147 return Err(invalid_argument(
148 "FetchJWTSVID requires a non-empty audience",
149 ));
150 }
151 if !body.spiffe_id.is_empty() && !is_spiffe_id(body.spiffe_id.as_str()) {
152 return Err(invalid_argument("requested SPIFFE ID is malformed"));
153 }
154
155 let generation = Arc::clone(&self.state.load_generation());
161 let issuer = self.jwt_issuer(&generation, uid, body.spiffe_id.as_str())?;
162 let spiffe_id = requested_or_templated_spiffe_id(
163 &generation,
164 uid,
165 body.spiffe_id.as_str(),
166 issuer.entry,
167 )?;
168 let issuer_id = issuer
169 .entry
170 .labels
171 .get("spiffe_id")
172 .unwrap_or(issuer.name.as_str());
173 let alg = svid_alg(issuer.entry.key_type)?;
174
175 let mut svids = Vec::with_capacity(body.audience.len());
176 for audience in &body.audience {
177 let token = crate::minter::mint_svid(
178 issuer.backend,
179 &issuer.path,
180 issuer_id,
181 alg,
182 &spiffe_id,
183 audience,
184 Some(DEFAULT_JWT_SVID_TTL_SECS),
185 &serde_json::Value::Null,
186 )
187 .await
188 .map_err(|e| mint_status(&e))?;
189 svids.push(Jwtsvid {
190 spiffe_id: spiffe_id.clone(),
191 svid: token,
192 hint: String::new(),
193 });
194 }
195 Ok(Response::new(JwtsvidResponse { svids }))
196 }
197
198 async fn fetch_jwt_bundles(
199 &self,
200 request: Request<JwtBundlesRequest>,
201 ) -> WorkloadResult<Self::FetchJWTBundlesStream> {
202 require_workload_header(&request)?;
203 let plan = self.jwt_bundle_plan()?;
204 let state = Arc::clone(&self.state);
205 let rx = state.events().subscribe();
206 let stream = futures::stream::unfold(
207 (state, rx, plan, false),
208 |(state, mut rx, plan, emitted)| async move {
209 if !emitted {
210 let response = jwt_bundles_response(&state, &plan).await;
211 return Some((response, (state, rx, plan, true)));
212 }
213
214 loop {
215 match rx.recv().await {
216 Ok(event) if jwt_bundle_refresh_event(&plan, &event.kind) => break,
217 Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
218 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
219 }
220 }
221
222 let response = jwt_bundles_response(&state, &plan).await;
223 Some((response, (state, rx, plan, true)))
224 },
225 );
226 Ok(Response::new(Box::pin(stream)))
227 }
228
229 async fn validate_jwtsvid(
230 &self,
231 request: Request<ValidateJwtsvidRequest>,
232 ) -> WorkloadResult<ValidateJwtsvidResponse> {
233 require_workload_header(&request)?;
234 let peer = peer_from_request(&request);
235 let uid = peer.uid.ok_or_else(|| {
236 Status::new(
237 Code::Unauthenticated,
238 "missing peer credentials for ValidateJWTSVID",
239 )
240 })?;
241 let body = request.get_ref();
242 if body.audience.trim().is_empty() || body.svid.trim().is_empty() {
243 return Err(invalid_argument(
244 "ValidateJWTSVID requires a non-empty audience and SVID",
245 ));
246 }
247
248 let generation = Arc::clone(&self.state.load_generation());
249 let unverified = unverified_jwt_svid_claims(&body.svid)?;
250 let issuer = self.jwt_validation_issuer(&generation, uid, &unverified)?;
251 let validation = validate_jwt_svid(
252 issuer.backend,
253 &issuer.path,
254 issuer.entry,
255 &issuer.issuer_id,
256 &body.audience,
257 &body.svid,
258 )
259 .await?;
260 let trust_domain = issuer
261 .entry
262 .labels
263 .get("trust_domain")
264 .ok_or_else(validation_failed)?;
265 reject_revoked_jwtsvid(
266 self.state.jwt_revocations(),
267 trust_domain,
268 &validation.claims,
269 )?;
270 Ok(Response::new(ValidateJwtsvidResponse {
271 spiffe_id: validation.spiffe_id,
272 claims: Some(json_struct(validation.claims)),
273 }))
274 }
275}
276
277fn require_workload_header<T>(request: &Request<T>) -> Result<(), Status> {
278 let mut values = request.metadata().get_all("workload.spiffe.io").iter();
279 let valid = values
280 .next()
281 .and_then(|value| value.to_str().ok())
282 .is_some_and(|value| value == "true")
283 && values.next().is_none();
284 if valid {
285 return Ok(());
286 }
287
288 Err(Status::new(
289 Code::InvalidArgument,
290 "SPIFFE Workload API requests require workload.spiffe.io=true",
291 ))
292}
293
294const DEFAULT_JWT_SVID_TTL_SECS: u64 = 300;
295
296struct JwtIssuer<'a> {
297 name: String,
298 path: String,
299 entry: &'a KeyEntry,
300 backend: &'a dyn crate::backend::Backend,
301}
302
303struct JwtValidationIssuer<'a> {
304 path: String,
305 issuer_id: String,
306 entry: &'a KeyEntry,
307 backend: &'a dyn crate::backend::Backend,
308}
309
310struct ValidJwtSvid {
311 spiffe_id: String,
312 claims: serde_json::Value,
313}
314
315#[derive(Debug, Clone)]
316struct X509IssuePlan {
317 key_name: String,
318 spiffe_id: String,
319 trust_domain: String,
320 ttl_seconds: u64,
321}
322
323#[derive(Debug, Clone)]
324struct X509BundlePlan {
325 key_name: String,
326 trust_domain: String,
327}
328
329#[derive(Debug, Clone)]
330struct JwtBundlePlan {
331 key_name: String,
332 trust_domain: String,
333}
334
335impl SpiffeWorkloadGrpc {
336 fn x509_issue_plan(&self, uid: u32) -> Result<X509IssuePlan, Status> {
337 let generation = self.state.load_generation();
340 let actor = generation.pdp().resolve_unix_actor(uid).map_err(|_| {
341 Status::new(
342 Code::PermissionDenied,
343 "no configured subject for FetchX509SVID caller",
344 )
345 })?;
346 let mut saw_candidate = false;
347 for (name, entry) in &generation.catalog().keys {
348 if !is_x509_svid_issuer(entry) {
349 continue;
350 }
351 saw_candidate = true;
352 let decision = generation.pdp().decide(&actor, Op::Mint, name);
353 self.state
354 .record_decision(&DecisionRecord::from_actor_decision(
355 generation.id(),
356 &actor,
357 Op::Mint,
358 name,
359 &decision,
360 ));
361 if decision.is_deny() {
362 continue;
363 }
364 let spiffe_id = requested_or_templated_spiffe_id(&generation, uid, "", entry)?;
365 let trust_domain = entry
366 .labels
367 .get("trust_domain")
368 .ok_or_else(|| {
369 Status::new(
370 Code::Internal,
371 "X.509-SVID issuer has no trust_domain label",
372 )
373 })?
374 .to_string();
375 return Ok(X509IssuePlan {
376 key_name: name.clone(),
377 spiffe_id,
378 trust_domain,
379 ttl_seconds: self.state.limits().svid_ttl_secs.max(1),
380 });
381 }
382
383 let reason = if saw_candidate {
384 "not authorized to mint an X.509-SVID"
385 } else {
386 "no X.509-SVID issuer is configured"
387 };
388 self.state
389 .record_decision(&DecisionRecord::from_actor_decision(
390 generation.id(),
391 &actor,
392 Op::Mint,
393 "spiffe.x509_svid",
394 &Decision::Deny {
395 reason: DenyReason::NotPermitted,
396 },
397 ));
398 Err(Status::new(Code::PermissionDenied, reason))
399 }
400
401 fn jwt_issuer<'a>(
405 &'a self,
406 generation: &'a Generation,
407 uid: u32,
408 requested_spiffe_id: &str,
409 ) -> Result<JwtIssuer<'a>, Status> {
410 let actor = generation.pdp().resolve_unix_actor(uid).map_err(|_| {
411 Status::new(
412 Code::PermissionDenied,
413 "no configured subject for FetchJWTSVID caller",
414 )
415 })?;
416 let mut saw_candidate = false;
417 for (name, entry) in &generation.catalog().keys {
418 if !is_jwt_svid_issuer(entry) {
419 continue;
420 }
421 if !requested_spiffe_id.is_empty()
422 && !spiffe_id_matches_trust_domain(requested_spiffe_id, entry)
423 {
424 continue;
425 }
426 saw_candidate = true;
427 let decision = generation.pdp().decide(&actor, Op::Mint, name);
428 self.state
429 .record_decision(&DecisionRecord::from_actor_decision(
430 generation.id(),
431 &actor,
432 Op::Mint,
433 name,
434 &decision,
435 ));
436 if decision.is_deny() {
437 continue;
438 }
439 let routed = self
440 .state
441 .manager()
442 .resolve(name)
443 .map_err(|e| Status::new(Code::Internal, e.to_string()))?;
444 return Ok(JwtIssuer {
445 name: name.clone(),
446 path: routed.path().to_string(),
447 entry,
448 backend: routed.backend,
449 });
450 }
451
452 let reason = if saw_candidate {
453 "not authorized to mint a JWT-SVID"
454 } else {
455 "no JWT-SVID issuer matches the requested SPIFFE ID"
456 };
457 self.state
458 .record_decision(&DecisionRecord::from_actor_decision(
459 generation.id(),
460 &actor,
461 Op::Mint,
462 "spiffe.jwt_svid",
463 &Decision::Deny {
464 reason: DenyReason::NotPermitted,
465 },
466 ));
467 Err(Status::new(Code::PermissionDenied, reason))
468 }
469
470 fn jwt_validation_issuer<'a>(
471 &'a self,
472 generation: &'a Generation,
473 uid: u32,
474 claims: &serde_json::Value,
475 ) -> Result<JwtValidationIssuer<'a>, Status> {
476 let actor = generation.pdp().resolve_unix_actor(uid).map_err(|_| {
477 Status::new(
478 Code::PermissionDenied,
479 "no configured subject for ValidateJWTSVID caller",
480 )
481 })?;
482 let iss = claims
483 .get("iss")
484 .and_then(serde_json::Value::as_str)
485 .ok_or_else(validation_failed)?;
486 let sub = claims
487 .get("sub")
488 .and_then(serde_json::Value::as_str)
489 .ok_or_else(validation_failed)?;
490 if !is_spiffe_id(sub) {
491 return Err(validation_failed());
492 }
493
494 let mut saw_candidate = false;
495 for (name, entry) in &generation.catalog().keys {
496 if !is_jwt_svid_issuer(entry) || !spiffe_id_matches_trust_domain(sub, entry) {
497 continue;
498 }
499 let issuer_id = entry
500 .labels
501 .get("spiffe_id")
502 .map_or_else(|| name.as_str(), |value| value);
503 if issuer_id != iss {
506 continue;
507 }
508 saw_candidate = true;
509 let decision = generation.pdp().decide(&actor, Op::Validate, name);
510 self.state
511 .record_decision(&DecisionRecord::from_actor_decision(
512 generation.id(),
513 &actor,
514 Op::Validate,
515 name,
516 &decision,
517 ));
518 if decision.is_deny() {
519 continue;
520 }
521 let routed = self
522 .state
523 .manager()
524 .resolve(name)
525 .map_err(|e| Status::new(Code::Internal, e.to_string()))?;
526 return Ok(JwtValidationIssuer {
527 path: routed.path().to_string(),
528 issuer_id: issuer_id.to_string(),
529 entry,
530 backend: routed.backend,
531 });
532 }
533
534 if saw_candidate {
535 return Err(Status::new(
536 Code::PermissionDenied,
537 "not authorized to validate a JWT-SVID",
538 ));
539 }
540 Err(validation_failed())
541 }
542
543 fn x509_bundle_plan(&self) -> Result<Vec<X509BundlePlan>, Status> {
544 let generation = self.state.load_generation();
545 let plans: Vec<_> = generation
546 .catalog()
547 .keys
548 .iter()
549 .filter(|(_, entry)| is_x509_svid_issuer(entry))
550 .filter_map(|(name, entry)| {
551 entry
552 .labels
553 .get("trust_domain")
554 .map(|trust_domain| X509BundlePlan {
555 key_name: name.clone(),
556 trust_domain: trust_domain.to_string(),
557 })
558 })
559 .collect();
560 if plans.is_empty() {
561 Err(Status::new(
562 Code::FailedPrecondition,
563 "no X.509 bundle publisher is configured",
564 ))
565 } else {
566 Ok(plans)
567 }
568 }
569
570 fn jwt_bundle_plan(&self) -> Result<Vec<JwtBundlePlan>, Status> {
571 let generation = self.state.load_generation();
572 let plans: Vec<_> = generation
573 .catalog()
574 .keys
575 .iter()
576 .filter(|(_, entry)| is_jwt_svid_issuer(entry))
577 .filter_map(|(name, entry)| {
578 entry
579 .labels
580 .get("trust_domain")
581 .map(|trust_domain| JwtBundlePlan {
582 key_name: name.clone(),
583 trust_domain: trust_domain.to_string(),
584 })
585 })
586 .collect();
587 if plans.is_empty() {
588 Err(Status::new(
589 Code::FailedPrecondition,
590 "no JWT bundle publisher is configured",
591 ))
592 } else {
593 Ok(plans)
594 }
595 }
596}
597
598async fn issue_x509_response(
599 state: &BrokerState,
600 _uid: u32,
601 plan: &X509IssuePlan,
602) -> Result<X509svidResponse, Status> {
603 let issued = state
604 .manager()
605 .issue_x509_svid(&plan.key_name, &plan.spiffe_id, plan.ttl_seconds)
606 .await
607 .map_err(|err| x509_issue_status(&err))?;
608 Ok(X509svidResponse {
609 svids: vec![X509svid {
610 spiffe_id: plan.spiffe_id.clone(),
611 x509_svid: issued.cert_chain_der.concat(),
612 x509_svid_key: issued.leaf_private_key_der.to_vec(),
613 bundle: issued.bundle_der.concat(),
614 hint: String::new(),
615 }],
616 crl: Vec::new(),
617 federated_bundles: std::collections::HashMap::default(),
618 })
619}
620
621fn x509_refresh_after_secs(plan: &X509IssuePlan) -> u64 {
622 (plan.ttl_seconds / 2).max(1)
623}
624
625fn x509_refresh_event(plan: &X509IssuePlan, kind: &BrokerEventKind) -> bool {
626 match kind {
627 BrokerEventKind::KeyRotated { key_id, .. } => {
629 key_id == &plan.key_name
631 }
632 BrokerEventKind::BundleChanged { trust_domain }
633 | BrokerEventKind::Revoked { trust_domain, .. } => {
634 trust_domain == &plan.trust_domain
636 }
637 }
638}
639
640fn x509_bundle_refresh_event(plans: &[X509BundlePlan], kind: &BrokerEventKind) -> bool {
641 plans.iter().any(|plan| match kind {
642 BrokerEventKind::KeyRotated { key_id, .. } => {
643 key_id == &plan.key_name
645 }
646 BrokerEventKind::BundleChanged { trust_domain }
647 | BrokerEventKind::Revoked { trust_domain, .. } => {
648 trust_domain == &plan.trust_domain
650 }
651 })
652}
653
654fn jwt_bundle_refresh_event(plans: &[JwtBundlePlan], kind: &BrokerEventKind) -> bool {
655 plans.iter().any(|plan| match kind {
656 BrokerEventKind::KeyRotated { key_id, .. } => {
657 key_id == &plan.key_name
659 }
660 BrokerEventKind::BundleChanged { trust_domain }
661 | BrokerEventKind::Revoked { trust_domain, .. } => {
662 trust_domain == &plan.trust_domain
664 }
665 })
666}
667
668async fn x509_bundles_response(
669 state: &BrokerState,
670 plans: &[X509BundlePlan],
671) -> Result<X509BundlesResponse, Status> {
672 let mut bundles = HashMap::new();
673 let mut crl = Vec::new();
674 for plan in plans {
675 let routed = state
676 .manager()
677 .resolve(&plan.key_name)
678 .map_err(|e| Status::new(Code::Internal, e.to_string()))?;
679 let bundle = routed
680 .backend
681 .x509_bundle(routed.path())
682 .await
683 .map_err(|_| upstream_unavailable())?;
684 bundles.insert(
685 format!("spiffe://{}", plan.trust_domain),
686 bundle.bundle_der.concat(),
687 );
688 if !bundle.crl_der.is_empty() {
689 crl.push(bundle.crl_der);
690 }
691 }
692 Ok(X509BundlesResponse { crl, bundles })
693}
694
695async fn jwt_bundles_response(
696 state: &BrokerState,
697 plans: &[JwtBundlePlan],
698) -> Result<JwtBundlesResponse, Status> {
699 let mut bundles = HashMap::new();
700 for plan in plans {
701 let routed = state
702 .manager()
703 .resolve(&plan.key_name)
704 .map_err(|e| Status::new(Code::Internal, e.to_string()))?;
705 let alg = svid_alg(routed.entry.key_type)?;
706 let limits = state.limits();
711 let jwks =
712 crate::minter::jwt_svid_jwks_grace(routed.backend, routed.path(), alg, |latest| {
713 limits.grace_floor(latest)
714 })
715 .await
716 .map_err(|_| upstream_unavailable())?;
717 bundles.insert(format!("spiffe://{}", plan.trust_domain), jwks);
718 }
719 Ok(JwtBundlesResponse { bundles })
720}
721
722async fn validate_jwt_svid(
723 backend: &dyn crate::backend::Backend,
724 key_path: &str,
725 entry: &KeyEntry,
726 issuer_id: &str,
727 audience: &str,
728 token: &str,
729) -> Result<ValidJwtSvid, Status> {
730 let alg = svid_alg(entry.key_type)?;
731 let public_key = backend
732 .public_key(key_path)
733 .await
734 .map_err(|_| validation_failed())?;
735 let decoding_key = decoding_key(&public_key, alg)?;
736 let algorithm = jwt_algorithm(alg);
737 let mut validation = jsonwebtoken::Validation::new(algorithm);
738 validation.set_required_spec_claims(&["exp", "iss", "sub", "aud"]);
739 validation.set_issuer(&[issuer_id]);
740 validation.set_audience(&[audience]);
741 let token_data = jsonwebtoken::decode::<serde_json::Value>(token, &decoding_key, &validation)
742 .map_err(|_| validation_failed())?;
743 let spiffe_id = token_data
744 .claims
745 .get("sub")
746 .and_then(serde_json::Value::as_str)
747 .filter(|sub| is_spiffe_id(sub) && spiffe_id_matches_trust_domain(sub, entry))
748 .ok_or_else(validation_failed)?
749 .to_string();
750 Ok(ValidJwtSvid {
751 spiffe_id,
752 claims: token_data.claims,
753 })
754}
755
756fn unverified_jwt_svid_claims(token: &str) -> Result<serde_json::Value, Status> {
757 let mut parts = token.split('.');
758 let _header = parts.next().ok_or_else(validation_failed)?;
759 let claims = parts.next().ok_or_else(validation_failed)?;
760 let _signature = parts.next().ok_or_else(validation_failed)?;
761 if parts.next().is_some() {
762 return Err(validation_failed());
763 }
764 let bytes = URL_SAFE_NO_PAD
765 .decode(claims)
766 .map_err(|_| validation_failed())?;
767 serde_json::from_slice(&bytes).map_err(|_| validation_failed())
768}
769
770fn reject_revoked_jwtsvid(
771 store: &crate::revocation::JwtRevocationStore,
772 trust_domain: &str,
773 claims: &serde_json::Value,
774) -> Result<(), Status> {
775 let Some(jti) = claims.get("jti").and_then(serde_json::Value::as_str) else {
776 return Ok(());
777 };
778 if store.is_revoked(trust_domain, jti) {
779 return Err(validation_failed());
780 }
781 Ok(())
782}
783
784fn decoding_key(
785 public_key: &[u8],
786 alg: crate::minter::SvidAlg,
787) -> Result<jsonwebtoken::DecodingKey, Status> {
788 match alg {
789 crate::minter::SvidAlg::EdDsa if public_key.len() == 32 => {
790 Ok(jsonwebtoken::DecodingKey::from_ed_der(public_key))
791 }
792 crate::minter::SvidAlg::EdDsa => Err(validation_failed()),
793 crate::minter::SvidAlg::Rs256 => {
794 if let Ok(pem) = std::str::from_utf8(public_key)
795 && pem.trim_start().starts_with("-----BEGIN ")
796 {
797 return jsonwebtoken::DecodingKey::from_rsa_pem(public_key)
798 .map_err(|_| validation_failed());
799 }
800 Ok(jsonwebtoken::DecodingKey::from_rsa_der(public_key))
801 }
802 crate::minter::SvidAlg::Es256 => {
803 if let Ok(pem) = std::str::from_utf8(public_key)
804 && pem.trim_start().starts_with("-----BEGIN ")
805 {
806 return jsonwebtoken::DecodingKey::from_ec_pem(public_key)
807 .map_err(|_| validation_failed());
808 }
809 Ok(jsonwebtoken::DecodingKey::from_ec_der(public_key))
810 }
811 crate::minter::SvidAlg::Es384 => {
812 if let Ok(pem) = std::str::from_utf8(public_key)
813 && pem.trim_start().starts_with("-----BEGIN ")
814 {
815 return jsonwebtoken::DecodingKey::from_ec_pem(public_key)
816 .map_err(|_| validation_failed());
817 }
818 Ok(jsonwebtoken::DecodingKey::from_ec_der(public_key))
819 }
820 }
821}
822
823const fn jwt_algorithm(alg: crate::minter::SvidAlg) -> jsonwebtoken::Algorithm {
824 match alg {
825 crate::minter::SvidAlg::EdDsa => jsonwebtoken::Algorithm::EdDSA,
826 crate::minter::SvidAlg::Rs256 => jsonwebtoken::Algorithm::RS256,
827 crate::minter::SvidAlg::Es256 => jsonwebtoken::Algorithm::ES256,
828 crate::minter::SvidAlg::Es384 => jsonwebtoken::Algorithm::ES384,
829 }
830}
831
832fn is_x509_svid_issuer(entry: &KeyEntry) -> bool {
833 entry.class == Class::Asymmetric
834 && entry.labels.get("svid_kind") == Some("x509")
835 && entry.labels.get("trust_domain").is_some()
836}
837
838fn is_jwt_svid_issuer(entry: &KeyEntry) -> bool {
839 entry.class == Class::Asymmetric
841 && entry.labels.get("svid_kind") == Some("jwt")
843 && entry.labels.get("trust_domain").is_some()
844 && entry
851 .key_type
852 .is_some_and(KeyAlgorithm::is_spiffe_jwt_svid_profile)
853}
854
855fn requested_or_templated_spiffe_id(
856 generation: &Generation,
857 uid: u32,
858 requested: &str,
859 entry: &KeyEntry,
860) -> Result<String, Status> {
861 if !requested.is_empty() {
862 if !is_spiffe_id(requested) || !spiffe_id_matches_trust_domain(requested, entry) {
863 return Err(invalid_argument(
864 "requested SPIFFE ID is malformed or out of trust domain",
865 ));
866 }
867 return Ok(requested.to_string());
868 }
869
870 let trust_domain = entry
871 .labels
872 .get("trust_domain")
873 .ok_or_else(|| Status::new(Code::Internal, "JWT-SVID issuer has no trust_domain label"))?;
874 let segment = generation
875 .config()
876 .names
877 .users
878 .get(&uid)
879 .map_or_else(|| uid.to_string(), std::string::ToString::to_string);
880 let id = format!("spiffe://{trust_domain}/{segment}");
881 if is_spiffe_id(&id) {
882 Ok(id)
883 } else {
884 Err(Status::new(
885 Code::Internal,
886 "templated SPIFFE ID is malformed",
887 ))
888 }
889}
890
891fn spiffe_id_matches_trust_domain(spiffe_id: &str, entry: &KeyEntry) -> bool {
892 entry
893 .labels
894 .get("trust_domain")
895 .is_some_and(|trust_domain| {
896 spiffe_id
897 .strip_prefix("spiffe://")
898 .and_then(|rest| rest.split_once('/'))
899 .is_some_and(|(td, path)| td == trust_domain && !path.is_empty())
902 })
903}
904
905fn svid_alg(key_type: Option<KeyAlgorithm>) -> Result<crate::minter::SvidAlg, Status> {
916 match key_type {
917 Some(KeyAlgorithm::Rsa2048) => Ok(crate::minter::SvidAlg::Rs256),
918 Some(KeyAlgorithm::EcdsaP256) => Ok(crate::minter::SvidAlg::Es256),
919 Some(KeyAlgorithm::EcdsaP384) => Ok(crate::minter::SvidAlg::Es384),
920 _ => Err(Status::new(
925 Code::FailedPrecondition,
926 "JWT-SVID issuer key cannot sign JWT-SVIDs",
927 )),
928 }
929}
930
931fn is_spiffe_id(id: &str) -> bool {
932 let Some(rest) = id.strip_prefix("spiffe://") else {
933 return false;
934 };
935 let Some((trust_domain, path)) = rest.split_once('/') else {
936 return false;
937 };
938 is_valid_spiffe_part(trust_domain) && is_valid_spiffe_part(path)
939}
940
941fn is_valid_spiffe_part(part: &str) -> bool {
942 !part.is_empty() && !part.chars().any(char::is_whitespace)
943}
944
945fn invalid_argument(message: &'static str) -> Status {
946 Status::new(Code::InvalidArgument, message)
947}
948
949fn validation_failed() -> Status {
950 invalid_argument("JWT-SVID validation failed")
951}
952
953fn upstream_unavailable() -> Status {
954 Status::new(Code::Unavailable, "backend unavailable")
955}
956
957fn json_struct(value: serde_json::Value) -> prost_types::Struct {
958 let serde_json::Value::Object(fields) = value else {
959 return prost_types::Struct::default();
960 };
961 prost_types::Struct {
962 fields: fields
963 .into_iter()
964 .map(|(key, value)| (key, json_value(value)))
965 .collect(),
966 }
967}
968
969fn json_value(value: serde_json::Value) -> prost_types::Value {
970 let kind = match value {
971 serde_json::Value::Null => prost_types::value::Kind::NullValue(0),
972 serde_json::Value::Bool(value) => prost_types::value::Kind::BoolValue(value),
973 serde_json::Value::Number(value) => {
974 prost_types::value::Kind::NumberValue(value.as_f64().unwrap_or(0.0))
975 }
976 serde_json::Value::String(value) => prost_types::value::Kind::StringValue(value),
977 serde_json::Value::Array(values) => {
978 prost_types::value::Kind::ListValue(prost_types::ListValue {
979 values: values.into_iter().map(json_value).collect(),
980 })
981 }
982 serde_json::Value::Object(_) => prost_types::value::Kind::StructValue(json_struct(value)),
983 };
984 prost_types::Value { kind: Some(kind) }
985}
986
987fn mint_status(err: &crate::minter::GenericMintError) -> Status {
988 match err {
989 crate::minter::GenericMintError::Reserved(e) => {
990 Status::new(Code::InvalidArgument, e.to_string())
991 }
992 crate::minter::GenericMintError::Backend(_) => upstream_unavailable(),
993 }
994}
995
996fn x509_issue_status(err: &crate::manager::ManagerError) -> Status {
997 match err {
998 crate::manager::ManagerError::UnknownKey(_) => {
999 Status::new(Code::PermissionDenied, "not authorized")
1000 }
1001 crate::manager::ManagerError::Unsupported(_)
1002 | crate::manager::ManagerError::OpNotValidForClass { .. }
1003 | crate::manager::ManagerError::UnsupportedKeyType { .. } => {
1004 Status::new(Code::FailedPrecondition, err.to_string())
1005 }
1006 crate::manager::ManagerError::Backend(_) => upstream_unavailable(),
1007 crate::manager::ManagerError::UnknownBackend { .. }
1008 | crate::manager::ManagerError::AlgorithmMismatch { .. }
1009 | crate::manager::ManagerError::KemAlgorithmMismatch { .. }
1010 | crate::manager::ManagerError::ValueRotateNeedsSet(_)
1011 | crate::manager::ManagerError::Sealing(_)
1016 | crate::manager::ManagerError::Signing(_)
1017 | crate::manager::ManagerError::Provider(_)
1018 | crate::manager::ManagerError::UnsealContextNotPermitted(_)
1021 | crate::manager::ManagerError::MissingPublicPath(_) => {
1022 Status::new(Code::Internal, err.to_string())
1023 }
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030 use std::collections::BTreeMap;
1031
1032 use async_trait::async_trait;
1033 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
1034
1035 use crate::backend::{Backend, BackendError, NewKey, X509Bundle, X509Svid};
1036 use crate::catalog::loader::load;
1037 use crate::manager::BackendManager;
1038 use crate::peer::PeerInfo;
1039 use crate::state::{BrokerLimits, DEFAULT_SVID_TTL_SECS};
1040
1041 const CATALOG: &str = r#"{
1042 "schemaVersion": 1,
1043 "backends": { "bao": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
1044 "keys": {
1045 "spire.jwt": {
1046 "class": "asymmetric", "keyType": "rsa-2048", "backend": "bao",
1047 "path": "jwt-issuer", "writable": false, "missing": "error",
1048 "labels": ["svid_kind=jwt", "trust_domain=example.org", "spiffe_id=spiffe://example.org/basil"],
1049 "description": "JWT-SVID issuer"
1050 },
1051 "spire.x509": {
1052 "class": "asymmetric", "keyType": "ed25519", "backend": "bao",
1053 "engine": "pki", "path": "pki/issue/workload", "writable": false, "missing": "error",
1054 "labels": ["svid_kind=x509", "trust_domain=example.org"],
1055 "description": "X.509-SVID issuer"
1056 }
1057 }
1058 }"#;
1059
1060 const POLICY: &str = r#"{
1061 "schemaVersion": 2,
1062 "subjects": {
1063 "svc.api": { "allOf": [ { "kind": "unix", "uid": 9100 } ] }
1064 },
1065 "roles": { "minter": ["mint"], "validator": ["validate"] },
1066 "rules": [
1067 { "id": "allow-svc-jwt", "subjects": ["svc.api"], "action": ["role:minter"], "target": ["spire.jwt"] },
1068 { "id": "allow-svc-x509", "subjects": ["svc.api"], "action": ["role:minter"], "target": ["spire.x509"] },
1069 { "id": "allow-svc-validate", "subjects": ["svc.api"], "action": ["role:validator"], "target": ["spire.jwt"] }
1070 ],
1071 "config": {
1072 "names": { "users": { "9100": "svc-api" }, "groups": {} },
1073 "memberships": {}
1074 }
1075 }"#;
1076
1077 #[derive(Default)]
1078 struct JwtBackend {
1079 sign_calls: std::sync::atomic::AtomicUsize,
1080 x509_calls: std::sync::Mutex<Vec<String>>,
1081 expected_x509_ttl: std::sync::atomic::AtomicU64,
1082 }
1083
1084 #[async_trait]
1085 impl Backend for JwtBackend {
1086 fn kind(&self) -> &'static str {
1087 "jwt-test"
1088 }
1089
1090 async fn new_key(&self, key_type: basil_proto::KeyType) -> Result<NewKey, BackendError> {
1091 let _ = key_type;
1092 Err(BackendError::Unsupported("new_key"))
1093 }
1094
1095 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
1096 let _ = key_id;
1097 Ok(test_public_key())
1098 }
1099
1100 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1101 let _ = key_id;
1102 self.sign_calls
1103 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1104 test_rs256_sign(message)
1105 }
1106
1107 async fn sign_with_options(
1108 &self,
1109 key_id: &str,
1110 message: &[u8],
1111 options: crate::backend::SignOptions,
1112 ) -> Result<Vec<u8>, BackendError> {
1113 let _ = key_id;
1114 if options != crate::backend::SignOptions::Rs256Pkcs1v15Sha256 {
1116 return Err(BackendError::Unsupported("jwt-test sign options"));
1117 }
1118 self.sign_calls
1119 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1120 test_rs256_sign(message)
1121 }
1122
1123 async fn verify(
1124 &self,
1125 key_id: &str,
1126 message: &[u8],
1127 signature: &[u8],
1128 ) -> Result<bool, BackendError> {
1129 let _ = (key_id, message, signature);
1130 Err(BackendError::Unsupported("verify"))
1131 }
1132
1133 async fn issue_x509_svid(
1134 &self,
1135 key_id: &str,
1136 spiffe_id: &str,
1137 ttl_seconds: u64,
1138 ) -> Result<X509Svid, BackendError> {
1139 assert_eq!(key_id, "pki/issue/workload");
1140 assert_eq!(
1141 ttl_seconds,
1142 self.expected_x509_ttl
1143 .load(std::sync::atomic::Ordering::SeqCst)
1144 );
1145 self.x509_calls
1146 .lock()
1147 .expect("calls lock")
1148 .push(spiffe_id.to_string());
1149 Ok(X509Svid {
1150 cert_chain_der: vec![b"leaf".to_vec(), b"issuer".to_vec()],
1151 leaf_private_key_der: zeroize::Zeroizing::new(b"private-key".to_vec()),
1152 bundle_der: vec![b"bundle".to_vec()],
1153 })
1154 }
1155
1156 async fn x509_bundle(&self, key_id: &str) -> Result<X509Bundle, BackendError> {
1157 assert_eq!(key_id, "pki/issue/workload");
1158 Ok(X509Bundle {
1159 bundle_der: vec![b"bundle".to_vec(), b"issuer".to_vec()],
1160 crl_der: b"crl".to_vec(),
1161 })
1162 }
1163 }
1164
1165 fn service() -> (SpiffeWorkloadGrpc, Arc<JwtBackend>) {
1166 service_with_limits(BrokerLimits::default())
1167 }
1168
1169 fn service_with_limits(limits: BrokerLimits) -> (SpiffeWorkloadGrpc, Arc<JwtBackend>) {
1170 let (catalog, policy, config, warnings) = load(CATALOG, POLICY).expect("fixture loads");
1171 assert!(warnings.is_empty());
1172 let backend = Arc::new(JwtBackend::default());
1173 backend.expected_x509_ttl.store(
1174 limits.svid_ttl_secs.max(1),
1175 std::sync::atomic::Ordering::SeqCst,
1176 );
1177 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
1178 backends.insert("bao".into(), Box::new(TestBackend(backend.clone())));
1179 let manager = BackendManager::new(catalog.clone(), backends).expect("manager builds");
1180 let state = Arc::new(BrokerState::with_limits(
1181 catalog, policy, config, manager, "jwt-test", limits,
1182 ));
1183 (SpiffeWorkloadGrpc::new(state), backend)
1184 }
1185
1186 struct TestBackend(Arc<JwtBackend>);
1187
1188 #[async_trait]
1189 impl Backend for TestBackend {
1190 fn kind(&self) -> &'static str {
1191 self.0.kind()
1192 }
1193
1194 async fn new_key(&self, key_type: basil_proto::KeyType) -> Result<NewKey, BackendError> {
1195 self.0.new_key(key_type).await
1196 }
1197
1198 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
1199 self.0.public_key(key_id).await
1200 }
1201
1202 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1203 self.0.sign(key_id, message).await
1204 }
1205
1206 async fn sign_with_options(
1207 &self,
1208 key_id: &str,
1209 message: &[u8],
1210 options: crate::backend::SignOptions,
1211 ) -> Result<Vec<u8>, BackendError> {
1212 self.0.sign_with_options(key_id, message, options).await
1213 }
1214
1215 async fn verify(
1216 &self,
1217 key_id: &str,
1218 message: &[u8],
1219 signature: &[u8],
1220 ) -> Result<bool, BackendError> {
1221 self.0.verify(key_id, message, signature).await
1222 }
1223
1224 async fn issue_x509_svid(
1225 &self,
1226 key_id: &str,
1227 spiffe_id: &str,
1228 ttl_seconds: u64,
1229 ) -> Result<X509Svid, BackendError> {
1230 self.0.issue_x509_svid(key_id, spiffe_id, ttl_seconds).await
1231 }
1232
1233 async fn x509_bundle(&self, key_id: &str) -> Result<X509Bundle, BackendError> {
1234 self.0.x509_bundle(key_id).await
1235 }
1236 }
1237
1238 fn jwt_request(uid: u32, spiffe_id: &str, audience: Vec<&str>) -> Request<JwtsvidRequest> {
1239 let mut request = Request::new(JwtsvidRequest {
1240 audience: audience.into_iter().map(str::to_string).collect(),
1241 spiffe_id: spiffe_id.to_string(),
1242 });
1243 request
1244 .metadata_mut()
1245 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
1246 request.extensions_mut().insert(PeerInfo {
1247 uid: Some(uid),
1248 ..PeerInfo::default()
1249 });
1250 request
1251 }
1252
1253 fn x509_request(uid: u32) -> Request<X509svidRequest> {
1254 let mut request = Request::new(X509svidRequest {});
1255 request
1256 .metadata_mut()
1257 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
1258 request.extensions_mut().insert(PeerInfo {
1259 uid: Some(uid),
1260 ..PeerInfo::default()
1261 });
1262 request
1263 }
1264
1265 fn x509_bundles_request() -> Request<X509BundlesRequest> {
1266 let mut request = Request::new(X509BundlesRequest {});
1267 request
1268 .metadata_mut()
1269 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
1270 request
1271 }
1272
1273 fn jwt_bundles_request() -> Request<JwtBundlesRequest> {
1274 let mut request = Request::new(JwtBundlesRequest {});
1275 request
1276 .metadata_mut()
1277 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
1278 request
1279 }
1280
1281 fn validate_request(uid: u32, audience: &str, svid: String) -> Request<ValidateJwtsvidRequest> {
1282 let mut request = Request::new(ValidateJwtsvidRequest {
1283 audience: audience.to_string(),
1284 svid,
1285 });
1286 request
1287 .metadata_mut()
1288 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
1289 request.extensions_mut().insert(PeerInfo {
1290 uid: Some(uid),
1291 ..PeerInfo::default()
1292 });
1293 request
1294 }
1295
1296 fn token_claims(token: &str) -> serde_json::Value {
1297 let mut parts = token.split('.');
1298 let _header = parts.next().expect("header");
1299 let claims = parts.next().expect("claims");
1300 let bytes = URL_SAFE_NO_PAD.decode(claims).expect("claims b64");
1301 serde_json::from_slice(&bytes).expect("claims json")
1302 }
1303
1304 fn test_issuer_key() -> &'static rsa::RsaPrivateKey {
1309 use std::sync::LazyLock;
1310 static KEY: LazyLock<rsa::RsaPrivateKey> = LazyLock::new(|| {
1311 let mut rng = rand::thread_rng();
1312 rsa::RsaPrivateKey::new(&mut rng, 2048).expect("test rsa keygen")
1313 });
1314 &KEY
1315 }
1316
1317 fn test_public_key() -> Vec<u8> {
1320 use rsa::pkcs8::{EncodePublicKey, LineEnding};
1321 rsa::RsaPublicKey::from(test_issuer_key())
1322 .to_public_key_pem(LineEnding::LF)
1323 .expect("spki pem")
1324 .into_bytes()
1325 }
1326
1327 fn test_rs256_sign(input: &[u8]) -> Result<Vec<u8>, BackendError> {
1331 use rsa::pkcs1::EncodeRsaPrivateKey;
1332 let private_pem = test_issuer_key()
1333 .to_pkcs1_pem(rsa::pkcs8::LineEnding::LF)
1334 .expect("pkcs1 pem");
1335 let encoding_key =
1336 jsonwebtoken::EncodingKey::from_rsa_pem(private_pem.as_bytes()).expect("encoding key");
1337 let b64 = jsonwebtoken::crypto::sign(input, &encoding_key, jsonwebtoken::Algorithm::RS256)
1338 .map_err(|e| BackendError::Backend(e.to_string()))?;
1339 URL_SAFE_NO_PAD
1340 .decode(b64)
1341 .map_err(|e| BackendError::Backend(e.to_string()))
1342 }
1343
1344 fn valid_jwt_svid(audience: &str, expires_at: u64) -> String {
1345 use rsa::pkcs1::EncodeRsaPrivateKey;
1346 let claims = serde_json::json!({
1347 "iss": "spiffe://example.org/basil",
1348 "sub": "spiffe://example.org/svc-api",
1349 "aud": audience,
1350 "iat": expires_at.saturating_sub(60),
1351 "exp": expires_at,
1352 "jti": "test-jti",
1353 "role": "api",
1354 });
1355 let private_pem = test_issuer_key()
1356 .to_pkcs1_pem(rsa::pkcs8::LineEnding::LF)
1357 .expect("pkcs1 pem");
1358 jsonwebtoken::encode(
1359 &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256),
1360 &claims,
1361 &jsonwebtoken::EncodingKey::from_rsa_pem(private_pem.as_bytes()).expect("private key"),
1362 )
1363 .expect("token signs")
1364 }
1365
1366 #[test]
1367 fn workload_header_requires_true() {
1368 let request = Request::new(X509svidRequest {});
1369 let status = require_workload_header(&request).expect_err("missing header is rejected");
1370 assert_eq!(status.code(), Code::InvalidArgument);
1371
1372 let mut request = Request::new(X509svidRequest {});
1373 request.metadata_mut().insert(
1374 "workload.spiffe.io",
1375 "false".parse().expect("valid metadata"),
1376 );
1377 let status = require_workload_header(&request).expect_err("false header is rejected");
1378 assert_eq!(status.code(), Code::InvalidArgument);
1379
1380 request.metadata_mut().insert(
1381 "workload.spiffe.io",
1382 "true".parse().expect("valid metadata"),
1383 );
1384 require_workload_header(&request).expect("true header is accepted");
1385 }
1386
1387 #[test]
1388 fn workload_header_rejects_duplicates_binary_and_malformed_values() {
1389 fn request_with_values(values: &[&str]) -> Request<X509svidRequest> {
1390 let mut request = Request::new(X509svidRequest {});
1391 for value in values {
1392 request.metadata_mut().append(
1393 "workload.spiffe.io",
1394 value.parse().expect("valid metadata value"),
1395 );
1396 }
1397 request
1398 }
1399
1400 const SECRET_METADATA: &str = "Authorization: Bearer vault-token-s.123";
1401 for values in [
1402 &["true", "true"][..],
1403 &["false", "true"],
1404 &["true", "false"],
1405 &["TRUE"],
1406 &["True"],
1407 &[" true"],
1408 &["true "],
1409 &["\ttrue"],
1410 &[SECRET_METADATA],
1411 &[&"a".repeat(16 * 1024)],
1412 ] {
1413 let status = require_workload_header(&request_with_values(values))
1414 .expect_err("malformed workload header is rejected");
1415 assert_eq!(status.code(), Code::InvalidArgument);
1416 assert_eq!(
1417 status.message(),
1418 "SPIFFE Workload API requests require workload.spiffe.io=true"
1419 );
1420 assert!(!status.message().contains("vault-token-s.123"));
1421 }
1422
1423 let mut binary_only = Request::new(X509svidRequest {});
1424 binary_only.metadata_mut().insert_bin(
1425 "workload.spiffe.io-bin",
1426 tonic::metadata::MetadataValue::from_bytes(b"true"),
1427 );
1428 let status = require_workload_header(&binary_only)
1429 .expect_err("binary metadata key does not satisfy string gate");
1430 assert_eq!(status.code(), Code::InvalidArgument);
1431 }
1432
1433 #[tokio::test]
1434 async fn workload_api_methods_reject_missing_header_consistently() {
1435 let (service, _backend) = service();
1436
1437 let status = service
1438 .fetch_x509svid(Request::new(X509svidRequest {}))
1439 .await
1440 .err()
1441 .expect("missing x509 svid header rejected");
1442 assert_eq!(status.code(), Code::InvalidArgument);
1443
1444 let status = service
1445 .fetch_x509_bundles(Request::new(X509BundlesRequest {}))
1446 .await
1447 .err()
1448 .expect("missing x509 bundles header rejected");
1449 assert_eq!(status.code(), Code::InvalidArgument);
1450
1451 let status = service
1452 .fetch_jwtsvid(Request::new(JwtsvidRequest {
1453 audience: vec!["vault".to_string()],
1454 spiffe_id: String::new(),
1455 }))
1456 .await
1457 .expect_err("missing jwt svid header rejected");
1458 assert_eq!(status.code(), Code::InvalidArgument);
1459
1460 let status = service
1461 .fetch_jwt_bundles(Request::new(JwtBundlesRequest {}))
1462 .await
1463 .err()
1464 .expect("missing jwt bundles header rejected");
1465 assert_eq!(status.code(), Code::InvalidArgument);
1466
1467 let status = service
1468 .validate_jwtsvid(Request::new(ValidateJwtsvidRequest {
1469 audience: "vault".to_string(),
1470 svid: valid_jwt_svid(
1471 "vault",
1472 jsonwebtoken::get_current_timestamp().saturating_add(300),
1473 ),
1474 }))
1475 .await
1476 .expect_err("missing validate header rejected");
1477 assert_eq!(status.code(), Code::InvalidArgument);
1478 }
1479
1480 #[tokio::test]
1481 async fn fetch_jwtsvid_templates_default_spiffe_id() {
1482 let (service, backend) = service();
1483 let response = service
1484 .fetch_jwtsvid(jwt_request(9100, "", vec!["vault"]))
1485 .await
1486 .expect("fetch jwt-svid")
1487 .into_inner();
1488 assert_eq!(response.svids.len(), 1);
1489 let svid = response.svids.first().expect("one svid");
1490 assert_eq!(svid.spiffe_id, "spiffe://example.org/svc-api");
1491 let claims = token_claims(&svid.svid);
1492 assert_eq!(claims["iss"], "spiffe://example.org/basil");
1493 assert_eq!(claims["sub"], "spiffe://example.org/svc-api");
1494 assert_eq!(claims["aud"], "vault");
1495 assert_eq!(
1496 backend.sign_calls.load(std::sync::atomic::Ordering::SeqCst),
1497 1
1498 );
1499 }
1500
1501 #[tokio::test]
1510 async fn fetch_jwtsvid_templates_against_reloaded_generation_coherently() {
1511 const RENAMED_POLICY: &str = r#"{
1514 "schemaVersion": 2,
1515 "subjects": {
1516 "svc.api": { "allOf": [ { "kind": "unix", "uid": 9100 } ] }
1517 },
1518 "roles": { "minter": ["mint"], "validator": ["validate"] },
1519 "rules": [
1520 { "id": "allow-svc-jwt", "subjects": ["svc.api"], "action": ["role:minter"], "target": ["spire.jwt"] },
1521 { "id": "allow-svc-x509", "subjects": ["svc.api"], "action": ["role:minter"], "target": ["spire.x509"] },
1522 { "id": "allow-svc-validate", "subjects": ["svc.api"], "action": ["role:validator"], "target": ["spire.jwt"] }
1523 ],
1524 "config": { "names": { "users": { "9100": "renamed" }, "groups": {} }, "memberships": {} }
1525 }"#;
1526
1527 let (service, _backend) = service();
1528
1529 let before = service
1531 .fetch_jwtsvid(jwt_request(9100, "", vec!["vault"]))
1532 .await
1533 .expect("gen 1 fetch jwt-svid")
1534 .into_inner();
1535 assert_eq!(
1536 before.svids.first().expect("svid").spiffe_id,
1537 "spiffe://example.org/svc-api"
1538 );
1539
1540 let (cat, pol, cfg, _) = load(CATALOG, RENAMED_POLICY).expect("reload fixture loads");
1543 let next = crate::state::Generation::new(2, std::sync::Arc::new(cat), pol, cfg);
1544 service.state.swap_generation(std::sync::Arc::new(next));
1545 assert_eq!(service.state.active_generation_id(), 2);
1546
1547 let after = service
1549 .fetch_jwtsvid(jwt_request(9100, "", vec!["vault"]))
1550 .await
1551 .expect("gen 2 fetch jwt-svid")
1552 .into_inner();
1553 let svid = after.svids.first().expect("svid");
1554 assert_eq!(svid.spiffe_id, "spiffe://example.org/renamed");
1555 let claims = token_claims(&svid.svid);
1558 assert_eq!(claims["sub"], "spiffe://example.org/renamed");
1559 assert_eq!(claims["iss"], "spiffe://example.org/basil");
1560 }
1561
1562 #[tokio::test]
1568 async fn fetch_jwtsvid_discovers_issuer_from_reloaded_generation_catalog() {
1569 let (service, _backend) = service();
1570 assert_eq!(service.state.active_generation_id(), 1);
1571
1572 let reloaded_catalog = CATALOG
1573 .replace("trust_domain=example.org", "trust_domain=other.org")
1574 .replace(
1575 "spiffe_id=spiffe://example.org/basil",
1576 "spiffe_id=spiffe://other.org/basil",
1577 );
1578 let (cat, pol, cfg, warnings) =
1579 load(&reloaded_catalog, POLICY).expect("label-only reload fixture loads");
1580 assert!(warnings.is_empty());
1581 let next = crate::state::Generation::new(2, std::sync::Arc::new(cat), pol, cfg);
1582 service.state.swap_generation(std::sync::Arc::new(next));
1583
1584 let response = service
1585 .fetch_jwtsvid(jwt_request(
1586 9100,
1587 "spiffe://other.org/svc-api",
1588 vec!["vault"],
1589 ))
1590 .await
1591 .expect("reloaded trust-domain issuer is discoverable")
1592 .into_inner();
1593 let svid = response.svids.first().expect("svid");
1594 assert_eq!(svid.spiffe_id, "spiffe://other.org/svc-api");
1595 let claims = token_claims(&svid.svid);
1596 assert_eq!(claims["iss"], "spiffe://other.org/basil");
1597 assert_eq!(claims["sub"], "spiffe://other.org/svc-api");
1598 }
1599
1600 #[tokio::test]
1601 async fn fetch_x509svid_streams_initial_svid_set() {
1602 use futures::StreamExt as _;
1603
1604 let (service, backend) = service();
1605 let mut stream = service
1606 .fetch_x509svid(x509_request(9100))
1607 .await
1608 .expect("fetch x509-svid")
1609 .into_inner();
1610 let response = stream
1611 .next()
1612 .await
1613 .expect("initial response")
1614 .expect("initial response ok");
1615 assert_eq!(response.svids.len(), 1);
1616 let svid = response.svids.first().expect("one svid");
1617 assert_eq!(svid.spiffe_id, "spiffe://example.org/svc-api");
1618 assert_eq!(svid.x509_svid, b"leafissuer");
1619 assert_eq!(svid.x509_svid_key, b"private-key");
1620 assert_eq!(svid.bundle, b"bundle");
1621 assert_eq!(
1622 backend.x509_calls.lock().expect("calls lock").as_slice(),
1623 ["spiffe://example.org/svc-api"]
1624 );
1625 }
1626
1627 #[tokio::test]
1628 async fn fetch_x509svid_honors_configured_ttl() {
1629 use futures::StreamExt as _;
1630
1631 let (service, backend) = service_with_limits(BrokerLimits {
1632 svid_ttl_secs: 4,
1633 ..BrokerLimits::default()
1634 });
1635 let mut stream = service
1636 .fetch_x509svid(x509_request(9100))
1637 .await
1638 .expect("fetch x509-svid")
1639 .into_inner();
1640 let response = stream
1641 .next()
1642 .await
1643 .expect("initial response")
1644 .expect("initial response ok");
1645 assert_eq!(response.svids.len(), 1);
1646 assert_eq!(
1647 backend
1648 .expected_x509_ttl
1649 .load(std::sync::atomic::Ordering::SeqCst),
1650 4
1651 );
1652 }
1653
1654 #[test]
1655 fn x509_refresh_interval_is_half_ttl_with_floor() {
1656 let plan = X509IssuePlan {
1657 key_name: "pki/issue/workload".to_string(),
1658 spiffe_id: "spiffe://example.org/svc-api".to_string(),
1659 trust_domain: "example.org".to_string(),
1660 ttl_seconds: DEFAULT_SVID_TTL_SECS,
1661 };
1662 assert_eq!(x509_refresh_after_secs(&plan), DEFAULT_SVID_TTL_SECS / 2);
1663
1664 let short_plan = X509IssuePlan {
1665 ttl_seconds: 1,
1666 ..plan
1667 };
1668 assert_eq!(x509_refresh_after_secs(&short_plan), 1);
1669 }
1670
1671 #[tokio::test]
1672 async fn fetch_x509svid_denies_unauthorized_uid_before_issuing() {
1673 let (service, backend) = service();
1674 let Err(status) = service.fetch_x509svid(x509_request(7777)).await else {
1675 panic!("unauthorized uid accepted");
1676 };
1677 assert_eq!(status.code(), Code::PermissionDenied);
1678 assert!(backend.x509_calls.lock().expect("calls lock").is_empty());
1679 }
1680
1681 #[tokio::test]
1682 async fn fetch_x509_bundles_streams_initial_bundle_map() {
1683 use futures::StreamExt as _;
1684
1685 let (service, _backend) = service();
1686 let mut stream = service
1687 .fetch_x509_bundles(x509_bundles_request())
1688 .await
1689 .expect("fetch x509 bundles")
1690 .into_inner();
1691 let response = stream
1692 .next()
1693 .await
1694 .expect("initial response")
1695 .expect("initial response ok");
1696 assert_eq!(
1697 response.bundles.get("spiffe://example.org"),
1698 Some(&b"bundleissuer".to_vec())
1699 );
1700 assert_eq!(response.crl, vec![b"crl".to_vec()]);
1701 }
1702
1703 #[tokio::test]
1704 async fn fetch_x509_bundles_pushes_on_bundle_change() {
1705 use futures::StreamExt as _;
1706
1707 let (service, _backend) = service();
1708 let events = service.state.events().clone();
1709 let mut stream = service
1710 .fetch_x509_bundles(x509_bundles_request())
1711 .await
1712 .expect("fetch x509 bundles")
1713 .into_inner();
1714 let _initial = stream.next().await.expect("initial response");
1715 events.bundle_changed("example.org");
1716 let response = tokio::time::timeout(Duration::from_secs(1), stream.next())
1717 .await
1718 .expect("refresh response")
1719 .expect("stream item")
1720 .expect("refresh response ok");
1721 assert!(response.bundles.contains_key("spiffe://example.org"));
1722 }
1723
1724 #[tokio::test]
1725 async fn fetch_jwt_bundles_streams_initial_jwks_map() {
1726 use futures::StreamExt as _;
1727
1728 let (service, _backend) = service();
1729 let mut stream = service
1730 .fetch_jwt_bundles(jwt_bundles_request())
1731 .await
1732 .expect("fetch jwt bundles")
1733 .into_inner();
1734 let response = stream
1735 .next()
1736 .await
1737 .expect("initial response")
1738 .expect("initial response ok");
1739 let jwks = response
1740 .bundles
1741 .get("spiffe://example.org")
1742 .expect("jwt bundle");
1743 let jwks: serde_json::Value = serde_json::from_slice(jwks).expect("jwks json");
1744 assert_eq!(jwks["keys"][0]["kty"], "RSA");
1747 assert_eq!(jwks["keys"][0]["alg"], "RS256");
1748 assert!(jwks["keys"][0]["n"].is_string());
1749 assert!(jwks["keys"][0]["e"].is_string());
1750 }
1751
1752 #[tokio::test]
1753 async fn fetch_jwt_bundles_pushes_on_issuer_rotation() {
1754 use futures::StreamExt as _;
1755
1756 let (service, _backend) = service();
1757 let events = service.state.events().clone();
1758 let mut stream = service
1759 .fetch_jwt_bundles(jwt_bundles_request())
1760 .await
1761 .expect("fetch jwt bundles")
1762 .into_inner();
1763 let _initial = stream.next().await.expect("initial response");
1764 events.key_rotated("spire.jwt", 2);
1765 let response = tokio::time::timeout(Duration::from_secs(1), stream.next())
1766 .await
1767 .expect("refresh response")
1768 .expect("stream item")
1769 .expect("refresh response ok");
1770 assert!(response.bundles.contains_key("spiffe://example.org"));
1771 }
1772
1773 #[tokio::test]
1774 async fn validate_jwtsvid_returns_spiffe_id_and_claims() {
1775 let (service, _backend) = service();
1776 let token = valid_jwt_svid(
1777 "vault",
1778 jsonwebtoken::get_current_timestamp().saturating_add(300),
1779 );
1780 let response = service
1781 .validate_jwtsvid(validate_request(9100, "vault", token))
1782 .await
1783 .expect("valid token")
1784 .into_inner();
1785 assert_eq!(response.spiffe_id, "spiffe://example.org/svc-api");
1786 let claims = response.claims.expect("claims");
1787 assert_eq!(
1788 claims
1789 .fields
1790 .get("role")
1791 .and_then(|value| value.kind.as_ref()),
1792 Some(&prost_types::value::Kind::StringValue("api".to_string()))
1793 );
1794 }
1795
1796 #[tokio::test]
1797 async fn validate_jwtsvid_rejects_active_revoked_jti() {
1798 let (service, _backend) = service();
1799 service
1800 .state
1801 .revoke_jwt_svid(
1802 "example.org",
1803 "test-jti",
1804 jsonwebtoken::get_current_timestamp().saturating_add(300),
1805 )
1806 .await
1807 .expect("revoked jti stored");
1808 let token = valid_jwt_svid(
1809 "vault",
1810 jsonwebtoken::get_current_timestamp().saturating_add(300),
1811 );
1812 let status = service
1813 .validate_jwtsvid(validate_request(9100, "vault", token))
1814 .await
1815 .expect_err("revoked jti rejected");
1816 assert_eq!(status.code(), Code::InvalidArgument);
1817 assert_eq!(status.message(), "JWT-SVID validation failed");
1818 }
1819
1820 #[tokio::test]
1821 async fn validate_jwtsvid_allows_expired_revoked_jti_entry() {
1822 let (service, _backend) = service();
1823 service
1824 .state
1825 .revoke_jwt_svid(
1826 "example.org",
1827 "test-jti",
1828 jsonwebtoken::get_current_timestamp().saturating_sub(1),
1829 )
1830 .await
1831 .expect("expired jti ignored");
1832 let token = valid_jwt_svid(
1833 "vault",
1834 jsonwebtoken::get_current_timestamp().saturating_add(300),
1835 );
1836 let response = service
1837 .validate_jwtsvid(validate_request(9100, "vault", token))
1838 .await
1839 .expect("expired deny-list entry does not reject")
1840 .into_inner();
1841 assert_eq!(response.spiffe_id, "spiffe://example.org/svc-api");
1842 }
1843
1844 #[tokio::test]
1845 async fn validate_jwtsvid_is_policy_gated() {
1846 let (service, _backend) = service();
1847 let token = valid_jwt_svid(
1848 "vault",
1849 jsonwebtoken::get_current_timestamp().saturating_add(300),
1850 );
1851 let status = service
1852 .validate_jwtsvid(validate_request(7777, "vault", token))
1853 .await
1854 .expect_err("unauthorized validator rejected");
1855 assert_eq!(status.code(), Code::PermissionDenied);
1856 }
1857
1858 #[tokio::test]
1859 async fn validate_jwtsvid_rejects_wrong_audience() {
1860 let (service, _backend) = service();
1861 let token = valid_jwt_svid(
1862 "vault",
1863 jsonwebtoken::get_current_timestamp().saturating_add(300),
1864 );
1865 let status = service
1866 .validate_jwtsvid(validate_request(9100, "other", token))
1867 .await
1868 .expect_err("wrong audience rejected");
1869 assert_eq!(status.code(), Code::InvalidArgument);
1870 assert_eq!(status.message(), "JWT-SVID validation failed");
1871 }
1872
1873 #[tokio::test]
1874 async fn validate_jwtsvid_rejects_expired_token() {
1875 let (service, _backend) = service();
1876 let token = valid_jwt_svid(
1877 "vault",
1878 jsonwebtoken::get_current_timestamp().saturating_sub(120),
1879 );
1880 let status = service
1881 .validate_jwtsvid(validate_request(9100, "vault", token))
1882 .await
1883 .expect_err("expired token rejected");
1884 assert_eq!(status.code(), Code::InvalidArgument);
1885 assert_eq!(status.message(), "JWT-SVID validation failed");
1886 }
1887
1888 #[tokio::test]
1889 async fn validate_jwtsvid_rejects_bad_signature() {
1890 let (service, _backend) = service();
1891 let mut token = valid_jwt_svid(
1892 "vault",
1893 jsonwebtoken::get_current_timestamp().saturating_add(300),
1894 );
1895 token.push('x');
1896 let status = service
1897 .validate_jwtsvid(validate_request(9100, "vault", token))
1898 .await
1899 .expect_err("bad signature rejected");
1900 assert_eq!(status.code(), Code::InvalidArgument);
1901 assert_eq!(status.message(), "JWT-SVID validation failed");
1902 }
1903
1904 #[tokio::test]
1905 async fn fetch_jwtsvid_rejects_malformed_spiffe_ids_and_audiences() {
1906 let (service, _backend) = service();
1907 for requested in [
1908 "example.org/no-scheme",
1909 "spiffe://example.org",
1910 "spiffe://example.org/",
1911 "spiffe://example.org/svc api",
1912 ] {
1913 let status = service
1914 .fetch_jwtsvid(jwt_request(9100, requested, vec!["vault"]))
1915 .await
1916 .expect_err("malformed requested SPIFFE ID rejected");
1917 assert_eq!(status.code(), Code::InvalidArgument);
1918 }
1919
1920 let status = service
1921 .fetch_jwtsvid(jwt_request(
1922 9100,
1923 "spiffe://other.org/svc-api",
1924 vec!["vault"],
1925 ))
1926 .await
1927 .expect_err("out-of-domain requested SPIFFE ID rejected");
1928 assert_eq!(status.code(), Code::PermissionDenied);
1929
1930 for audience in ["", " ", "\t", "\n"] {
1931 let status = service
1932 .fetch_jwtsvid(jwt_request(9100, "", vec![audience]))
1933 .await
1934 .expect_err("blank audience rejected");
1935 assert_eq!(status.code(), Code::InvalidArgument);
1936 assert_eq!(
1937 status.message(),
1938 "FetchJWTSVID requires a non-empty audience"
1939 );
1940 }
1941 }
1942
1943 #[tokio::test]
1944 async fn validate_jwtsvid_rejects_malformed_inputs_consistently() {
1945 let (service, _backend) = service();
1946 for (audience, token) in [
1947 ("", valid_jwt_svid("vault", 300)),
1948 (" ", valid_jwt_svid("vault", 300)),
1949 ("vault", String::new()),
1950 ("vault", "not.a.jwt".to_string()),
1951 ] {
1952 let status = service
1953 .validate_jwtsvid(validate_request(9100, audience, token))
1954 .await
1955 .expect_err("malformed validation input rejected");
1956 assert_eq!(status.code(), Code::InvalidArgument);
1957 }
1958 }
1959
1960 #[test]
1961 fn workload_api_upstream_errors_omit_secret_bearing_details() {
1962 let canaries = [
1963 "vault-token-s.123",
1964 "Authorization: Bearer secret",
1965 "/run/credentials/basil/passphrase",
1966 "-----BEGIN PRIVATE KEY-----",
1967 "upstream-response-body-with-credential",
1968 ];
1969 let statuses = [
1970 mint_status(&crate::minter::GenericMintError::Backend(
1971 crate::backend::BackendError::Backend(canaries[4].to_string()),
1972 )),
1973 x509_issue_status(&crate::manager::ManagerError::Backend(
1974 crate::backend::BackendError::Transport(canaries[1].to_string()),
1975 )),
1976 upstream_unavailable(),
1977 ];
1978 for status in statuses {
1979 assert_eq!(status.code(), Code::Unavailable);
1980 for canary in canaries {
1981 assert!(
1982 !status.message().contains(canary),
1983 "Workload API status leaked secret canary `{canary}`"
1984 );
1985 }
1986 }
1987 }
1988
1989 #[tokio::test]
1990 async fn fetch_jwtsvid_accepts_explicit_same_domain_id() {
1991 let (service, _backend) = service();
1992 let response = service
1993 .fetch_jwtsvid(jwt_request(
1994 9100,
1995 "spiffe://example.org/custom",
1996 vec!["vault", "nats"],
1997 ))
1998 .await
1999 .expect("fetch jwt-svid")
2000 .into_inner();
2001 assert_eq!(response.svids.len(), 2);
2002 assert!(
2003 response
2004 .svids
2005 .iter()
2006 .all(|svid| svid.spiffe_id == "spiffe://example.org/custom")
2009 );
2010 }
2011
2012 #[tokio::test]
2013 async fn fetch_jwtsvid_requires_audience() {
2014 let (service, _backend) = service();
2015 let status = service
2016 .fetch_jwtsvid(jwt_request(9100, "", vec![]))
2017 .await
2018 .expect_err("missing audience rejected");
2019 assert_eq!(status.code(), Code::InvalidArgument);
2020 }
2021
2022 #[tokio::test]
2023 async fn fetch_jwtsvid_denies_unauthorized_uid_before_signing() {
2024 let (service, backend) = service();
2025 let status = service
2026 .fetch_jwtsvid(jwt_request(7777, "", vec!["vault"]))
2027 .await
2028 .expect_err("unauthorized uid rejected");
2029 assert_eq!(status.code(), Code::PermissionDenied);
2030 assert_eq!(
2031 backend.sign_calls.load(std::sync::atomic::Ordering::SeqCst),
2032 0
2033 );
2034 }
2035
2036 #[test]
2037 fn svid_alg_selects_profile_algs_and_fails_closed_otherwise() {
2038 assert_eq!(
2039 svid_alg(Some(KeyAlgorithm::Rsa2048)).expect("rsa is a JWT-SVID alg"),
2040 crate::minter::SvidAlg::Rs256
2041 );
2042 assert_eq!(
2043 svid_alg(Some(KeyAlgorithm::EcdsaP256)).expect("p256 is a JWT-SVID alg"),
2044 crate::minter::SvidAlg::Es256
2045 );
2046 assert_eq!(
2047 svid_alg(Some(KeyAlgorithm::EcdsaP384)).expect("p384 is a JWT-SVID alg"),
2048 crate::minter::SvidAlg::Es384
2049 );
2050 for key_type in [
2053 None,
2054 Some(KeyAlgorithm::Ed25519),
2055 Some(KeyAlgorithm::Ed25519Nkey),
2056 Some(KeyAlgorithm::EcdsaP521),
2057 Some(KeyAlgorithm::Aes256Gcm),
2058 Some(KeyAlgorithm::X25519),
2059 ] {
2060 let status = svid_alg(key_type).expect_err("non-profile alg is rejected");
2061 assert_eq!(status.code(), Code::FailedPrecondition);
2062 }
2063 }
2064}