spiffe/spiffe_id/
mod.rs

1//! SPIFFE-ID and TrustDomain types compliant with the SPIFFE standard.
2
3use std::convert::TryFrom;
4use std::fmt;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7
8use thiserror::Error;
9
10const SPIFFE_SCHEME: &str = "spiffe";
11const SCHEME_PREFIX: &str = "spiffe://";
12
13const VALID_TRUST_DOMAIN_CHARS: &str = "abcdefghijklmnopqrstuvwxyz0123456789-._";
14const VALID_PATH_SEGMENT_CHARS: &str =
15    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._";
16
17/// Represents a [SPIFFE ID](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#2-spiffe-identity).
18#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
19pub struct SpiffeId {
20    trust_domain: TrustDomain,
21    path: String,
22}
23
24/// Represents a [SPIFFE Trust domain](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#21-trust-domain)
25#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
26pub struct TrustDomain {
27    name: String,
28}
29
30/// An error that can arise parsing a SPIFFE ID.
31#[derive(Debug, Error, PartialEq, Clone)]
32#[non_exhaustive]
33pub enum SpiffeIdError {
34    /// An empty string cannot be parsed as a SPIFFE ID.
35    #[error("cannot be empty")]
36    Empty,
37
38    /// The trust domain name of SPIFFE ID cannot be empty.
39    #[error("trust domain is missing")]
40    MissingTrustDomain,
41
42    /// A SPIFFE ID must have a scheme 'spiffe'.
43    #[error("scheme is missing or invalid")]
44    WrongScheme,
45
46    /// A trust domain name can only contain chars in a limited char set.
47    #[error(
48        "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and \
49         underscores"
50    )]
51    BadTrustDomainChar,
52
53    /// A path segment can only contain chars in a limited char set.
54    #[error(
55        "path segment characters are limited to letters, numbers, dots, dashes, and underscores"
56    )]
57    BadPathSegmentChar,
58
59    /// Path cannot contain empty segments, e.g '//'
60    #[error("path cannot contain empty segments")]
61    EmptySegment,
62
63    /// Path cannot contain dot segments, e.g '/.', '/..'
64    #[error("path cannot contain dot segments")]
65    DotSegment,
66
67    /// Path cannot have a trailing slash.
68    #[error("path cannot have a trailing slash")]
69    TrailingSlash,
70}
71
72impl SpiffeId {
73    /// Attempts to parse a SPIFFE ID from the given id string.
74    ///
75    /// # Arguments
76    ///
77    /// * `id` - A SPIFFE ID, e.g. 'spiffe://trustdomain/path/other'
78    ///
79    /// # Errors
80    ///
81    /// If the function cannot parse the input as a SPIFFE ID, a [`SpiffeIdError`] variant will be returned.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use spiffe::SpiffeId;
87    ///
88    /// let spiffe_id = SpiffeId::new("spiffe://trustdomain/path").unwrap();
89    /// assert_eq!("trustdomain", spiffe_id.trust_domain().to_string());
90    /// assert_eq!("/path", spiffe_id.path());
91    /// ```
92    pub fn new(id: &str) -> Result<Self, SpiffeIdError> {
93        if id.is_empty() {
94            return Err(SpiffeIdError::Empty);
95        }
96
97        if !id.starts_with(SCHEME_PREFIX) {
98            return Err(SpiffeIdError::WrongScheme);
99        }
100
101        let rest = &id[SCHEME_PREFIX.len()..];
102        let i = rest.find('/').unwrap_or(rest.len());
103
104        if i == 0 {
105            return Err(SpiffeIdError::MissingTrustDomain);
106        }
107
108        let td = &rest[..i];
109        if td.chars().any(|c| !is_valid_trust_domain_char(c)) {
110            return Err(SpiffeIdError::BadTrustDomainChar);
111        }
112
113        let path = &rest[i..];
114
115        if !path.is_empty() {
116            validate_path(path)?;
117        }
118
119        let trust_domain = TrustDomain {
120            name: td.to_string(),
121        };
122        let path = path.to_string();
123        Ok(SpiffeId { trust_domain, path })
124    }
125
126    /// Returns a new SPIFFE ID in the given trust domain with joined
127    /// path segments. The path segments must be valid according to the SPIFFE
128    /// specification and must not contain path separators.
129    /// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
130    ///
131    /// # Arguments
132    ///
133    /// * `trust_domain` - A [`TrustDomain`] object.
134    /// * `segments` - A slice of path segments.
135    ///
136    /// # Errors
137    ///
138    /// If the segments contain not allowed characters, a [`SpiffeIdError`] variant will be returned.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use spiffe::{SpiffeId, TrustDomain};
144    ///
145    /// let trust_domain = TrustDomain::new("trustdomain").unwrap();
146    /// let spiffe_id = SpiffeId::from_segments(trust_domain, &["path1", "path2", "path3"]).unwrap();
147    /// assert_eq!(
148    ///     "spiffe://trustdomain/path1/path2/path3",
149    ///     spiffe_id.to_string()
150    /// );
151    /// ```
152    pub fn from_segments(
153        trust_domain: TrustDomain,
154        segments: &[&str],
155    ) -> Result<Self, SpiffeIdError> {
156        let mut path = String::new();
157        for p in segments {
158            validate_path(p)?;
159            path.push('/');
160            path.push_str(p);
161        }
162
163        Ok(SpiffeId { trust_domain, path })
164    }
165
166    /// Returns the trust domain of the SPIFFE ID.
167    pub fn trust_domain(&self) -> &TrustDomain {
168        &self.trust_domain
169    }
170
171    /// Returns the path of the SPIFFE ID.
172    pub fn path(&self) -> &str {
173        &self.path
174    }
175
176    /// Returns `true` if this SPIFFE ID has the given TrustDomain.
177    pub fn is_member_of(&self, trust_domain: &TrustDomain) -> bool {
178        &self.trust_domain == trust_domain
179    }
180}
181
182impl Display for SpiffeId {
183    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
184        write!(f, "{}://{}{}", SPIFFE_SCHEME, self.trust_domain, self.path)
185    }
186}
187
188impl FromStr for SpiffeId {
189    type Err = SpiffeIdError;
190
191    fn from_str(id: &str) -> Result<Self, Self::Err> {
192        Self::new(id)
193    }
194}
195
196impl TryFrom<String> for SpiffeId {
197    type Error = SpiffeIdError;
198    fn try_from(s: String) -> Result<SpiffeId, Self::Error> {
199        Self::new(s.as_ref())
200    }
201}
202
203impl TryFrom<&str> for SpiffeId {
204    type Error = SpiffeIdError;
205    fn try_from(s: &str) -> Result<SpiffeId, Self::Error> {
206        Self::new(s)
207    }
208}
209
210/// Validates that a path string is a conformant path for a SPIFFE ID.
211/// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
212pub fn validate_path(path: &str) -> Result<(), SpiffeIdError> {
213    if path.is_empty() {
214        return Err(SpiffeIdError::Empty);
215    }
216
217    let chars = path.char_indices().peekable();
218    let mut segment_start = 0;
219
220    for (idx, c) in chars {
221        if c == '/' {
222            match &path[segment_start..idx] {
223                "/" => return Err(SpiffeIdError::EmptySegment),
224                "/." | "/.." => return Err(SpiffeIdError::DotSegment),
225                _ => {}
226            }
227            segment_start = idx;
228            continue;
229        }
230
231        if !is_valid_path_segment_char(c) {
232            return Err(SpiffeIdError::BadPathSegmentChar);
233        }
234    }
235
236    match &path[segment_start..] {
237        "/" => return Err(SpiffeIdError::TrailingSlash),
238        "/." | "/.." => return Err(SpiffeIdError::DotSegment),
239        _ => {}
240    }
241
242    Ok(())
243}
244
245fn is_valid_path_segment_char(c: char) -> bool {
246    VALID_PATH_SEGMENT_CHARS.contains(c)
247}
248
249impl TrustDomain {
250    /// Attempts to parse a TrustDomain instance from the given name or spiffe_id string.
251    ///
252    /// # Arguments
253    ///
254    /// * `id_or_name` - Name of a trust domain, it also can be a SPIFFE ID string from which the domain name
255    ///   is extracted.
256    ///
257    /// # Errors
258    ///
259    /// If the function cannot parse the input as a Trust domain, a [`SpiffeIdError`] variant will be returned.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use spiffe::TrustDomain;
265    ///
266    /// let trust_domain = TrustDomain::new("domain.test").unwrap();
267    /// assert_eq!("domain.test", trust_domain.to_string());
268    /// assert_eq!("spiffe://domain.test", trust_domain.id_string());
269    ///
270    /// let trust_domain = TrustDomain::new("spiffe://example.org/path").unwrap();
271    /// assert_eq!("example.org", trust_domain.to_string());
272    /// assert_eq!("spiffe://example.org", trust_domain.id_string());
273    /// ```
274    pub fn new(id_or_name: &str) -> Result<Self, SpiffeIdError> {
275        if id_or_name.is_empty() {
276            return Err(SpiffeIdError::MissingTrustDomain);
277        }
278
279        match id_or_name.find(":/") {
280            Some(_) => {
281                let spiffe_id = SpiffeId::try_from(id_or_name)?;
282                Ok(spiffe_id.trust_domain)
283            }
284            None => {
285                validate_trust_domain_name(id_or_name)?;
286                Ok(TrustDomain {
287                    name: id_or_name.to_string(),
288                })
289            }
290        }
291    }
292
293    /// Returns a string representation of the SPIFFE ID of the trust domain,
294    /// e.g. "spiffe://example.org".
295    pub fn id_string(&self) -> String {
296        format!("{}://{}", SPIFFE_SCHEME, self.name)
297    }
298}
299
300impl Display for TrustDomain {
301    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
302        write!(f, "{}", self.name)
303    }
304}
305
306impl AsRef<str> for TrustDomain {
307    fn as_ref(&self) -> &str {
308        self.name.as_str()
309    }
310}
311
312impl FromStr for TrustDomain {
313    type Err = SpiffeIdError;
314
315    fn from_str(name: &str) -> Result<Self, Self::Err> {
316        TrustDomain::new(name)
317    }
318}
319
320impl TryFrom<&str> for TrustDomain {
321    type Error = SpiffeIdError;
322
323    fn try_from(name: &str) -> Result<Self, Self::Error> {
324        Self::new(name)
325    }
326}
327
328impl TryFrom<String> for TrustDomain {
329    type Error = SpiffeIdError;
330
331    fn try_from(value: String) -> Result<Self, Self::Error> {
332        Self::new(value.as_ref())
333    }
334}
335
336fn validate_trust_domain_name(name: &str) -> Result<(), SpiffeIdError> {
337    if name.chars().all(is_valid_trust_domain_char) {
338        Ok(())
339    } else {
340        Err(SpiffeIdError::BadTrustDomainChar)
341    }
342}
343
344fn is_valid_trust_domain_char(c: char) -> bool {
345    VALID_TRUST_DOMAIN_CHARS.contains(c)
346}
347
348#[cfg(test)]
349mod spiffe_id_tests {
350    use std::str::FromStr;
351
352    use super::*;
353
354    macro_rules! spiffe_id_success_tests {
355        ($($name:ident: $value:expr,)*) => {
356        $(
357            #[test]
358            fn $name() {
359                let (input, expected) = $value;
360                let spiffe_id = SpiffeId::from_str(input).unwrap();
361                assert_eq!(spiffe_id, expected);
362            }
363        )*
364        }
365    }
366
367    spiffe_id_success_tests! {
368        from_valid_spiffe_id_str: (
369            "spiffe://trustdomain",
370            SpiffeId {
371                trust_domain: TrustDomain::from_str("trustdomain").unwrap(),
372                path: "".to_string(),
373            }
374        ),
375        from_valid_uri_str: (
376            "spiffe://trustdomain/path/element",
377            SpiffeId {
378                trust_domain: TrustDomain::from_str("trustdomain").unwrap(),
379                path: "/path/element".to_string(),
380            }
381        ),
382    }
383
384    #[test]
385    fn test_is_member_of() {
386        let spiffe_id = SpiffeId::from_str("spiffe://example.org").unwrap();
387        let trust_domain = TrustDomain::from_str("example.org").unwrap();
388
389        assert!(spiffe_id.is_member_of(&trust_domain));
390    }
391
392    #[test]
393    fn test_new_from_string() {
394        let id_string = String::from("spiffe://example.org/path/element");
395        let spiffe_id = SpiffeId::try_from(id_string).unwrap();
396
397        let expected_trust_domain = TrustDomain::from_str("example.org").unwrap();
398
399        assert_eq!(spiffe_id.trust_domain, expected_trust_domain);
400        assert_eq!(spiffe_id.path(), "/path/element");
401    }
402
403    #[test]
404    fn test_to_string() {
405        let spiffe_id = SpiffeId::from_str("spiffe://example.org/path/element").unwrap();
406        assert_eq!(spiffe_id.to_string(), "spiffe://example.org/path/element");
407    }
408
409    #[test]
410    fn test_try_from_str() {
411        let spiffe_id = SpiffeId::try_from("spiffe://example.org/path").unwrap();
412
413        assert_eq!(
414            spiffe_id.trust_domain,
415            TrustDomain::from_str("example.org").unwrap()
416        );
417        assert_eq!(spiffe_id.path, "/path");
418    }
419
420    #[test]
421    fn test_try_from_string() {
422        let spiffe_id = SpiffeId::try_from(String::from("spiffe://example.org/path")).unwrap();
423
424        assert_eq!(
425            spiffe_id.trust_domain,
426            TrustDomain::from_str("example.org").unwrap()
427        );
428        assert_eq!(spiffe_id.path, "/path");
429    }
430
431    macro_rules! spiffe_id_error_tests {
432        ($($name:ident: $value:expr,)*) => {
433        $(
434            #[test]
435            fn $name() {
436            let (input, expected_error) = $value;
437                let spiffe_id = SpiffeId::from_str(input);
438                let error = spiffe_id.unwrap_err();
439
440                assert_eq!(error, expected_error);
441            }
442        )*
443        }
444    }
445
446    spiffe_id_error_tests! {
447        from_empty_str: ("", SpiffeIdError::Empty),
448        from_str_invalid_uri_str_contains_ip_address: (
449            "192.168.2.2:6688",
450            SpiffeIdError::WrongScheme,
451        ),
452        from_str_uri_str_invalid_scheme: (
453            "http://domain.test/path/element",
454            SpiffeIdError::WrongScheme,
455        ),
456        from_str_uri_str_empty_authority: (
457            "spiffe:/path/element",
458            SpiffeIdError::WrongScheme,
459        ),
460        from_str_uri_str_empty_authority_after_slashes: (
461            "spiffe:///path/element",
462            SpiffeIdError::MissingTrustDomain,
463        ),
464        from_str_uri_str_empty_authority_no_slashes: (
465            "spiffe:path/element",
466            SpiffeIdError::WrongScheme,
467        ),
468        from_str_uri_str_with_query: (
469            "spiffe://domain.test/path/element?query=1",
470            SpiffeIdError::BadPathSegmentChar,
471        ),
472        from_str_uri_str_with_fragment: (
473            "spiffe://domain.test/path/element#fragment-1",
474            SpiffeIdError::BadPathSegmentChar,
475        ),
476        from_str_uri_str_with_port: (
477            "spiffe://domain.test:8080/path/element",
478            SpiffeIdError::BadTrustDomainChar,
479        ),
480        from_str_uri_str_with_user_info: (
481            "spiffe://user:password@test.org/path/element",
482            SpiffeIdError::BadTrustDomainChar,
483        ),
484        from_str_uri_str_with_trailing_slash: (
485            "spiffe://test.org/",
486            SpiffeIdError::TrailingSlash,
487        ),
488        from_str_uri_str_with_emtpy_segment: (
489            "spiffe://test.org//",
490            SpiffeIdError::EmptySegment,
491        ),
492        from_str_uri_str_with_path_with_trailing_slash: (
493            "spiffe://test.org/path/other/",
494            SpiffeIdError::TrailingSlash,
495        ),
496        from_str_uri_str_with_dot_segment: (
497            "spiffe://test.org/./other",
498            SpiffeIdError::DotSegment,
499        ),
500        from_str_uri_str_with_double_dot_segment: (
501            "spiffe://test.org/../other",
502            SpiffeIdError::DotSegment,
503        ),
504    }
505
506    #[test]
507    fn test_parse_with_all_chars() {
508        // Go all the way through 255, which ensures we reject UTF-8 appropriately
509        for i in 0..=255_u8 {
510            let c = i as char;
511
512            // Don't test '/' since it is the delimiter between path segments
513            if c == '/' {
514                continue;
515            }
516
517            let path = format!("/path{}", c);
518            let id = format!("spiffe://trustdomain{}", path);
519
520            if VALID_PATH_SEGMENT_CHARS.contains(c) {
521                let spiffe_id = SpiffeId::new(&id).unwrap();
522                assert_eq!(spiffe_id.to_string(), id)
523            } else {
524                assert_eq!(
525                    SpiffeId::new(&id).unwrap_err(),
526                    SpiffeIdError::BadPathSegmentChar
527                );
528            }
529
530            let td = format!("spiffe://trustdomain{}", c);
531
532            if VALID_TRUST_DOMAIN_CHARS.contains(c) {
533                let spiffe_id = SpiffeId::new(&td).unwrap();
534                assert_eq!(spiffe_id.to_string(), td)
535            } else {
536                assert_eq!(
537                    SpiffeId::new(&td).unwrap_err(),
538                    SpiffeIdError::BadTrustDomainChar
539                );
540            }
541        }
542    }
543
544    #[test]
545    fn test_from_segments_with_all_chars() {
546        // Go all the way through 255, which ensures we reject UTF-8 appropriately
547        for i in 0..=255_u8 {
548            let c = i as char;
549
550            let path = format!("path{}", c);
551            let trust_domain = TrustDomain::new("trustdomain").unwrap();
552
553            if VALID_PATH_SEGMENT_CHARS.contains(c) {
554                let spiffe_id = SpiffeId::from_segments(trust_domain, &[path.as_str()]).unwrap();
555                assert_eq!(
556                    spiffe_id.to_string(),
557                    format!("spiffe://trustdomain/{}", path)
558                )
559            } else if c == '/' {
560                assert_eq!(
561                    SpiffeId::from_segments(trust_domain, &[path.as_str()]).unwrap_err(),
562                    SpiffeIdError::TrailingSlash
563                );
564            } else {
565                assert_eq!(
566                    SpiffeId::from_segments(trust_domain, &[path.as_str()]).unwrap_err(),
567                    SpiffeIdError::BadPathSegmentChar
568                );
569            }
570        }
571    }
572}
573
574#[cfg(test)]
575mod trust_domain_tests {
576    use super::*;
577    use std::str::FromStr;
578
579    macro_rules! trust_domain_success_tests {
580        ($($name:ident: $value:expr,)*) => {
581        $(
582            #[test]
583            fn $name() {
584                let (input, expected) = $value;
585                let trust_domain = TrustDomain::new(input).unwrap();
586                assert_eq!(trust_domain, expected);
587            }
588        )*
589        }
590    }
591
592    trust_domain_success_tests! {
593        from_str_domain: ("trustdomain", TrustDomain{name: "trustdomain".to_string()}),
594        from_str_spiffeid: ("spiffe://other.test", TrustDomain{name: "other.test".to_string()}),
595        from_str_spiffeid_with_path: ("spiffe://domain.test/path/element", TrustDomain{name: "domain.test".to_string()}),
596    }
597
598    macro_rules! trust_domain_error_tests {
599        ($($name:ident: $value:expr,)*) => {
600        $(
601            #[test]
602            fn $name() {
603                let (input, expected_error) = $value;
604                let trust_domain = TrustDomain::new(input);
605                let error = trust_domain.unwrap_err();
606                assert_eq!(error, expected_error);
607            }
608        )*
609        }
610    }
611
612    trust_domain_error_tests! {
613        from_empty_str: ("", SpiffeIdError::MissingTrustDomain),
614        from_invalid_scheme:  ("other://domain.test", SpiffeIdError::WrongScheme),
615        from_uri_with_port: ("spiffe://domain.test:80", SpiffeIdError::BadTrustDomainChar),
616        from_uri_with_userinfo: ("spiffe://user:pass@domain.test", SpiffeIdError::BadTrustDomainChar),
617        from_uri_with_invalid_domain: ("spiffe:// domain.test", SpiffeIdError::BadTrustDomainChar),
618        from_uri_with_empty_scheme: ("://domain.test", SpiffeIdError::WrongScheme),
619        from_uri_with_empty_domain: ("spiffe:///path", SpiffeIdError::MissingTrustDomain),
620    }
621
622    #[test]
623    fn test_equals() {
624        let td_1 = TrustDomain::new("domain.test").unwrap();
625        let td_2 = TrustDomain::new("domain.test").unwrap();
626        assert_eq!(td_1, td_2);
627    }
628
629    #[test]
630    fn test_not_equals() {
631        let td_1 = TrustDomain::new("domain.test").unwrap();
632        let td_2 = TrustDomain::new("other.test").unwrap();
633        assert_ne!(td_1, td_2);
634    }
635
636    #[test]
637    fn test_to_string() {
638        let trust_domain = TrustDomain::from_str("spiffe://example.org").unwrap();
639        assert_eq!(trust_domain.to_string(), "example.org");
640    }
641
642    #[test]
643    fn test_to_id_string() {
644        let trust_domain = TrustDomain::from_str("example.org").unwrap();
645        assert_eq!(trust_domain.id_string(), "spiffe://example.org");
646    }
647
648    #[test]
649    fn test_try_from_str() {
650        let trust_domain = TrustDomain::try_from("example.org").unwrap();
651        assert_eq!(trust_domain.to_string(), "example.org");
652    }
653
654    #[test]
655    fn test_try_from_string() {
656        let trust_domain = TrustDomain::try_from(String::from("example.org")).unwrap();
657        assert_eq!(trust_domain.to_string(), "example.org");
658    }
659
660    #[test]
661    fn test_parse_with_all_chars() {
662        // Go all the way through 255, which ensures we reject UTF-8 appropriately
663        for i in 0..=255_u8 {
664            let c = i as char;
665            let td = format!("trustdomain{}", c);
666
667            if VALID_TRUST_DOMAIN_CHARS.contains(c) {
668                let trust_domain = TrustDomain::new(&td).unwrap();
669                assert_eq!(trust_domain.to_string(), td)
670            } else {
671                assert_eq!(
672                    TrustDomain::new(&td).unwrap_err(),
673                    SpiffeIdError::BadTrustDomainChar
674                );
675            }
676        }
677    }
678}