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