Introduce a helper package for BundlePublisher plugins

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
This commit is contained in:
Agustín Martínez Fayó 2023-06-22 20:21:39 -03:00 committed by Andrew Harding
parent b364a36a0c
commit 7278d36914
26 changed files with 1512 additions and 35 deletions

7
go.mod
View File

@ -6,6 +6,9 @@ require (
github.com/hashicorp/go-hclog v0.15.0
github.com/hashicorp/go-plugin v1.4.0
github.com/hashicorp/hcl v1.0.0
google.golang.org/grpc v1.48.0
google.golang.org/protobuf v1.28.0
github.com/spiffe/go-spiffe/v2 v2.1.6
github.com/stretchr/testify v1.8.2
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
gopkg.in/square/go-jose.v2 v2.6.0
)

1050
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/service/private/init/v1/init.proto

View File

@ -0,0 +1,307 @@
// Package bundlepublisherutil provides helper functions for plugins
// implementing the BundlePublisher interface.
// BundlePublisher plugins should use this package as a way to have a
// standarized name for bundle formats in their configuration, and avoid the
// re-implementation of bundle parsing logic of formats supported in this
// package.
package bundlepublisherutil
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/types"
"gopkg.in/square/go-jose.v2"
)
const (
BundleFormatUnset BundleFormat = iota
SPIFFE
PEM
JWKS
)
// Bundle represents a bundle that can be formatted in different formats.
type Bundle struct {
bundle *types.Bundle
bytesMtx sync.RWMutex
jwksBytes []byte
pemBytes []byte
spiffeBytes []byte
}
// KeyType represents the types of keys that are supported by the KeyManager.
type BundleFormat int
// BundleFormatFromString returns the BundleFormat corresponding to the provided
// string.
func BundleFormatFromString(s string) (BundleFormat, error) {
switch strings.ToLower(s) {
case "spiffe":
return SPIFFE, nil
case "jwks":
return JWKS, nil
case "pem":
return PEM, nil
default:
return BundleFormatUnset, fmt.Errorf("unknown bundle format: %q", s)
}
}
// NewBundle return a new *Bundle with the *types.Bundle provided.
// Use the Bytes() function to get a slice of bytes with the bundle formatted in
// the format specified.
func NewBundle(pluginBundle *types.Bundle) *Bundle {
return &Bundle{
bundle: pluginBundle,
}
}
// String returns the string name for the bundle format.
func (bundleFormat BundleFormat) String() string {
switch bundleFormat {
case BundleFormatUnset:
return "UNSET"
case SPIFFE:
return "spiffe"
case PEM:
return "pem"
case JWKS:
return "jwks"
default:
return fmt.Sprintf("UNKNOWN(%d)", int(bundleFormat))
}
}
// Bytes returns the bundle in the form of a slice of bytes in
// the chosen format.
func (b *Bundle) Bytes(format BundleFormat) ([]byte, error) {
if b.bundle == nil {
return nil, errors.New("missing bundle")
}
switch format {
case BundleFormatUnset:
return nil, errors.New("no format specified")
case JWKS:
if jwksBytes := b.getJWKSBytes(); jwksBytes != nil {
return jwksBytes, nil
}
jwksBytes, err := b.toJWKS()
if err != nil {
return nil, fmt.Errorf("could not convert bundle to jwks format: %w", err)
}
b.setJWKSBytes(jwksBytes)
return jwksBytes, nil
case PEM:
if pemBytes := b.getPEMBytes(); pemBytes != nil {
return pemBytes, nil
}
pemBytes, err := b.toPEM()
if err != nil {
return nil, fmt.Errorf("could not convert bundle to pem format: %w", err)
}
b.setPEMBytes(pemBytes)
return pemBytes, nil
case SPIFFE:
if spiffeBytes := b.getSPIFFEBytes(); spiffeBytes != nil {
return spiffeBytes, nil
}
spiffeBytes, err := b.toSPIFFEBundle()
if err != nil {
return nil, fmt.Errorf("could not convert bundle to spiffe format: %w", err)
}
b.setSPIFFEBytes(spiffeBytes)
return spiffeBytes, nil
default:
return nil, fmt.Errorf("invalid format: %q", format)
}
}
func (b *Bundle) getJWKSBytes() []byte {
b.bytesMtx.RLock()
defer b.bytesMtx.RUnlock()
return b.jwksBytes
}
func (b *Bundle) getPEMBytes() []byte {
b.bytesMtx.RLock()
defer b.bytesMtx.RUnlock()
return b.pemBytes
}
func (b *Bundle) getSPIFFEBytes() []byte {
b.bytesMtx.RLock()
defer b.bytesMtx.RUnlock()
return b.spiffeBytes
}
func (b *Bundle) setJWKSBytes(jwksBytes []byte) {
b.bytesMtx.Lock()
defer b.bytesMtx.Unlock()
b.jwksBytes = jwksBytes
}
func (b *Bundle) setPEMBytes(pemBytes []byte) {
b.bytesMtx.Lock()
defer b.bytesMtx.Unlock()
b.pemBytes = pemBytes
}
func (b *Bundle) setSPIFFEBytes(spiffeBytes []byte) {
b.bytesMtx.Lock()
defer b.bytesMtx.Unlock()
b.spiffeBytes = spiffeBytes
}
// toJWKS converts to JWKS the current bundle.
func (b *Bundle) toJWKS() ([]byte, error) {
var jwks jose.JSONWebKeySet
x509Authorities, jwtAuthorities, err := getAuthorities(b.bundle)
if err != nil {
return nil, err
}
for _, rootCA := range x509Authorities {
jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
Key: rootCA.PublicKey,
Certificates: []*x509.Certificate{rootCA},
})
}
for keyID, jwtSigningKey := range jwtAuthorities {
jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
Key: jwtSigningKey,
KeyID: keyID,
})
}
var out interface{} = jwks
return json.MarshalIndent(out, "", " ")
}
// toPEM converts to PEM the current bundle.
func (b *Bundle) toPEM() ([]byte, error) {
bundleData := new(bytes.Buffer)
for _, x509Authority := range b.bundle.X509Authorities {
if err := pem.Encode(bundleData, &pem.Block{
Type: "CERTIFICATE",
Bytes: x509Authority.Asn1,
}); err != nil {
return nil, fmt.Errorf("could not perform PEM encoding: %w", err)
}
}
return bundleData.Bytes(), nil
}
// toSPIFFEBundle converts to a SPIFFE bundle the current bundle.
func (b *Bundle) toSPIFFEBundle() ([]byte, error) {
sb, err := spiffeBundleFromPluginProto(b.bundle)
if err != nil {
return nil, fmt.Errorf("failed to convert bundle: %w", err)
}
docBytes, err := sb.Marshal()
if err != nil {
return nil, fmt.Errorf("failed to marshal bundle: %w", err)
}
var o bytes.Buffer
if err := json.Indent(&o, docBytes, "", " "); err != nil {
return nil, err
}
return o.Bytes(), nil
}
// getAuthorities gets the X.509 authorities and JWT authorities from the
// provided *types.Bundle.
func getAuthorities(bundleProto *types.Bundle) ([]*x509.Certificate, map[string]crypto.PublicKey, error) {
x509Authorities, err := x509CertificatesFromProto(bundleProto.X509Authorities)
if err != nil {
return nil, nil, err
}
jwtAuthorities, err := jwtKeysFromProto(bundleProto.JwtAuthorities)
if err != nil {
return nil, nil, err
}
return x509Authorities, jwtAuthorities, nil
}
// jwtKeysFromProto converts JWT keys from the given []*types.JWTKey to
// map[string]crypto.PublicKey.
// The key ID of the public key is used as the key in the returned map.
func jwtKeysFromProto(proto []*types.JWTKey) (map[string]crypto.PublicKey, error) {
keys := make(map[string]crypto.PublicKey)
for i, publicKey := range proto {
jwtSigningKey, err := x509.ParsePKIXPublicKey(publicKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("unable to parse JWT signing key %d: %w", i, err)
}
keys[publicKey.KeyId] = jwtSigningKey
}
return keys, nil
}
// spiffeBundleFromPluginProto converts a bundle from the given *types.Bundle to
// *spiffebundle.Bundle.
func spiffeBundleFromPluginProto(bundleProto *types.Bundle) (*spiffebundle.Bundle, error) {
td, err := spiffeid.TrustDomainFromString(bundleProto.TrustDomain)
if err != nil {
return nil, err
}
x509Authorities, jwtAuthorities, err := getAuthorities(bundleProto)
if err != nil {
return nil, err
}
bundle := spiffebundle.New(td)
bundle.SetX509Authorities(x509Authorities)
bundle.SetJWTAuthorities(jwtAuthorities)
if bundleProto.RefreshHint > 0 {
bundle.SetRefreshHint(time.Duration(bundleProto.RefreshHint) * time.Second)
}
if bundleProto.SequenceNumber > 0 {
bundle.SetSequenceNumber(bundleProto.SequenceNumber)
}
return bundle, nil
}
// x509CertificatesFromProto converts X.509 certificates from the given
// []*types.X509Certificate to []*x509.Certificate.
func x509CertificatesFromProto(proto []*types.X509Certificate) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for i, auth := range proto {
cert, err := x509.ParseCertificate(auth.Asn1)
if err != nil {
return nil, fmt.Errorf("unable to parse root CA %d: %w", i, err)
}
certs = append(certs, cert)
}
return certs, nil
}

