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