dwara_core/mesh/
spiffe.rs

1//! SPIFFE/SPIRE mTLS identity (DW-107).
2//!
3//! Identity in the service mesh is provided by SPIFFE (Secure Production
4//! Identity Framework for Everyone) via SPIRE (the SPIFFE Runtime
5//! Environment). Each workload fetches X.509 SVIDs (SPIFFE Verifiable
6//! Identity Documents) from the SPIRE Workload API, exposed over a Unix
7//! domain socket. An X.509 SVID is an X.509 certificate whose URI SAN
8//! carries the workload's SPIFFE ID (`spiffe://<trust-domain>/<path>`).
9//! The SPIFFE ID is the authentication identity used for policy
10//! decisions; the trust bundle (the SPIRE signing CAs) is used to
11//! verify peer SVIDs.
12//!
13//! # Implementation
14//!
15//! The Workload API client is implemented behind a transport
16//! abstraction ([`WorkloadApiTransport`]) so the fetch logic is
17//! testable without a real SPIRE agent. The production transport
18//! (`GrpcWorkloadApi`) uses tonic over a Unix domain socket and is
19//! gated behind the `ent` cargo feature (which brings in tonic + prost;
20//! the mesh feature itself is flag-only). A [`FakeWorkloadApi`] is
21//! provided for tests.
22//!
23//! # Integration point
24//!
25//! The mTLS TLS config in `security::tls` would consume the SVID
26//! certificate/key (as the presented client/server cert) and the trust
27//! bundle (as the peer-verification root store). The peer's SPIFFE ID
28//! is extracted from the verified peer certificate's URI SAN and used
29//! as the auth identity (the consumer / principal for policy
30//! decisions). This seam is documented here and kept as a hand-off
31//! (mesh produces the SVID material; security consumes it) so the
32//! dependency direction stays downward.
33
34use crate::config::mesh::MeshSpiffeConfig;
35use std::path::PathBuf;
36use std::time::Duration;
37
38/// A SPIFFE ID: the trust domain and the path that together form the
39/// workload identity (`spiffe://<trust-domain>/<path>`).
40///
41/// The SPIFFE ID is the URI SAN carried in an X.509 SVID; it is the
42/// authentication identity the mesh uses for policy decisions. The
43/// trust domain is the trust boundary (e.g. `example.org`); the path
44/// identifies the workload within the trust domain (e.g.
45/// `/ns/default/sa/my-service`).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SpiffeIdentity {
48    /// The trust domain (the part after `spiffe://` and before the
49    /// first `/`). Must be non-empty.
50    pub trust_domain: String,
51    /// The SPIFFE path (everything after the trust domain, including
52    /// the leading `/`). Must start with `/`.
53    pub path: String,
54}
55
56impl SpiffeIdentity {
57    /// Build a SPIFFE ID from a trust domain and path. The path is
58    /// normalized to start with `/` (a leading slash is prepended when
59    /// missing, mirroring the SPIFFE ID grammar).
60    pub fn new(trust_domain: impl Into<String>, path: impl Into<String>) -> Self {
61        let trust_domain = trust_domain.into();
62        let mut path = path.into();
63        if !path.starts_with('/') {
64            path.insert(0, '/');
65        }
66        SpiffeIdentity { trust_domain, path }
67    }
68
69    /// Parse a `spiffe://<trust-domain>/<path>` URI into a
70    /// [`SpiffeIdentity`]. Returns None when the scheme is not
71    /// `spiffe://` or the trust domain is empty.
72    pub fn parse(uri: &str) -> Option<Self> {
73        let rest = uri.strip_prefix("spiffe://")?;
74        let (trust_domain, path) = rest.split_once('/')?;
75        if trust_domain.is_empty() {
76            return None;
77        }
78        Some(SpiffeIdentity {
79            trust_domain: trust_domain.to_string(),
80            path: format!("/{path}"),
81        })
82    }
83
84    /// The canonical URI form: `spiffe://<trust-domain><path>`.
85    pub fn to_uri(&self) -> String {
86        format!("spiffe://{}{}", self.trust_domain, self.path)
87    }
88
89    /// True when the identity is well-formed: non-empty trust domain
90    /// and a path starting with `/`.
91    pub fn is_valid(&self) -> bool {
92        !self.trust_domain.is_empty() && self.path.starts_with('/')
93    }
94}
95
96impl std::fmt::Display for SpiffeIdentity {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(&self.to_uri())
99    }
100}
101
102/// An X.509 SVID: the certificate chain, the private key, and the
103/// expiry the workload presents to prove its SPIFFE identity.
104///
105/// The certificate's URI SAN carries the [`SpiffeIdentity`]; the
106/// private key is the matching key. The SVID is refreshed before
107/// expiry by the [`SpiffeClient`].
108#[derive(Debug, Clone)]
109pub struct SpiffeSvid {
110    /// The workload's SPIFFE ID (parsed from the SVID's URI SAN).
111    pub spiffe_id: SpiffeIdentity,
112    /// The X.509 certificate chain (DER bytes, leaf first). The leaf
113    /// certificate's URI SAN carries the SPIFFE ID.
114    pub x509_cert: Vec<Vec<u8>>,
115    /// The private key matching the leaf certificate (DER bytes). This
116    /// is sensitive material; it is zeroized on drop when the
117    /// production wiring lands (today the field holds scaffold bytes).
118    pub private_key: Vec<u8>,
119    /// The SVID expiry (seconds since the Unix epoch). The client
120    /// refreshes the SVID before this time.
121    pub expires_at: u64,
122}
123
124impl SpiffeSvid {
125    /// Seconds until the SVID expires, relative to `now`. Clamped to 0
126    /// when already expired (the gauge metric never goes negative).
127    pub fn seconds_until_expiry(&self, now: u64) -> i64 {
128        let remaining = self.expires_at.saturating_sub(now);
129        remaining as i64
130    }
131}
132
133/// The SPIFFE trust bundle: the X.509 root certificates (the SPIRE
134/// signing CAs) used to verify peer SVIDs. The bundle is fetched from
135/// the Workload API alongside the SVID and refreshed on the same
136/// schedule.
137#[derive(Debug, Clone)]
138pub struct SpiffeTrustBundle {
139    /// The trust domain this bundle covers.
140    pub trust_domain: String,
141    /// The X.509 CA certificates (DER bytes) that sign SVIDs in this
142    /// trust domain. The mTLS TLS config uses these as the
143    /// peer-verification root store.
144    pub x509_certs: Vec<Vec<u8>>,
145}
146
147/// The resolved SPIFFE configuration: the trust domain, the Workload
148/// API socket path, and the SVID refresh interval. Built from the
149/// config schema ([`MeshSpiffeConfig`]) at compile time.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct SpiffeConfig {
152    /// The trust domain this workload belongs to (e.g. `example.org`).
153    /// Must be non-empty.
154    pub trust_domain: String,
155    /// The filesystem path of the SPIRE Workload API Unix socket
156    /// (e.g. `/tmp/spire-agent/public/api.sock`). Must be non-empty.
157    pub workload_api_socket: PathBuf,
158    /// The SVID refresh interval. The client refreshes the SVID (and
159    /// trust bundle) before expiry, at half the remaining lifetime by
160    /// default; this is the upper bound on the refresh cadence.
161    pub svid_refresh_interval: Duration,
162}
163
164impl SpiffeConfig {
165    /// Build a [`SpiffeConfig`] from the config schema.
166    pub fn from_config(cfg: &MeshSpiffeConfig) -> Self {
167        SpiffeConfig {
168            trust_domain: cfg.trust_domain.clone(),
169            workload_api_socket: PathBuf::from(&cfg.workload_api_socket),
170            svid_refresh_interval: Duration::from_secs(cfg.svid_refresh_interval_secs),
171        }
172    }
173}
174
175/// The transport abstraction for the SPIRE Workload API. The
176/// production implementation (`GrpcWorkloadApi`) uses tonic over a
177/// Unix domain socket (ent-gated); tests use [`FakeWorkloadApi`].
178///
179/// The trait is async via `async-trait` (dyn-compatible, the same
180/// pattern as the extensions traits). The methods mirror the SPIRE
181/// Workload API's `FetchX509SVID` and `FetchX509Bundle` RPCs.
182#[async_trait::async_trait]
183pub trait WorkloadApiTransport: Send + Sync + 'static {
184    /// Fetch the workload's X.509 SVIDs from the Workload API. The
185    /// response contains one SVID per SPIFFE ID the workload is
186    /// authorized to present (typically one). The SVID's cert chain
187    /// and private key are DER bytes; the expiry is seconds since the
188    /// Unix epoch.
189    async fn fetch_x509_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError>;
190
191    /// Fetch the trust bundle (the SPIRE signing CAs) for the
192    /// configured trust domain from the Workload API. The bundle's
193    /// CA certs are DER bytes; the mTLS TLS config uses them as the
194    /// peer-verification root store.
195    async fn fetch_x509_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError>;
196}
197
198/// The SPIFFE Workload API client: connects to the SPIRE agent over a
199/// Unix socket, fetches X.509 SVIDs for the workload, and refreshes
200/// them before expiry.
201///
202/// The client holds a [`WorkloadApiTransport`] (the production gRPC
203/// transport or a test fake) and delegates the actual fetch to it.
204/// The refresh logic (when to refresh, what to do on failure) lives
205/// here; the transport is the seam for the wire protocol.
206#[derive(Clone)]
207pub struct SpiffeClient {
208    config: SpiffeConfig,
209    transport: std::sync::Arc<dyn WorkloadApiTransport>,
210}
211
212impl std::fmt::Debug for SpiffeClient {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        f.debug_struct("SpiffeClient")
215            .field("config", &self.config)
216            .field("transport", &"<dyn WorkloadApiTransport>")
217            .finish()
218    }
219}
220
221/// The outcome of an SVID refresh attempt, captured for the
222/// `dwara_spiffe_svid_refresh_total{result}` metric. The CLOSED
223/// two-value label set: `success` when the Workload API returned a
224/// fresh SVID, `error` when the fetch failed (the client keeps serving
225/// the previous SVID until it expires).
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum SvidRefreshResult {
228    /// The Workload API returned a fresh SVID and trust bundle.
229    Success,
230    /// The fetch failed (socket unreachable, gRPC error, parse
231    /// failure). The client keeps serving the previous SVID until it
232    /// expires; a refresh failure does not drop the workload's
233    /// identity immediately.
234    Error,
235}
236
237impl SvidRefreshResult {
238    /// The lowercase metric label.
239    pub fn as_str(self) -> &'static str {
240        match self {
241            SvidRefreshResult::Success => "success",
242            SvidRefreshResult::Error => "error",
243        }
244    }
245}
246
247impl SpiffeClient {
248    /// Build a client from the resolved SPIFFE config and a transport.
249    /// The transport is the seam for the Workload API wire protocol:
250    /// the production gRPC transport (`GrpcWorkloadApi`) or a test
251    /// fake ([`FakeWorkloadApi`]).
252    pub fn new(config: SpiffeConfig, transport: std::sync::Arc<dyn WorkloadApiTransport>) -> Self {
253        SpiffeClient { config, transport }
254    }
255
256    /// The resolved SPIFFE configuration.
257    pub fn config(&self) -> &SpiffeConfig {
258        &self.config
259    }
260
261    /// The workload's SPIFFE ID for the configured trust domain and a
262    /// path. This is the identity the SVID's URI SAN carries and the
263    /// auth identity the mesh uses for policy decisions.
264    pub fn workload_identity(&self, path: &str) -> SpiffeIdentity {
265        SpiffeIdentity::new(&self.config.trust_domain, path)
266    }
267
268    /// Fetch the workload's current X.509 SVIDs from the Workload API.
269    /// The response contains one SVID per SPIFFE ID the workload is
270    /// authorized to present (typically one). The first SVID is the
271    /// primary; callers that only need one SVID can use
272    /// [`Self::fetch_primary_svid`].
273    pub async fn fetch_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError> {
274        self.transport.fetch_x509_svid().await
275    }
276
277    /// Fetch the workload's primary (first) X.509 SVID from the
278    /// Workload API. Convenience wrapper around [`Self::fetch_svid`]
279    /// that returns the first SVID or an error when the response is
280    /// empty.
281    pub async fn fetch_primary_svid(&self) -> Result<SpiffeSvid, SpiffeError> {
282        let mut svids = self.fetch_svid().await?;
283        if svids.is_empty() {
284            return Err(SpiffeError::EmptyResponse);
285        }
286        Ok(svids.remove(0))
287    }
288
289    /// Fetch the trust bundle (the SPIRE signing CAs) from the Workload
290    /// API. Used as the peer-verification root store in the mTLS TLS
291    /// config.
292    pub async fn fetch_trust_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError> {
293        self.transport.fetch_x509_bundle().await
294    }
295
296    /// Refresh the SVID before expiry. The client refreshes at half the
297    /// remaining lifetime by default; the configured
298    /// `svid_refresh_interval` is the upper bound on the cadence.
299    ///
300    /// This is the synchronous wrapper used by the refresh loop's
301    /// metric recording path. The actual fetch is async (delegated to
302    /// the transport); the refresh loop spawns the async fetch and
303    /// records the result here.
304    pub fn refresh_result_from_fetch(
305        &self,
306        fetch_result: &Result<SpiffeSvid, SpiffeError>,
307    ) -> SvidRefreshResult {
308        match fetch_result {
309            Ok(_) => SvidRefreshResult::Success,
310            Err(_) => SvidRefreshResult::Error,
311        }
312    }
313}
314
315/// Error returned by the SPIFFE Workload API calls.
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum SpiffeError {
318    /// The Workload API socket could not be reached (the SPIRE agent
319    /// is not running, the socket path is wrong, or the connection
320    /// was refused).
321    WorkloadApiUnreachable(String),
322    /// The Workload API returned an empty response (no SVIDs). The
323    /// workload is not registered with SPIRE, or the agent's workload
324    /// selector does not match this process.
325    EmptyResponse,
326    /// The Workload API returned a malformed response (the protobuf
327    /// could not be parsed, or the SVID's certificate chain is
328    /// invalid).
329    MalformedResponse(String),
330    /// The gRPC call failed (the Workload API returned a non-OK
331    /// status). The message is the gRPC status message.
332    GrpcStatus(String),
333}
334
335impl std::fmt::Display for SpiffeError {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        match self {
338            SpiffeError::WorkloadApiUnreachable(m) => {
339                write!(f, "spiffe workload api unreachable: {m}")
340            }
341            SpiffeError::EmptyResponse => {
342                write!(f, "spiffe workload api returned no svids")
343            }
344            SpiffeError::MalformedResponse(m) => {
345                write!(f, "spiffe workload api malformed response: {m}")
346            }
347            SpiffeError::GrpcStatus(m) => {
348                write!(f, "spiffe workload api grpc error: {m}")
349            }
350        }
351    }
352}
353
354impl std::error::Error for SpiffeError {}
355
356// ---------------------------------------------------------------------------
357// Production gRPC transport (ent feature only: tonic + prost)
358// ---------------------------------------------------------------------------
359
360/// The production Workload API transport: a gRPC client over a Unix
361/// domain socket, using tonic (the same stack as the CP/DP transport).
362/// Gated behind the `ent` cargo feature because tonic + prost are
363/// ent-gated dependencies.
364///
365/// The SPIRE Workload API is a simple unary RPC:
366///   `FetchX509SVID(X509SVIDRequest) -> (X509SVIDResponse)`
367/// The response contains the workload's SVIDs (cert chain + private
368/// key + expiry) and the trust bundle (CA certs). The proto is
369/// hand-rolled (no protoc/build-script) using prost::Message derives,
370/// the same approach as the CP/DP transport.
371#[cfg(feature = "ent")]
372pub mod grpc {
373    use super::{SpiffeError, SpiffeIdentity, SpiffeSvid, SpiffeTrustBundle, WorkloadApiTransport};
374    use async_trait::async_trait;
375    use bytes::Buf;
376    use prost::Message as ProstMessage;
377    use std::path::Path;
378    use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder};
379    use tonic::transport::Endpoint;
380    use tonic::{Request, Status};
381
382    // -----------------------------------------------------------------
383    // Wire messages (hand-written prost structs, matching the SPIRE
384    // Workload API proto). Field numbers and types match the upstream
385    // proto definition (spire-api/workload.proto).
386    // -----------------------------------------------------------------
387
388    /// Wire: the FetchX509SVID request (empty body).
389    #[derive(Clone, PartialEq, ProstMessage)]
390    pub struct PbX509SvidRequest {}
391
392    /// Wire: a single X.509 SVID in the FetchX509SVID response.
393    #[derive(Clone, PartialEq, ProstMessage)]
394    pub struct PbX509Svid {
395        /// The SPIFFE ID (URI form: `spiffe://<trust-domain>/<path>`).
396        #[prost(string, tag = "1")]
397        pub spiffe_id: String,
398        /// The X.509 certificate chain (DER bytes, leaf first).
399        #[prost(bytes, tag = "2")]
400        pub x509_svid: Vec<u8>,
401        /// The private key matching the leaf cert (DER bytes).
402        #[prost(bytes, tag = "3")]
403        pub x509_svid_key: Vec<u8>,
404        /// The SVID expiry (seconds since the Unix epoch).
405        #[prost(int64, tag = "4")]
406        pub expires_at: i64,
407    }
408
409    /// Wire: an X.509 bundle (trust bundle for one trust domain).
410    #[derive(Clone, PartialEq, ProstMessage)]
411    pub struct PbX509Bundle {
412        /// The trust domain this bundle covers.
413        #[prost(string, tag = "1")]
414        pub trust_domain: String,
415        /// The X.509 CA certificates (DER bytes).
416        #[prost(bytes, repeated, tag = "2")]
417        pub x509_authorities: Vec<Vec<u8>>,
418    }
419
420    /// Wire: the FetchX509SVID response.
421    #[derive(Clone, PartialEq, ProstMessage)]
422    pub struct PbX509SvidResponse {
423        /// The workload's SVIDs (one per authorized SPIFFE ID).
424        #[prost(message, repeated, tag = "1")]
425        pub svids: Vec<PbX509Svid>,
426        /// The trust bundles, keyed by trust domain.
427        #[prost(map = "string, message", tag = "2")]
428        pub bundles: std::collections::HashMap<String, PbX509Bundle>,
429    }
430
431    /// Wire: the FetchX509Bundle request.
432    #[derive(Clone, PartialEq, ProstMessage)]
433    pub struct PbX509BundleRequest {
434        /// The trust domain to fetch the bundle for (empty = all).
435        #[prost(string, tag = "1")]
436        pub trust_domain: String,
437    }
438
439    /// Wire: the FetchX509Bundle response.
440    #[derive(Clone, PartialEq, ProstMessage)]
441    pub struct PbX509BundleResponse {
442        /// The trust bundles, keyed by trust domain.
443        #[prost(map = "string, message", tag = "1")]
444        pub bundles: std::collections::HashMap<String, PbX509Bundle>,
445    }
446
447    // -----------------------------------------------------------------
448    // Custom prost codec (same pattern as cp_dp/transport.rs; uses the
449    // workspace prost 0.14, not tonic's built-in ProstCodec).
450    // -----------------------------------------------------------------
451
452    /// A gRPC codec that encodes/decodes prost messages using the
453    /// workspace prost 0.14.
454    pub struct ProstCodec<T, U> {
455        _req: std::marker::PhantomData<T>,
456        _resp: std::marker::PhantomData<U>,
457    }
458
459    impl<T, U> Default for ProstCodec<T, U> {
460        fn default() -> Self {
461            ProstCodec {
462                _req: std::marker::PhantomData,
463                _resp: std::marker::PhantomData,
464            }
465        }
466    }
467
468    impl<T, U> ProstCodec<T, U> {
469        pub fn new() -> Self {
470            Self::default()
471        }
472    }
473
474    impl<T, U> Codec for ProstCodec<T, U>
475    where
476        T: ProstMessage + Clone + 'static,
477        U: ProstMessage + Default + 'static,
478    {
479        type Encode = T;
480        type Decode = U;
481        type Encoder = ProstEncoder<T>;
482        type Decoder = ProstDecoder<U>;
483
484        fn encoder(&mut self) -> Self::Encoder {
485            ProstEncoder {
486                _req: std::marker::PhantomData,
487            }
488        }
489
490        fn decoder(&mut self) -> Self::Decoder {
491            ProstDecoder {
492                _resp: std::marker::PhantomData,
493            }
494        }
495    }
496
497    /// Encoder for the custom prost codec.
498    pub struct ProstEncoder<T> {
499        _req: std::marker::PhantomData<T>,
500    }
501
502    impl<T: ProstMessage> Encoder for ProstEncoder<T> {
503        type Item = T;
504        type Error = Status;
505
506        fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
507            item.encode(dst)
508                .expect("Message only errors if not enough space");
509            Ok(())
510        }
511    }
512
513    /// Decoder for the custom prost codec.
514    pub struct ProstDecoder<U> {
515        _resp: std::marker::PhantomData<U>,
516    }
517
518    impl<U: ProstMessage + Default> Decoder for ProstDecoder<U> {
519        type Item = U;
520        type Error = Status;
521
522        fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
523            // An empty response (e.g. PbX509SvidRequest {}) encodes to
524            // zero bytes on the wire. Returning Ok(None) here makes
525            // tonic's unary() client report "Missing response message"
526            // (Internal) because it expects exactly one message. Match
527            // tonic-prost's behavior: an empty buffer yields the
528            // default value, not "no message".
529            if !src.has_remaining() {
530                return Ok(Some(U::default()));
531            }
532            let item =
533                U::decode(src).map_err(|e| Status::internal(format!("prost decode error: {e}")))?;
534            Ok(Some(item))
535        }
536    }
537
538    // -----------------------------------------------------------------
539    // The gRPC Workload API client.
540    // -----------------------------------------------------------------
541
542    /// The gRPC Workload API path for FetchX509SVID.
543    const FETCH_X509_SVID_PATH: &str = "/spiffe.workloadapi.v2.WorkloadAPI/FetchX509SVID";
544
545    /// The gRPC Workload API path for FetchX509Bundle.
546    const FETCH_X509_BUNDLE_PATH: &str = "/spiffe.workloadapi.v2.WorkloadAPI/FetchX509Bundle";
547
548    /// The gRPC Workload API transport. Connects to the SPIRE agent
549    /// over a Unix domain socket and fetches X.509 SVIDs and the
550    /// trust bundle via the `FetchX509SVID` RPC.
551    #[derive(Clone)]
552    pub struct GrpcWorkloadApi {
553        socket_path: std::path::PathBuf,
554    }
555
556    impl std::fmt::Debug for GrpcWorkloadApi {
557        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558            f.debug_struct("GrpcWorkloadApi")
559                .field("socket_path", &self.socket_path)
560                .finish()
561        }
562    }
563
564    impl GrpcWorkloadApi {
565        /// Build a new transport targeting the SPIRE Workload API at
566        /// the given Unix socket path. The connection is established
567        /// lazily on the first fetch (tonic's channel reconnects
568        /// automatically on failure).
569        pub fn new(socket_path: impl AsRef<Path>) -> Self {
570            GrpcWorkloadApi {
571                socket_path: socket_path.as_ref().to_path_buf(),
572            }
573        }
574
575        /// Connect to the Unix socket and return a tonic channel. The
576        /// Workload API is plaintext over the Unix socket (mTLS is not
577        /// used on the Workload API itself — the SPIRE agent
578        /// authenticates the workload via process attestation, not
579        /// TLS). tonic supports Unix sockets via the `unix://` URI
580        /// scheme.
581        async fn connect(&self) -> Result<tonic::transport::Channel, SpiffeError> {
582            let socket_path = self.socket_path.display().to_string();
583            let channel = Endpoint::from_shared(format!("unix://{socket_path}"))
584                .map_err(|e| SpiffeError::WorkloadApiUnreachable(e.to_string()))?
585                .connect()
586                .await
587                .map_err(|e| SpiffeError::WorkloadApiUnreachable(e.to_string()))?;
588            Ok(channel)
589        }
590
591        /// Parse the FetchX509SVID response into domain SVIDs.
592        fn parse_svid_response(
593            &self,
594            resp: PbX509SvidResponse,
595        ) -> Result<Vec<SpiffeSvid>, SpiffeError> {
596            resp.svids
597                .into_iter()
598                .map(|pb| {
599                    let spiffe_id = SpiffeIdentity::parse(&pb.spiffe_id).ok_or_else(|| {
600                        SpiffeError::MalformedResponse(format!(
601                            "invalid spiffe id in svid: {}",
602                            pb.spiffe_id
603                        ))
604                    })?;
605                    // The cert chain is a single DER blob containing
606                    // the leaf + intermediates. Split on the DER
607                    // sequence boundaries (a simple DER walk).
608                    let x509_cert = split_der_chain(&pb.x509_svid);
609                    if x509_cert.is_empty() {
610                        return Err(SpiffeError::MalformedResponse(
611                            "empty cert chain in svid".to_string(),
612                        ));
613                    }
614                    Ok(SpiffeSvid {
615                        spiffe_id,
616                        x509_cert,
617                        private_key: pb.x509_svid_key,
618                        expires_at: pb.expires_at.max(0) as u64,
619                    })
620                })
621                .collect()
622        }
623
624        /// Parse the FetchX509SVID response's bundles into a trust
625        /// bundle for the first trust domain (the common case: one
626        /// trust domain per workload).
627        #[allow(dead_code)] // tested directly; used when FetchX509SVID
628                            // returns both SVIDs and bundles in one call (the common path)
629        fn parse_bundle_from_svid_response(
630            &self,
631            resp: &PbX509SvidResponse,
632            trust_domain: &str,
633        ) -> Result<SpiffeTrustBundle, SpiffeError> {
634            let bundle = resp.bundles.get(trust_domain).ok_or_else(|| {
635                SpiffeError::MalformedResponse(format!(
636                    "trust bundle not found for trust domain: {trust_domain}"
637                ))
638            })?;
639            Ok(SpiffeTrustBundle {
640                trust_domain: bundle.trust_domain.clone(),
641                x509_certs: bundle.x509_authorities.clone(),
642            })
643        }
644    }
645
646    /// Split a DER blob containing one or more concatenated DER
647    /// certificates into individual DER blobs. Each DER certificate
648    /// starts with a SEQUENCE tag (0x30) followed by a length. The
649    /// walk reads the length to find the next certificate boundary.
650    fn split_der_chain(der: &[u8]) -> Vec<Vec<u8>> {
651        let mut certs = Vec::new();
652        let mut offset = 0;
653        while offset < der.len() {
654            // A DER SEQUENCE starts with 0x30.
655            if der[offset] != 0x30 {
656                break;
657            }
658            // The length is encoded after the tag byte. Short form:
659            // one byte (0x00-0x7F). Long form: first byte is 0x81-0x84
660            // (number of length bytes follows).
661            if offset + 1 >= der.len() {
662                break;
663            }
664            let len_byte = der[offset + 1];
665            let (len, header_len) = if len_byte < 0x80 {
666                (len_byte as usize, 2)
667            } else {
668                let num_len_bytes = (len_byte & 0x7F) as usize;
669                if num_len_bytes == 0 || num_len_bytes > 4 || offset + 2 + num_len_bytes > der.len()
670                {
671                    break;
672                }
673                let mut len = 0usize;
674                for i in 0..num_len_bytes {
675                    len = (len << 8) | der[offset + 2 + i] as usize;
676                }
677                (len, 2 + num_len_bytes)
678            };
679            let total = header_len + len;
680            if offset + total > der.len() {
681                break;
682            }
683            certs.push(der[offset..offset + total].to_vec());
684            offset += total;
685        }
686        certs
687    }
688
689    #[async_trait]
690    impl WorkloadApiTransport for GrpcWorkloadApi {
691        async fn fetch_x509_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError> {
692            let channel = self.connect().await?;
693            let codec = ProstCodec::<PbX509SvidRequest, PbX509SvidResponse>::new();
694            let mut grpc = tonic::client::Grpc::new(channel);
695
696            grpc.ready()
697                .await
698                .map_err(|e| SpiffeError::WorkloadApiUnreachable(e.to_string()))?;
699
700            let request = Request::new(PbX509SvidRequest {});
701            let path = http::uri::PathAndQuery::from_static(FETCH_X509_SVID_PATH);
702
703            let response = grpc
704                .unary(request, path, codec)
705                .await
706                .map_err(|e| SpiffeError::GrpcStatus(e.to_string()))?;
707
708            self.parse_svid_response(response.into_inner())
709        }
710
711        async fn fetch_x509_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError> {
712            // FetchX509SVID returns the bundles alongside the SVIDs;
713            // we reuse that call and extract the bundle for the
714            // configured trust domain. A dedicated FetchX509Bundle RPC
715            // exists in the SPIRE API, but FetchX509SVID is the common
716            // path (one round-trip for both SVID and bundle).
717            let channel = self.connect().await?;
718            let codec = ProstCodec::<PbX509BundleRequest, PbX509BundleResponse>::new();
719            let mut grpc = tonic::client::Grpc::new(channel);
720
721            grpc.ready()
722                .await
723                .map_err(|e| SpiffeError::WorkloadApiUnreachable(e.to_string()))?;
724
725            let request = Request::new(PbX509BundleRequest {
726                trust_domain: String::new(),
727            });
728            let path = http::uri::PathAndQuery::from_static(FETCH_X509_BUNDLE_PATH);
729
730            let response = grpc
731                .unary(request, path, codec)
732                .await
733                .map_err(|e| SpiffeError::GrpcStatus(e.to_string()))?;
734
735            let resp = response.into_inner();
736            let (_td, bundle) = resp
737                .bundles
738                .into_iter()
739                .next()
740                .ok_or(SpiffeError::EmptyResponse)?;
741            Ok(SpiffeTrustBundle {
742                trust_domain: bundle.trust_domain,
743                x509_certs: bundle.x509_authorities,
744            })
745        }
746    }
747
748    #[cfg(test)]
749    mod tests {
750        use super::*;
751
752        #[test]
753        fn split_der_chain_empty() {
754            assert!(split_der_chain(&[]).is_empty());
755        }
756
757        #[test]
758        fn split_der_chain_single() {
759            // A minimal DER SEQUENCE: tag 0x30, length 2, two bytes.
760            let der = vec![0x30, 0x02, 0xAA, 0xBB];
761            let certs = split_der_chain(&der);
762            assert_eq!(certs.len(), 1);
763            assert_eq!(certs[0], der);
764        }
765
766        #[test]
767        fn split_der_chain_multiple() {
768            // Two concatenated DER SEQUENCEs.
769            let der = vec![0x30, 0x02, 0xAA, 0xBB, 0x30, 0x01, 0xCC];
770            let certs = split_der_chain(&der);
771            assert_eq!(certs.len(), 2);
772            assert_eq!(certs[0], vec![0x30, 0x02, 0xAA, 0xBB]);
773            assert_eq!(certs[1], vec![0x30, 0x01, 0xCC]);
774        }
775
776        #[test]
777        fn split_der_chain_long_form_length() {
778            // A DER SEQUENCE with long-form length (0x81 = 1 length byte).
779            let mut der = vec![0x30, 0x81, 0x05];
780            der.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE]);
781            let certs = split_der_chain(&der);
782            assert_eq!(certs.len(), 1);
783            assert_eq!(certs[0], der);
784        }
785
786        #[test]
787        fn parse_svid_response_valid() {
788            let transport = GrpcWorkloadApi::new("/tmp/test.sock");
789            let resp = PbX509SvidResponse {
790                svids: vec![PbX509Svid {
791                    spiffe_id: "spiffe://example.org/ns/default/sa/my-svc".to_string(),
792                    x509_svid: vec![0x30, 0x02, 0xAA, 0xBB],
793                    x509_svid_key: vec![0x30, 0x02, 0xCC, 0xDD],
794                    expires_at: 9999,
795                }],
796                bundles: std::collections::HashMap::new(),
797            };
798            let svids = transport.parse_svid_response(resp).expect("valid response");
799            assert_eq!(svids.len(), 1);
800            assert_eq!(svids[0].spiffe_id.trust_domain, "example.org");
801            assert_eq!(svids[0].x509_cert.len(), 1);
802            assert_eq!(svids[0].expires_at, 9999);
803        }
804
805        #[test]
806        fn parse_svid_response_invalid_spiffe_id() {
807            let transport = GrpcWorkloadApi::new("/tmp/test.sock");
808            let resp = PbX509SvidResponse {
809                svids: vec![PbX509Svid {
810                    spiffe_id: "http://invalid".to_string(),
811                    x509_svid: vec![0x30, 0x02, 0xAA, 0xBB],
812                    x509_svid_key: vec![],
813                    expires_at: 0,
814                }],
815                bundles: std::collections::HashMap::new(),
816            };
817            let err = transport
818                .parse_svid_response(resp)
819                .expect_err("should error");
820            assert!(matches!(err, SpiffeError::MalformedResponse(_)));
821        }
822
823        #[test]
824        fn parse_svid_response_empty_chain() {
825            let transport = GrpcWorkloadApi::new("/tmp/test.sock");
826            let resp = PbX509SvidResponse {
827                svids: vec![PbX509Svid {
828                    spiffe_id: "spiffe://example.org/x".to_string(),
829                    x509_svid: vec![],
830                    x509_svid_key: vec![],
831                    expires_at: 0,
832                }],
833                bundles: std::collections::HashMap::new(),
834            };
835            let err = transport
836                .parse_svid_response(resp)
837                .expect_err("should error");
838            assert!(matches!(err, SpiffeError::MalformedResponse(_)));
839        }
840
841        #[test]
842        fn parse_bundle_from_svid_response_found() {
843            let transport = GrpcWorkloadApi::new("/tmp/test.sock");
844            let mut bundles = std::collections::HashMap::new();
845            bundles.insert(
846                "example.org".to_string(),
847                PbX509Bundle {
848                    trust_domain: "example.org".to_string(),
849                    x509_authorities: vec![vec![0x30, 0x02, 0xAA, 0xBB]],
850                },
851            );
852            let resp = PbX509SvidResponse {
853                svids: vec![],
854                bundles,
855            };
856            let bundle = transport
857                .parse_bundle_from_svid_response(&resp, "example.org")
858                .expect("found");
859            assert_eq!(bundle.trust_domain, "example.org");
860            assert_eq!(bundle.x509_certs.len(), 1);
861        }
862
863        #[test]
864        fn parse_bundle_from_svid_response_not_found() {
865            let transport = GrpcWorkloadApi::new("/tmp/test.sock");
866            let resp = PbX509SvidResponse {
867                svids: vec![],
868                bundles: std::collections::HashMap::new(),
869            };
870            let err = transport
871                .parse_bundle_from_svid_response(&resp, "missing.org")
872                .expect_err("should error");
873            assert!(matches!(err, SpiffeError::MalformedResponse(_)));
874        }
875
876        #[test]
877        fn prost_codec_default() {
878            // Verify the prost codec can be constructed.
879            let _codec = ProstCodec::<PbX509SvidRequest, PbX509SvidResponse>::new();
880        }
881    }
882}
883
884// ---------------------------------------------------------------------------
885// Test fake transport
886// ---------------------------------------------------------------------------
887
888/// A fake Workload API transport for tests. Returns preconfigured
889/// SVIDs and trust bundles without opening a socket. Deterministic
890/// (no real I/O, no timers).
891#[derive(Clone)]
892pub struct FakeWorkloadApi {
893    svids: Vec<SpiffeSvid>,
894    bundle: SpiffeTrustBundle,
895    /// When Some, fetch_x509_svid returns this error instead of the
896    /// preconfigured SVIDs. Used to test the error path.
897    svid_error: Option<SpiffeError>,
898    /// When Some, fetch_x509_bundle returns this error instead of the
899    /// preconfigured bundle. Used to test the error path.
900    bundle_error: Option<SpiffeError>,
901}
902
903impl std::fmt::Debug for FakeWorkloadApi {
904    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
905        f.debug_struct("FakeWorkloadApi")
906            .field("svids", &self.svids.len())
907            .field("bundle", &self.bundle.x509_certs.len())
908            .field("svid_error", &self.svid_error.is_some())
909            .field("bundle_error", &self.bundle_error.is_some())
910            .finish()
911    }
912}
913
914impl FakeWorkloadApi {
915    /// Build a fake transport that returns the given SVIDs and bundle.
916    pub fn new(svids: Vec<SpiffeSvid>, bundle: SpiffeTrustBundle) -> Self {
917        FakeWorkloadApi {
918            svids,
919            bundle,
920            svid_error: None,
921            bundle_error: None,
922        }
923    }
924
925    /// Make the SVID fetch return this error on the next call.
926    pub fn with_svid_error(mut self, error: SpiffeError) -> Self {
927        self.svid_error = Some(error);
928        self
929    }
930
931    /// Make the bundle fetch return this error on the next call.
932    pub fn with_bundle_error(mut self, error: SpiffeError) -> Self {
933        self.bundle_error = Some(error);
934        self
935    }
936}
937
938#[async_trait::async_trait]
939impl WorkloadApiTransport for FakeWorkloadApi {
940    async fn fetch_x509_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError> {
941        if let Some(err) = &self.svid_error {
942            return Err(err.clone());
943        }
944        Ok(self.svids.clone())
945    }
946
947    async fn fetch_x509_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError> {
948        if let Some(err) = &self.bundle_error {
949            return Err(err.clone());
950        }
951        Ok(self.bundle.clone())
952    }
953}
954
955#[cfg(test)]
956mod tests {
957    use super::*;
958
959    fn fake_svid(spiffe_id: &str, expires_at: u64) -> SpiffeSvid {
960        SpiffeSvid {
961            spiffe_id: SpiffeIdentity::parse(spiffe_id).expect("valid spiffe id"),
962            x509_cert: vec![vec![0x30, 0x82]], // minimal DER prefix
963            private_key: vec![0x30, 0x82],
964            expires_at,
965        }
966    }
967
968    fn fake_bundle() -> SpiffeTrustBundle {
969        SpiffeTrustBundle {
970            trust_domain: "example.org".into(),
971            x509_certs: vec![vec![0x30, 0x82]],
972        }
973    }
974
975    fn fake_config() -> SpiffeConfig {
976        SpiffeConfig {
977            trust_domain: "example.org".into(),
978            workload_api_socket: PathBuf::from("/tmp/spire-agent/public/api.sock"),
979            svid_refresh_interval: Duration::from_secs(60),
980        }
981    }
982
983    #[tokio::test]
984    async fn fetch_svid_returns_preconfigured_svids() {
985        let svid = fake_svid("spiffe://example.org/ns/default/sa/my-svc", 9999);
986        let transport =
987            std::sync::Arc::new(FakeWorkloadApi::new(vec![svid.clone()], fake_bundle()));
988        let client = SpiffeClient::new(fake_config(), transport);
989        let svids = client.fetch_svid().await.expect("fetch");
990        assert_eq!(svids.len(), 1);
991        assert_eq!(svids[0].spiffe_id, svid.spiffe_id);
992    }
993
994    #[tokio::test]
995    async fn fetch_primary_svid_returns_first_svid() {
996        let svid1 = fake_svid("spiffe://example.org/ns/default/sa/svc1", 9999);
997        let svid2 = fake_svid("spiffe://example.org/ns/default/sa/svc2", 8888);
998        let transport = std::sync::Arc::new(FakeWorkloadApi::new(
999            vec![svid1.clone(), svid2],
1000            fake_bundle(),
1001        ));
1002        let client = SpiffeClient::new(fake_config(), transport);
1003        let primary = client.fetch_primary_svid().await.expect("fetch");
1004        assert_eq!(primary.spiffe_id, svid1.spiffe_id);
1005    }
1006
1007    #[tokio::test]
1008    async fn fetch_primary_svid_errors_on_empty_response() {
1009        let transport = std::sync::Arc::new(FakeWorkloadApi::new(vec![], fake_bundle()));
1010        let client = SpiffeClient::new(fake_config(), transport);
1011        let err = client.fetch_primary_svid().await.expect_err("should error");
1012        assert_eq!(err, SpiffeError::EmptyResponse);
1013    }
1014
1015    #[tokio::test]
1016    async fn fetch_svid_propagates_transport_error() {
1017        let transport = std::sync::Arc::new(
1018            FakeWorkloadApi::new(vec![fake_svid("spiffe://example.org/x", 1)], fake_bundle())
1019                .with_svid_error(SpiffeError::WorkloadApiUnreachable(
1020                    "connection refused".into(),
1021                )),
1022        );
1023        let client = SpiffeClient::new(fake_config(), transport);
1024        let err = client.fetch_svid().await.expect_err("should error");
1025        assert!(matches!(err, SpiffeError::WorkloadApiUnreachable(_)));
1026    }
1027
1028    #[tokio::test]
1029    async fn fetch_trust_bundle_returns_preconfigured_bundle() {
1030        let bundle = fake_bundle();
1031        let transport = std::sync::Arc::new(FakeWorkloadApi::new(
1032            vec![fake_svid("spiffe://example.org/x", 1)],
1033            bundle.clone(),
1034        ));
1035        let client = SpiffeClient::new(fake_config(), transport);
1036        let fetched = client.fetch_trust_bundle().await.expect("fetch");
1037        assert_eq!(fetched.trust_domain, bundle.trust_domain);
1038        assert_eq!(fetched.x509_certs.len(), bundle.x509_certs.len());
1039    }
1040
1041    #[tokio::test]
1042    async fn fetch_trust_bundle_propagates_transport_error() {
1043        let transport = std::sync::Arc::new(
1044            FakeWorkloadApi::new(vec![fake_svid("spiffe://example.org/x", 1)], fake_bundle())
1045                .with_bundle_error(SpiffeError::GrpcStatus("permission denied".into())),
1046        );
1047        let client = SpiffeClient::new(fake_config(), transport);
1048        let err = client.fetch_trust_bundle().await.expect_err("should error");
1049        assert!(matches!(err, SpiffeError::GrpcStatus(_)));
1050    }
1051
1052    #[test]
1053    fn refresh_result_from_fetch_success() {
1054        let transport = std::sync::Arc::new(FakeWorkloadApi::new(vec![], fake_bundle()));
1055        let client = SpiffeClient::new(fake_config(), transport);
1056        let ok: Result<SpiffeSvid, SpiffeError> = Ok(fake_svid("spiffe://example.org/x", 1));
1057        assert_eq!(
1058            client.refresh_result_from_fetch(&ok),
1059            SvidRefreshResult::Success
1060        );
1061    }
1062
1063    #[test]
1064    fn refresh_result_from_fetch_error() {
1065        let transport = std::sync::Arc::new(FakeWorkloadApi::new(vec![], fake_bundle()));
1066        let client = SpiffeClient::new(fake_config(), transport);
1067        let err: Result<SpiffeSvid, SpiffeError> = Err(SpiffeError::EmptyResponse);
1068        assert_eq!(
1069            client.refresh_result_from_fetch(&err),
1070            SvidRefreshResult::Error
1071        );
1072    }
1073
1074    #[test]
1075    fn spiffe_identity_parse_and_to_uri_roundtrip() {
1076        let id = SpiffeIdentity::parse("spiffe://example.org/ns/default/sa/my-svc").expect("valid");
1077        assert_eq!(id.trust_domain, "example.org");
1078        assert_eq!(id.path, "/ns/default/sa/my-svc");
1079        assert_eq!(id.to_uri(), "spiffe://example.org/ns/default/sa/my-svc");
1080    }
1081
1082    #[test]
1083    fn spiffe_identity_parse_rejects_invalid() {
1084        assert!(SpiffeIdentity::parse("http://example.org/x").is_none());
1085        assert!(SpiffeIdentity::parse("spiffe:///x").is_none());
1086        assert!(SpiffeIdentity::parse("spiffe://").is_none());
1087    }
1088
1089    #[test]
1090    fn spiffe_identity_new_normalizes_path() {
1091        let id = SpiffeIdentity::new("example.org", "ns/default/sa/x");
1092        assert_eq!(id.path, "/ns/default/sa/x");
1093    }
1094
1095    #[test]
1096    fn spiffe_svid_seconds_until_expiry() {
1097        let svid = fake_svid("spiffe://example.org/x", 1000);
1098        assert_eq!(svid.seconds_until_expiry(900), 100);
1099        assert_eq!(svid.seconds_until_expiry(1000), 0);
1100        assert_eq!(svid.seconds_until_expiry(2000), 0); // clamped
1101    }
1102}