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}