1use crate::config::mesh::MeshSpiffeConfig;
35use std::path::PathBuf;
36use std::time::Duration;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SpiffeIdentity {
48 pub trust_domain: String,
51 pub path: String,
54}
55
56impl SpiffeIdentity {
57 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 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 pub fn to_uri(&self) -> String {
86 format!("spiffe://{}{}", self.trust_domain, self.path)
87 }
88
89 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#[derive(Debug, Clone)]
109pub struct SpiffeSvid {
110 pub spiffe_id: SpiffeIdentity,
112 pub x509_cert: Vec<Vec<u8>>,
115 pub private_key: Vec<u8>,
119 pub expires_at: u64,
122}
123
124impl SpiffeSvid {
125 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#[derive(Debug, Clone)]
138pub struct SpiffeTrustBundle {
139 pub trust_domain: String,
141 pub x509_certs: Vec<Vec<u8>>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct SpiffeConfig {
152 pub trust_domain: String,
155 pub workload_api_socket: PathBuf,
158 pub svid_refresh_interval: Duration,
162}
163
164impl SpiffeConfig {
165 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#[async_trait::async_trait]
183pub trait WorkloadApiTransport: Send + Sync + 'static {
184 async fn fetch_x509_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError>;
190
191 async fn fetch_x509_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError>;
196}
197
198#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum SvidRefreshResult {
228 Success,
230 Error,
235}
236
237impl SvidRefreshResult {
238 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 pub fn new(config: SpiffeConfig, transport: std::sync::Arc<dyn WorkloadApiTransport>) -> Self {
253 SpiffeClient { config, transport }
254 }
255
256 pub fn config(&self) -> &SpiffeConfig {
258 &self.config
259 }
260
261 pub fn workload_identity(&self, path: &str) -> SpiffeIdentity {
265 SpiffeIdentity::new(&self.config.trust_domain, path)
266 }
267
268 pub async fn fetch_svid(&self) -> Result<Vec<SpiffeSvid>, SpiffeError> {
274 self.transport.fetch_x509_svid().await
275 }
276
277 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 pub async fn fetch_trust_bundle(&self) -> Result<SpiffeTrustBundle, SpiffeError> {
293 self.transport.fetch_x509_bundle().await
294 }
295
296 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#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum SpiffeError {
318 WorkloadApiUnreachable(String),
322 EmptyResponse,
326 MalformedResponse(String),
330 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#[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 #[derive(Clone, PartialEq, ProstMessage)]
390 pub struct PbX509SvidRequest {}
391
392 #[derive(Clone, PartialEq, ProstMessage)]
394 pub struct PbX509Svid {
395 #[prost(string, tag = "1")]
397 pub spiffe_id: String,
398 #[prost(bytes, tag = "2")]
400 pub x509_svid: Vec<u8>,
401 #[prost(bytes, tag = "3")]
403 pub x509_svid_key: Vec<u8>,
404 #[prost(int64, tag = "4")]
406 pub expires_at: i64,
407 }
408
409 #[derive(Clone, PartialEq, ProstMessage)]
411 pub struct PbX509Bundle {
412 #[prost(string, tag = "1")]
414 pub trust_domain: String,
415 #[prost(bytes, repeated, tag = "2")]
417 pub x509_authorities: Vec<Vec<u8>>,
418 }
419
420 #[derive(Clone, PartialEq, ProstMessage)]
422 pub struct PbX509SvidResponse {
423 #[prost(message, repeated, tag = "1")]
425 pub svids: Vec<PbX509Svid>,
426 #[prost(map = "string, message", tag = "2")]
428 pub bundles: std::collections::HashMap<String, PbX509Bundle>,
429 }
430
431 #[derive(Clone, PartialEq, ProstMessage)]
433 pub struct PbX509BundleRequest {
434 #[prost(string, tag = "1")]
436 pub trust_domain: String,
437 }
438
439 #[derive(Clone, PartialEq, ProstMessage)]
441 pub struct PbX509BundleResponse {
442 #[prost(map = "string, message", tag = "1")]
444 pub bundles: std::collections::HashMap<String, PbX509Bundle>,
445 }
446
447 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 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 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 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 const FETCH_X509_SVID_PATH: &str = "/spiffe.workloadapi.v2.WorkloadAPI/FetchX509SVID";
544
545 const FETCH_X509_BUNDLE_PATH: &str = "/spiffe.workloadapi.v2.WorkloadAPI/FetchX509Bundle";
547
548 #[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 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 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 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 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 #[allow(dead_code)] 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 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 if der[offset] != 0x30 {
656 break;
657 }
658 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 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 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 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 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 let _codec = ProstCodec::<PbX509SvidRequest, PbX509SvidResponse>::new();
880 }
881 }
882}
883
884#[derive(Clone)]
892pub struct FakeWorkloadApi {
893 svids: Vec<SpiffeSvid>,
894 bundle: SpiffeTrustBundle,
895 svid_error: Option<SpiffeError>,
898 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 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 pub fn with_svid_error(mut self, error: SpiffeError) -> Self {
927 self.svid_error = Some(error);
928 self
929 }
930
931 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]], 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); }
1102}