1package connect 2 3import ( 4 "fmt" 5 "net/url" 6 "regexp" 7 "strings" 8) 9 10// CertURI represents a Connect-valid URI value for a TLS certificate. 11// The user should type switch on the various implementations in this 12// package to determine the type of URI and the data encoded within it. 13// 14// Note that the current implementations of this are all also SPIFFE IDs. 15// However, we anticipate that we may accept URIs that are also not SPIFFE 16// compliant and therefore the interface is named as such. 17type CertURI interface { 18 // URI is the valid URI value used in the cert. 19 URI() *url.URL 20} 21 22var ( 23 spiffeIDServiceRegexp = regexp.MustCompile( 24 `^/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`) 25 spiffeIDAgentRegexp = regexp.MustCompile( 26 `^/agent/client/dc/([^/]+)/id/([^/]+)$`) 27) 28 29// ParseCertURIFromString attempts to parse a string representation of a 30// certificate URI as a convenience helper around ParseCertURI. 31func ParseCertURIFromString(input string) (CertURI, error) { 32 // Parse the certificate URI from the string 33 uriRaw, err := url.Parse(input) 34 if err != nil { 35 return nil, err 36 } 37 return ParseCertURI(uriRaw) 38} 39 40// ParseCertURI parses a the URI value from a TLS certificate. 41func ParseCertURI(input *url.URL) (CertURI, error) { 42 if input.Scheme != "spiffe" { 43 return nil, fmt.Errorf("SPIFFE ID must have 'spiffe' scheme") 44 } 45 46 // Path is the raw value of the path without url decoding values. 47 // RawPath is empty if there were no encoded values so we must 48 // check both. 49 path := input.Path 50 if input.RawPath != "" { 51 path = input.RawPath 52 } 53 54 // Test for service IDs 55 if v := spiffeIDServiceRegexp.FindStringSubmatch(path); v != nil { 56 // Determine the values. We assume they're sane to save cycles, 57 // but if the raw path is not empty that means that something is 58 // URL encoded so we go to the slow path. 59 ns := v[1] 60 dc := v[2] 61 service := v[3] 62 if input.RawPath != "" { 63 var err error 64 if ns, err = url.PathUnescape(v[1]); err != nil { 65 return nil, fmt.Errorf("Invalid namespace: %s", err) 66 } 67 if dc, err = url.PathUnescape(v[2]); err != nil { 68 return nil, fmt.Errorf("Invalid datacenter: %s", err) 69 } 70 if service, err = url.PathUnescape(v[3]); err != nil { 71 return nil, fmt.Errorf("Invalid service: %s", err) 72 } 73 } 74 75 return &SpiffeIDService{ 76 Host: input.Host, 77 Namespace: ns, 78 Datacenter: dc, 79 Service: service, 80 }, nil 81 } else if v := spiffeIDAgentRegexp.FindStringSubmatch(path); v != nil { 82 // Determine the values. We assume they're sane to save cycles, 83 // but if the raw path is not empty that means that something is 84 // URL encoded so we go to the slow path. 85 dc := v[1] 86 agent := v[2] 87 if input.RawPath != "" { 88 var err error 89 if dc, err = url.PathUnescape(v[1]); err != nil { 90 return nil, fmt.Errorf("Invalid datacenter: %s", err) 91 } 92 if agent, err = url.PathUnescape(v[2]); err != nil { 93 return nil, fmt.Errorf("Invalid node: %s", err) 94 } 95 } 96 97 return &SpiffeIDAgent{ 98 Host: input.Host, 99 Datacenter: dc, 100 Agent: agent, 101 }, nil 102 } 103 104 // Test for signing ID 105 if input.Path == "" { 106 idx := strings.Index(input.Host, ".") 107 if idx > 0 { 108 return &SpiffeIDSigning{ 109 ClusterID: input.Host[:idx], 110 Domain: input.Host[idx+1:], 111 }, nil 112 } 113 } 114 115 return nil, fmt.Errorf("SPIFFE ID is not in the expected format: %s", input.String()) 116} 117