mirror of https://github.com/status-im/consul.git
agent/connect: package for agent-related Connect, parse SPIFFE IDs
This commit is contained in:
parent
7349c94c23
commit
548ce190d5
|
@ -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