View File

@ -0,0 +1,139 @@
package bundlepublisherutil
import (
"crypto/x509"
"encoding/pem"
"fmt"
"math"
"testing"
"github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/types"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func TestBytes(t *testing.T) {
const (
certPEM = `-----BEGIN CERTIFICATE-----
MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa
GA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyv
sCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXs
RxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkw
F4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09X
makw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylA
dZglS5kKnYigmwDh+/U=
-----END CERTIFICATE-----
`
)
block, _ := pem.Decode([]byte(certPEM))
require.NotNil(t, block, "unable to unmarshal certificate response: malformed PEM block")
cert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
keyPkix, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
require.NoError(t, err)
testBundle := &types.Bundle{
TrustDomain: "example.org",
X509Authorities: []*types.X509Certificate{{Asn1: cert.Raw}},
JwtAuthorities: []*types.JWTKey{
{
KeyId: "KID",
PublicKey: keyPkix,
},
},
RefreshHint: 1440,
SequenceNumber: 100,
}
standardJWKS := `{
"keys": [
{
%s"kty": "EC",
"crv": "P-256",
"x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4",
"y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI",
"x5c": [
"MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U="
]
},
{
%s"kty": "EC",
"kid": "KID",
"crv": "P-256",
"x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4",
"y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI"
}
]%s
}`
expectedJWKS := fmt.Sprintf(standardJWKS, "", "", "")
expectedSPIFFEBundle := fmt.Sprintf(standardJWKS,
`"use": "x509-svid",
`,
`"use": "jwt-svid",
`,
`,
"spiffe_sequence": 100,
"spiffe_refresh_hint": 1440`,
)
for _, tt := range []struct {
name string
format BundleFormat
bundle *types.Bundle
expectBytes []byte
expectError string
}{
{
name: "format not set",
bundle: testBundle,
expectError: "no format specified",
},
{
name: "invalid format",
format: math.MaxInt,
bundle: testBundle,
expectError: fmt.Sprintf("invalid format: \"UNKNOWN(%d)\"", math.MaxInt),
},
{
name: "no bundle",
format: SPIFFE,
expectError: "missing bundle",
},
{
name: "jwks format",
format: JWKS,
bundle: testBundle,
expectBytes: []byte(expectedJWKS),
},
{
name: "pem format",
format: PEM,
bundle: testBundle,
expectBytes: []byte(certPEM),
},
{
name: "spiffe format",
format: SPIFFE,
bundle: testBundle,
expectBytes: []byte(expectedSPIFFEBundle),
},
} {
t.Run(tt.name, func(t *testing.T) {
b := NewBundle(tt.bundle)
if !proto.Equal(tt.bundle, b.bundle) {
require.Equal(t, tt.bundle, b.bundle)
}
bytes, err := b.Bytes(tt.format)
if tt.expectError != "" {
require.EqualError(t, err, tt.expectError)
require.Nil(t, bytes)
return
}
require.NoError(t, err)
require.Equal(t, string(tt.expectBytes), string(bytes))
})
}
}

View File

@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: test/echo.proto

View File

@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: test/somehostservice.proto

View File

@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: test/someplugin.proto

View File

@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: test/someservice.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/hostservice/common/metrics/v1/metrics.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/hostservice/server/agentstore/v1/agentstore.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/hostservice/server/identityprovider/v1/identityprovider.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/agent/keymanager/v1/keymanager.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/agent/nodeattestor/v1/nodeattestor.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/agent/svidstore/v1/svidstore.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/agent/workloadattestor/v1/workloadattestor.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/bundlepublisher/v1/bundlepublisher.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/credentialcomposer/v1/credentialcomposer.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/keymanager/v1/keymanager.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/nodeattestor/v1/nodeattestor.proto

View File

@ -2,7 +2,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/notifier/v1/notifier.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/server/upstreamauthority/v1/upstreamauthority.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/types/bundle.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/types/jwtkey.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/plugin/types/x509certificate.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: spire/service/common/config/v1/config.proto