agent/connect: package for agent-related Connect, parse SPIFFE IDs
This commit is contained in:
parent
d4e232f69b
commit
6550ff9492
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue