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