spiffe/workload_api/client.rs
1//! A Workload API client implementation to fetch X.509 and JWT materials.
2//! # Examples
3//!
4//! ```no_run
5//! use spiffe::{WorkloadApiClient, X509BundleSet, X509Context, X509Svid};
6//! use std::error::Error;
7//! use tokio_stream::StreamExt;
8//!
9//! # async fn example() -> Result<(), Box< dyn Error>> {
10//!
11//! let mut client =
12//! WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/public/api.sock").await?;
13//!
14//! let target_audience = &["service1", "service2"];
15//! // fetch a jwt token for the default identity with the target audience
16//! let jwt_token = client.fetch_jwt_token(target_audience, None).await?;
17//!
18//! // fetch the jwt token for the default identity and parses it as a `JwtSvid`
19//! let jwt_svid = client.fetch_jwt_svid(target_audience, None).await?;
20//!
21//! // fetch a set of jwt bundles (public keys for validating jwt token)
22//! let jwt_bundles = client.fetch_jwt_bundles().await?;
23//!
24//! // fetch the default X.509 SVID
25//! let x509_svid: X509Svid = client.fetch_x509_svid().await?;
26//!
27//! // fetch a set of X.509 bundles (X.509 public key authorities)
28//! let x509_bundles: X509BundleSet = client.fetch_x509_bundles().await?;
29//!
30//! // fetch all the X.509 materials (SVIDs and bundles)
31//! let x509_context: X509Context = client.fetch_x509_context().await?;
32//!
33//! // watch for updates on the X.509 context
34//! let mut x509_context_stream = client.stream_x509_contexts().await?;
35//! while let Some(x509_context_update) = x509_context_stream.next().await {
36//! match x509_context_update {
37//! Ok(context) => {
38//! // handle the updated X509Context
39//! }
40//! Err(e) => {
41//! // handle the error
42//! }
43//! }
44//! }
45//!
46//! # Ok(())
47//! # }
48//! ```
49
50use std::str::FromStr;
51
52use crate::bundle::jwt::{JwtBundle, JwtBundleSet};
53use crate::bundle::x509::{X509Bundle, X509BundleSet};
54use crate::endpoint::{get_default_socket_path, validate_socket_path};
55use crate::spiffe_id::{SpiffeId, TrustDomain};
56use crate::svid::jwt::JwtSvid;
57use crate::svid::x509::X509Svid;
58use crate::workload_api::x509_context::X509Context;
59use std::convert::TryFrom;
60
61use hyper_util::rt::TokioIo;
62use tokio::net::UnixStream;
63use tokio_stream::{Stream, StreamExt};
64
65use crate::constants::DEFAULT_SVID;
66use crate::error::GrpcClientError;
67use crate::proto::workload::spiffe_workload_api_client::SpiffeWorkloadApiClient;
68use crate::proto::workload::{
69 JwtBundlesRequest, JwtBundlesResponse, JwtsvidRequest, JwtsvidResponse, ValidateJwtsvidRequest,
70 ValidateJwtsvidResponse, X509BundlesRequest, X509BundlesResponse, X509svidRequest,
71 X509svidResponse,
72};
73use tonic::transport::{Endpoint, Uri};
74use tower::service_fn;
75
76const SPIFFE_HEADER_KEY: &str = "workload.spiffe.io";
77const SPIFFE_HEADER_VALUE: &str = "true";
78
79/// This type represents a client to interact with the Workload API.
80///
81/// Supports one-shot calls and streaming updates for X.509 and JWT SVIDs and bundles.
82/// The client can be used to fetch the current SVIDs and bundles, as well as to
83/// subscribe for updates whenever the SVIDs or bundles change.
84#[derive(Debug, Clone)]
85pub struct WorkloadApiClient {
86 client: SpiffeWorkloadApiClient<
87 tonic::service::interceptor::InterceptedService<tonic::transport::Channel, MetadataAdder>,
88 >,
89}
90
91#[derive(Clone)]
92struct MetadataAdder;
93
94impl tonic::service::Interceptor for MetadataAdder {
95 fn call(
96 &mut self,
97 mut request: tonic::Request<()>,
98 ) -> Result<tonic::Request<()>, tonic::Status> {
99 let parsed_header = SPIFFE_HEADER_VALUE
100 .parse()
101 .map_err(|e| tonic::Status::internal(format!("Failed to parse header: {e}")))?;
102 request
103 .metadata_mut()
104 .insert(SPIFFE_HEADER_KEY, parsed_header);
105 Ok(request)
106 }
107}
108
109impl WorkloadApiClient {
110 const UNIX_PREFIX: &'static str = "unix:";
111 const TONIC_DEFAULT_URI: &'static str = "http://[::]:50051";
112
113 /// Creates a new instance of `WorkloadApiClient` by connecting to the specified socket path.
114 ///
115 /// # Arguments
116 ///
117 /// * `path` - The path to the UNIX domain socket, which can optionally start with "unix:".
118 ///
119 /// # Returns
120 ///
121 /// * `Result<Self, ClientError>` - Returns an instance of `WorkloadApiClient` if successful, otherwise returns an error.
122 ///
123 /// # Errors
124 ///
125 /// This function will return an error if the provided socket path is invalid or if there are issues connecting.
126 pub async fn new_from_path(path: &str) -> Result<Self, GrpcClientError> {
127 validate_socket_path(path)?;
128
129 // Strip the 'unix:' prefix for tonic compatibility.
130 let stripped_path = path
131 .strip_prefix(Self::UNIX_PREFIX)
132 .unwrap_or(path)
133 .to_string();
134
135 let channel = Endpoint::try_from(Self::TONIC_DEFAULT_URI)?
136 .connect_with_connector(service_fn(move |_: Uri| {
137 let stripped_path = stripped_path.clone();
138 async {
139 // Connect to the UDS socket using the modified path.
140 UnixStream::connect(stripped_path).await.map(TokioIo::new)
141 }
142 }))
143 .await?;
144
145 Ok(WorkloadApiClient {
146 client: SpiffeWorkloadApiClient::with_interceptor(channel, MetadataAdder {}),
147 })
148 }
149
150 /// Creates a new `WorkloadApiClient` using the default socket endpoint address.
151 ///
152 /// Requires that the environment variable `SPIFFE_ENDPOINT_SOCKET` be set with
153 /// the path to the Workload API endpoint socket.
154 ///
155 /// # Errors
156 ///
157 /// The function returns a variant of [`GrpcClientError`] if environment variable is not set or if
158 /// the provided socket path is not valid.
159 pub async fn default() -> Result<Self, GrpcClientError> {
160 let socket_path =
161 get_default_socket_path().ok_or(GrpcClientError::MissingEndpointSocketPath)?;
162 Self::new_from_path(socket_path.as_str()).await
163 }
164
165 /// Constructs a new `WorkloadApiClient` using the provided Tonic transport channel.
166 ///
167 /// # Arguments
168 ///
169 /// * `conn`: A `tonic::transport::Channel` used for gRPC communication.
170 ///
171 /// # Returns
172 ///
173 /// A `Result` containing a `WorkloadApiClient` if successful, or a `ClientError` if an error occurs.
174 pub fn new(conn: tonic::transport::Channel) -> Result<Self, GrpcClientError> {
175 Ok(WorkloadApiClient {
176 client: SpiffeWorkloadApiClient::with_interceptor(conn, MetadataAdder {}),
177 })
178 }
179
180 /// Fetches a single X509 SPIFFE Verifiable Identity Document (SVID).
181 ///
182 /// This method connects to the SPIFFE Workload API and returns the first X509 SVID in the response.
183 ///
184 /// # Returns
185 ///
186 /// On success, it returns a valid [`X509Svid`] which represents the parsed SVID.
187 /// If the fetch operation or the parsing fails, it returns a [`GrpcClientError`].
188 ///
189 /// # Errors
190 ///
191 /// Returns [`GrpcClientError`] if the gRPC call fails or if the SVID could not be parsed from the gRPC response.
192 pub async fn fetch_x509_svid(&mut self) -> Result<X509Svid, GrpcClientError> {
193 let request = X509svidRequest::default();
194
195 let grpc_stream_response: tonic::Response<tonic::Streaming<X509svidResponse>> =
196 self.client.fetch_x509svid(request).await?;
197
198 let response = grpc_stream_response
199 .into_inner()
200 .message()
201 .await?
202 .ok_or(GrpcClientError::EmptyResponse)?;
203 WorkloadApiClient::parse_x509_svid_from_grpc_response(response)
204 }
205
206 /// Fetches all X509 SPIFFE Verifiable Identity Documents (SVIDs) available to the workload.
207 ///
208 /// This method sends a request to the SPIFFE Workload API, retrieving a stream of X509 SVID responses.
209 /// All SVIDs are then parsed and returned as a list.
210 ///
211 /// # Returns
212 ///
213 /// On success, it returns a `Vec` containing valid [`X509Svid`] instances, each representing a parsed SVID.
214 /// If the fetch operation or any parsing fails, it returns a [`GrpcClientError`].
215 ///
216 /// # Errors
217 ///
218 /// Returns [`GrpcClientError`] if the gRPC call fails, if the SVIDs could not be parsed from the gRPC response,
219 /// or if the stream unexpectedly terminates.
220 pub async fn fetch_all_x509_svids(&mut self) -> Result<Vec<X509Svid>, GrpcClientError> {
221 let request = X509svidRequest::default();
222
223 let grpc_stream_response: tonic::Response<tonic::Streaming<X509svidResponse>> =
224 self.client.fetch_x509svid(request).await?;
225
226 let response = grpc_stream_response
227 .into_inner()
228 .message()
229 .await?
230 .ok_or(GrpcClientError::EmptyResponse)?;
231 WorkloadApiClient::parse_x509_svids_from_grpc_response(response)
232 }
233
234 /// Fetches [`X509BundleSet`], that is a set of [`X509Bundle`] keyed by the trust domain to which they belong.
235 ///
236 /// # Errors
237 ///
238 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
239 /// there is a problem processing the response.
240 pub async fn fetch_x509_bundles(&mut self) -> Result<X509BundleSet, GrpcClientError> {
241 let request = X509BundlesRequest::default();
242
243 let grpc_stream_response: tonic::Response<tonic::Streaming<X509BundlesResponse>> =
244 self.client.fetch_x509_bundles(request).await?;
245
246 let response = grpc_stream_response
247 .into_inner()
248 .message()
249 .await?
250 .ok_or(GrpcClientError::EmptyResponse)?;
251 WorkloadApiClient::parse_x509_bundle_set_from_grpc_response(response)
252 }
253
254 /// Fetches [`JwtBundleSet`] that is a set of [`JwtBundle`] keyed by the trust domain to which they belong.
255 ///
256 /// # Errors
257 ///
258 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
259 /// there is a problem processing the response.
260 pub async fn fetch_jwt_bundles(&mut self) -> Result<JwtBundleSet, GrpcClientError> {
261 let request = JwtBundlesRequest::default();
262
263 let grpc_stream_response: tonic::Response<tonic::Streaming<JwtBundlesResponse>> =
264 self.client.fetch_jwt_bundles(request).await?;
265
266 let response = grpc_stream_response
267 .into_inner()
268 .message()
269 .await?
270 .ok_or(GrpcClientError::EmptyResponse)?;
271 WorkloadApiClient::parse_jwt_bundle_set_from_grpc_response(response)
272 }
273
274 /// Fetches the [`X509Context`], which contains all the X.509 materials,
275 /// i.e. X509-SVIDs and X.509 bundles.
276 ///
277 /// # Errors
278 ///
279 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
280 /// there is a problem processing the response.
281 pub async fn fetch_x509_context(&mut self) -> Result<X509Context, GrpcClientError> {
282 let request = X509svidRequest::default();
283
284 let grpc_stream_response: tonic::Response<tonic::Streaming<X509svidResponse>> =
285 self.client.fetch_x509svid(request).await?;
286
287 let response = grpc_stream_response
288 .into_inner()
289 .message()
290 .await?
291 .ok_or(GrpcClientError::EmptyResponse)?;
292 WorkloadApiClient::parse_x509_context_from_grpc_response(response)
293 }
294
295 /// Fetches a [`JwtSvid`] parsing the JWT token in the Workload API response, for the given audience and spiffe_id.
296 ///
297 /// # Arguments
298 ///
299 /// * `audience` - A list of audiences to include in the JWT token. Cannot be empty nor contain only empty strings.
300 /// * `spiffe_id` - Optional [`SpiffeId`] for the token 'sub' claim. If not provided, the Workload API returns the
301 /// default identity.
302 ///
303 /// # Errors
304 ///
305 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
306 /// there is a problem processing the response.
307 ///
308 /// IMPORTANT: If there's no registration entries with the requested [`SpiffeId`] mapped to the calling workload,
309 /// it will return a [`GrpcClientError::EmptyResponse`].
310 pub async fn fetch_jwt_svid<T: AsRef<str> + ToString>(
311 &mut self,
312 audience: &[T],
313 spiffe_id: Option<&SpiffeId>,
314 ) -> Result<JwtSvid, GrpcClientError> {
315 let response = self.fetch_jwt(audience, spiffe_id).await?;
316 response
317 .svids
318 .get(DEFAULT_SVID)
319 .ok_or(GrpcClientError::EmptyResponse)
320 .and_then(|r| JwtSvid::from_str(&r.svid).map_err(GrpcClientError::InvalidJwtSvid))
321 }
322
323 /// Fetches a JWT token for the given audience and [`SpiffeId`].
324 ///
325 /// # Arguments
326 ///
327 /// * `audience` - A list of audiences to include in the JWT token. Cannot be empty nor contain only empty strings.
328 /// * `spiffe_id` - Optional reference [`SpiffeId`] for the token 'sub' claim. If not provided, the Workload API returns the
329 /// default identity,
330 ///
331 /// # Errors
332 ///
333 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
334 /// there is a problem processing the response.
335 ///
336 /// IMPORTANT: If there's no registration entries with the requested [`SpiffeId`] mapped to the calling workload,
337 /// it will return a [`GrpcClientError::EmptyResponse`].
338 pub async fn fetch_jwt_token<T: AsRef<str> + ToString>(
339 &mut self,
340 audience: &[T],
341 spiffe_id: Option<&SpiffeId>,
342 ) -> Result<String, GrpcClientError> {
343 let response = self.fetch_jwt(audience, spiffe_id).await?;
344 response
345 .svids
346 .get(DEFAULT_SVID)
347 .map(|r| r.svid.to_string())
348 .ok_or(GrpcClientError::EmptyResponse)
349 }
350
351 /// Validates a JWT SVID token against the given audience. Returns the [`JwtSvid`] parsed from
352 /// the validated token.
353 ///
354 /// # Arguments
355 ///
356 /// * `audience` - The audience of the validating party. Cannot be empty nor contain an empty string.
357 /// * `jwt_token` - The JWT token to validate.
358 ///
359 /// # Errors
360 ///
361 /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or
362 /// there is a problem processing the response.
363 pub async fn validate_jwt_token<T: AsRef<str> + ToString>(
364 &mut self,
365 audience: T,
366 jwt_token: &str,
367 ) -> Result<JwtSvid, GrpcClientError> {
368 // validate token with Workload API, the returned claims and spiffe_id are ignored as
369 // they are parsed from token when the `JwtSvid` object is created, this way we avoid having
370 // to validate that the response from the Workload API contains correct claims.
371 let _ = self.validate_jwt(audience, jwt_token).await?;
372 let jwt_svid = JwtSvid::parse_insecure(jwt_token)?;
373 Ok(jwt_svid)
374 }
375
376 /// Watches the stream of [`X509Context`] updates.
377 ///
378 /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Context`].
379 /// The returned stream can be used to asynchronously yield new `X509Context` updates as they become available.
380 ///
381 /// # Returns
382 ///
383 /// Returns a stream of `Result<X509Context, ClientError>`. Each item represents an updated [`X509Context`] or an error if
384 /// there was a problem processing an update from the stream.
385 ///
386 /// # Errors
387 ///
388 /// The function can return an error variant of [`GrpcClientError`] in the following scenarios:
389 ///
390 /// * There's an issue connecting to the Workload API.
391 /// * An error occurs while setting up the stream.
392 ///
393 /// Individual stream items might also be errors if there's an issue processing the response for a specific update.
394 pub async fn stream_x509_contexts(
395 &mut self,
396 ) -> Result<impl Stream<Item = Result<X509Context, GrpcClientError>>, GrpcClientError> {
397 let request = X509svidRequest::default();
398 let response = self.client.fetch_x509svid(request).await?;
399 let stream = response.into_inner().map(|message| {
400 message
401 .map_err(GrpcClientError::from)
402 .and_then(WorkloadApiClient::parse_x509_context_from_grpc_response)
403 });
404 Ok(stream)
405 }
406
407 /// Watches the stream of [`X509Svid`] updates.
408 ///
409 /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Svid`].
410 /// The returned stream can be used to asynchronously yield new `X509Svid` updates as they become available.
411 ///
412 /// # Returns
413 ///
414 /// Returns a stream of `Result<X509Svid, ClientError>`. Each item represents an updated [`X509Svid`] or an error if
415 /// there was a problem processing an update from the stream.
416 ///
417 /// # Errors
418 ///
419 /// The function can return an error variant of [`GrpcClientError`] in the following scenarios:
420 ///
421 /// * There's an issue connecting to the Workload API.
422 /// * An error occurs while setting up the stream.
423 ///
424 /// Individual stream items might also be errors if there's an issue processing the response for a specific update.
425 pub async fn stream_x509_svids(
426 &mut self,
427 ) -> Result<impl Stream<Item = Result<X509Svid, GrpcClientError>>, GrpcClientError> {
428 let request = X509svidRequest::default();
429 let response = self.client.fetch_x509svid(request).await?;
430 let stream = response.into_inner().map(|message| {
431 message
432 .map_err(GrpcClientError::from)
433 .and_then(WorkloadApiClient::parse_x509_svid_from_grpc_response)
434 });
435 Ok(stream)
436 }
437
438 /// Watches the stream of [`X509BundleSet`] updates.
439 ///
440 /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509BundleSet`].
441 /// The returned stream can be used to asynchronously yield new `X509BundleSet` updates as they become available.
442 ///
443 /// # Returns
444 ///
445 /// Returns a stream of `Result<X509BundleSet, ClientError>`. Each item represents an updated [`X509BundleSet`] or an error if
446 /// there was a problem processing an update from the stream.
447 ///
448 /// # Errors
449 ///
450 /// The function can return an error variant of [`GrpcClientError`] in the following scenarios:
451 ///
452 /// * There's an issue connecting to the Workload API.
453 /// * An error occurs while setting up the stream.
454 ///
455 /// Individual stream items might also be errors if there's an issue processing the response for a specific update.
456 pub async fn stream_x509_bundles(
457 &mut self,
458 ) -> Result<impl Stream<Item = Result<X509BundleSet, GrpcClientError>>, GrpcClientError> {
459 let request = X509BundlesRequest::default();
460 let response = self.client.fetch_x509_bundles(request).await?;
461 let stream = response.into_inner().map(|message| {
462 message
463 .map_err(GrpcClientError::from)
464 .and_then(WorkloadApiClient::parse_x509_bundle_set_from_grpc_response)
465 });
466 Ok(stream)
467 }
468
469 /// Watches the stream of [`JwtBundleSet`] updates.
470 ///
471 /// This function establishes a stream with the Workload API to continuously receive updates for the [`JwtBundleSet`].
472 /// The returned stream can be used to asynchronously yield new `JwtBundleSet` updates as they become available.
473 ///
474 /// # Returns
475 ///
476 /// Returns a stream of `Result<JwtBundleSet, ClientError>`. Each item represents an updated [`JwtBundleSet`] or an error if
477 /// there was a problem processing an update from the stream.
478 ///
479 /// # Errors
480 ///
481 /// The function can return an error variant of [`GrpcClientError`] in the following scenarios:
482 ///
483 /// * There's an issue connecting to the Workload API.
484 /// * An error occurs while setting up the stream.
485 ///
486 /// Individual stream items might also be errors if there's an issue processing the response for a specific update.
487 pub async fn stream_jwt_bundles(
488 &mut self,
489 ) -> Result<impl Stream<Item = Result<JwtBundleSet, GrpcClientError>>, GrpcClientError> {
490 let request = JwtBundlesRequest::default();
491 let response = self.client.fetch_jwt_bundles(request).await?;
492 let stream = response.into_inner().map(|message| {
493 message
494 .map_err(GrpcClientError::from)
495 .and_then(WorkloadApiClient::parse_jwt_bundle_set_from_grpc_response)
496 });
497 Ok(stream)
498 }
499}
500
501/// private
502impl WorkloadApiClient {
503 async fn fetch_jwt<T: AsRef<str> + ToString>(
504 &mut self,
505 audience: &[T],
506 spiffe_id: Option<&SpiffeId>,
507 ) -> Result<JwtsvidResponse, GrpcClientError> {
508 let request = JwtsvidRequest {
509 spiffe_id: spiffe_id.map(ToString::to_string).unwrap_or_default(),
510 audience: audience.iter().map(|s| s.to_string()).collect(),
511 };
512
513 Ok(self.client.fetch_jwtsvid(request).await?.into_inner())
514 }
515
516 async fn validate_jwt<T: AsRef<str>>(
517 &mut self,
518 audience: T,
519 jwt_svid: &str,
520 ) -> Result<ValidateJwtsvidResponse, GrpcClientError> {
521 let request = ValidateJwtsvidRequest {
522 audience: audience.as_ref().into(),
523 svid: jwt_svid.into(),
524 };
525
526 Ok(self.client.validate_jwtsvid(request).await?.into_inner())
527 }
528
529 fn parse_x509_svid_from_grpc_response(
530 response: X509svidResponse,
531 ) -> Result<X509Svid, GrpcClientError> {
532 let svid = response
533 .svids
534 .get(DEFAULT_SVID)
535 .ok_or(GrpcClientError::EmptyResponse)?;
536
537 X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref())
538 .map_err(GrpcClientError::from)
539 }
540
541 fn parse_x509_svids_from_grpc_response(
542 response: X509svidResponse,
543 ) -> Result<Vec<X509Svid>, GrpcClientError> {
544 let mut svids_vec = Vec::new();
545
546 for svid in response.svids.iter() {
547 let parsed_svid =
548 X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref())
549 .map_err(GrpcClientError::from)?;
550
551 svids_vec.push(parsed_svid);
552 }
553
554 Ok(svids_vec)
555 }
556
557 fn parse_x509_bundle_set_from_grpc_response(
558 response: X509BundlesResponse,
559 ) -> Result<X509BundleSet, GrpcClientError> {
560 let bundles: Result<Vec<_>, _> = response
561 .bundles
562 .into_iter()
563 .map(|(td, bundle_data)| {
564 let trust_domain = TrustDomain::try_from(td)?;
565 X509Bundle::parse_from_der(trust_domain, &bundle_data)
566 .map_err(GrpcClientError::from)
567 })
568 .collect();
569
570 let mut bundle_set = X509BundleSet::new();
571 for bundle in bundles? {
572 bundle_set.add_bundle(bundle);
573 }
574
575 Ok(bundle_set)
576 }
577
578 fn parse_jwt_bundle_set_from_grpc_response(
579 response: JwtBundlesResponse,
580 ) -> Result<JwtBundleSet, GrpcClientError> {
581 let mut bundle_set = JwtBundleSet::new();
582
583 for (td, bundle_data) in response.bundles.into_iter() {
584 let trust_domain = TrustDomain::try_from(td)?;
585 let bundle = JwtBundle::from_jwt_authorities(trust_domain, &bundle_data)
586 .map_err(GrpcClientError::from)?;
587
588 bundle_set.add_bundle(bundle);
589 }
590
591 Ok(bundle_set)
592 }
593
594 fn parse_x509_context_from_grpc_response(
595 response: X509svidResponse,
596 ) -> Result<X509Context, GrpcClientError> {
597 let mut svids = Vec::new();
598 let mut bundle_set = X509BundleSet::new();
599
600 for svid in response.svids.into_iter() {
601 let x509_svid =
602 X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref())
603 .map_err(GrpcClientError::from)?;
604
605 let trust_domain = x509_svid.spiffe_id().trust_domain().clone();
606 svids.push(x509_svid);
607
608 let bundle = X509Bundle::parse_from_der(trust_domain, svid.bundle.as_ref())
609 .map_err(GrpcClientError::from)?;
610 bundle_set.add_bundle(bundle);
611 }
612
613 Ok(X509Context::new(svids, bundle_set))
614 }
615}