diff --git a/agent/connect/ca.go b/agent/connect/ca.go new file mode 100644 index 0000000000..90c4845290 --- /dev/null +++ b/agent/connect/ca.go @@ -0,0 +1,59 @@ +package connect + +import ( + "crypto" + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// ParseCert parses the x509 certificate from a PEM-encoded value. +func ParseCert(pemValue string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(pemValue)) + if block == nil { + return nil, fmt.Errorf("no PEM-encoded data found") + } + + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type") + } + + return x509.ParseCertificate(block.Bytes) +} + +// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key +// is expected to be the first block in the PEM value. +func ParseSigner(pemValue string) (crypto.Signer, error) { + block, _ := pem.Decode([]byte(pemValue)) + if block == nil { + return nil, fmt.Errorf("no PEM-encoded data found") + } + + switch block.Type { + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(block.Bytes) + + default: + return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type) + } +} + +// KeyId returns a x509 KeyId from the given signing key. The key must be +// an *ecdsa.PublicKey, but is an interface type to support crypto.Signer. +func KeyId(raw interface{}) ([]byte, error) { + pub, ok := raw.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("invalid key type: %T", raw) + } + + // This is not standard; RFC allows any unique identifier as long as they + // match in subject/authority chains but suggests specific hashing of DER + // bytes of public key including DER tags. I can't be bothered to do esp. + // since ECDSA keys don't have a handy way to marshal the publick key alone. + h := sha256.New() + h.Write(pub.X.Bytes()) + h.Write(pub.Y.Bytes()) + return h.Sum([]byte{}), nil +} diff --git a/agent/connect/spiffe.go b/agent/connect/spiffe.go new file mode 100644 index 0000000000..58a6b83e32 --- /dev/null +++ b/agent/connect/spiffe.go @@ -0,0 +1,55 @@ +package connect + +import ( + "fmt" + "net/url" + "regexp" +) + +// SpiffeID represents a Connect-valid SPIFFE ID. The user should type switch +// on the various implementations in this package to determine the type of ID. +type SpiffeID interface { + URI() *url.URL +} + +var ( + spiffeIDServiceRegexp = regexp.MustCompile( + `^/ns/(\w+)/dc/(\w+)/svc/(\w+)$`) +) + +// ParseSpiffeID parses a SPIFFE ID from the input URI. +func ParseSpiffeID(input *url.URL) (SpiffeID, error) { + if input.Scheme != "spiffe" { + return nil, fmt.Errorf("SPIFFE ID must have 'spiffe' scheme") + } + + // Test for service IDs + if v := spiffeIDServiceRegexp.FindStringSubmatch(input.Path); v != nil { + return &SpiffeIDService{ + Host: input.Host, + Namespace: v[1], + Datacenter: v[2], + Service: v[3], + }, nil + } + + return nil, fmt.Errorf("SPIFFE ID is not in the expected format") +} + +// SpiffeIDService is the structure to represent the SPIFFE ID for a service. +type SpiffeIDService struct { + Host string + Namespace string + Datacenter string + Service string +} + +// URI returns the *url.URL for this SPIFFE ID. +func (id *SpiffeIDService) URI() *url.URL { + var result url.URL + result.Scheme = "spiffe" + result.Host = id.Host + result.Path = fmt.Sprintf("/ns/%s/dc/%s/svc/%s", + id.Namespace, id.Datacenter, id.Service) + return &result +} diff --git a/agent/connect/spiffe_test.go b/agent/connect/spiffe_test.go new file mode 100644 index 0000000000..861a4fa638 --- /dev/null +++ b/agent/connect/spiffe_test.go @@ -0,0 +1,57 @@ +package connect + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +// testSpiffeIDCases contains the test cases for parsing and encoding +// the SPIFFE IDs. This is a global since it is used in multiple test functions. +var testSpiffeIDCases = []struct { + Name string + URI string + Struct interface{} + ParseError string +}{ + { + "invalid scheme", + "http://google.com/", + nil, + "scheme", + }, + + { + "basic service ID", + "spiffe://1234.consul/ns/default/dc/dc01/svc/web", + &SpiffeIDService{ + Host: "1234.consul", + Namespace: "default", + Datacenter: "dc01", + Service: "web", + }, + "", + }, +} + +func TestParseSpiffeID(t *testing.T) { + for _, tc := range testSpiffeIDCases { + t.Run(tc.Name, func(t *testing.T) { + assert := assert.New(t) + + // Parse the URI, should always be valid + uri, err := url.Parse(tc.URI) + assert.Nil(err) + + // Parse the ID and check the error/return value + actual, err := ParseSpiffeID(uri) + assert.Equal(tc.ParseError != "", err != nil, "error value") + if err != nil { + assert.Contains(err.Error(), tc.ParseError) + return + } + assert.Equal(tc.Struct, actual) + }) + } +}