agent/connect: package for agent-related Connect, parse SPIFFE IDs

This commit is contained in:
Mitchell Hashimoto 2018-03-19 13:53:57 -07:00
parent 7349c94c23
commit 548ce190d5
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
3 changed files with 171 additions and 0 deletions

59
agent/connect/ca.go Normal file
View File

@ -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
}

55
agent/connect/spiffe.go Normal file
View File

@ -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
}

View File

@ -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)
})
}
}