1use 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#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
19pub struct SpiffeId {
20 trust_domain: TrustDomain,
21 path: String,
22}
23
24#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
26pub struct TrustDomain {
27 name: String,
28}
29
30#[derive(Debug, Error, PartialEq, Clone)]
32#[non_exhaustive]
33pub enum SpiffeIdError {
34 #[error("cannot be empty")]
36 Empty,
37
38 #[error("trust domain is missing")]
40 MissingTrustDomain,
41
42 #[error("scheme is missing or invalid")]
44 WrongScheme,
45
46 #[error(
48 "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and \
49 underscores"
50 )]
51 BadTrustDomainChar,
52
53 #[error(
55 "path segment characters are limited to letters, numbers, dots, dashes, and underscores"
56 )]
57 BadPathSegmentChar,
58
59 #[error("path cannot contain empty segments")]
61 EmptySegment,
62
63 #[error("path cannot contain dot segments")]
65 DotSegment,
66
67 #[error("path cannot have a trailing slash")]
69 TrailingSlash,
70}
71
72impl SpiffeId {
73 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 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 pub fn trust_domain(&self) -> &TrustDomain {
168 &self.trust_domain
169 }
170
171 pub fn path(&self) -> &str {
173 &self.path
174 }
175
176 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
210pub 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 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 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 for i in 0..=255_u8 {
510 let c = i as char;
511
512 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 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 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}