Simplify the CA provider interface by moving some logic out

This commit is contained in:
Kyle Havlovitz 2018-04-24 16:16:37 -07:00 committed by Mitchell Hashimoto
parent a325388939
commit c6e1b72ccb
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 169 additions and 170 deletions

View File

@ -1,8 +1,11 @@
package connect
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
@ -25,6 +28,23 @@ func ParseCert(pemValue string) (*x509.Certificate, error) {
return x509.ParseCertificate(block.Bytes)
}
// ParseCertFingerprint parses the x509 certificate from a PEM-encoded value
// and returns the SHA-1 fingerprint.
func ParseCertFingerprint(pemValue string) (string, error) {
// The _ result below is not an error but the remaining PEM bytes.
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return "", fmt.Errorf("no PEM-encoded data found")
}
hash := sha1.Sum(block.Bytes)
hexified := make([][]byte, len(hash))
for i, data := range hash {
hexified[i] = []byte(fmt.Sprintf("%02X", data))
}
return string(bytes.Join(hexified, []byte(":"))), nil
}
// 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) {
@ -38,6 +58,9 @@ func ParseSigner(pemValue string) (crypto.Signer, error) {
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
signer, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
@ -74,15 +97,17 @@ func ParseCSR(pemValue string) (*x509.CertificateRequest, error) {
// KeyId returns a x509 KeyId from the given signing key. The key must be
// an *ecdsa.PublicKey currently, but may support more types in the future.
func KeyId(raw interface{}) ([]byte, error) {
pub, ok := raw.(*ecdsa.PublicKey)
if !ok {
switch raw.(type) {
case *ecdsa.PublicKey:
case *rsa.PublicKey:
default:
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.
bs, err := x509.MarshalPKIXPublicKey(pub)
bs, err := x509.MarshalPKIXPublicKey(raw)
if err != nil {
return nil, err
}

View File

@ -13,21 +13,28 @@ type CAProvider interface {
// Active root returns the currently active root CA for this
// provider. This should be a parent of the certificate returned by
// ActiveIntermediate()
ActiveRoot() (*structs.CARoot, error)
ActiveRoot() (string, error)
// ActiveIntermediate returns the current signing cert used by this
// provider for generating SPIFFE leaf certs.
ActiveIntermediate() (*structs.CARoot, error)
ActiveIntermediate() (string, error)
// GenerateIntermediate returns a new intermediate signing cert, a
// cross-signing CSR for it and sets it to the active intermediate.
GenerateIntermediate() (*structs.CARoot, *x509.CertificateRequest, error)
// GenerateIntermediate returns a new intermediate signing cert and
// sets it to the active intermediate.
GenerateIntermediate() (string, error)
// Sign signs a leaf certificate used by Connect proxies from a CSR.
Sign(*SpiffeIDService, *x509.CertificateRequest) (*structs.IssuedCert, error)
Sign(*x509.CertificateRequest) (*structs.IssuedCert, error)
// SignCA signs a CA CSR and returns the resulting cross-signed cert.
SignCA(*x509.CertificateRequest) (string, error)
// CrossSignCA must accept a CA certificate signed by another CA's key
// and cross sign it exactly as it is such that it forms a chain back the the
// CAProvider's current root. Specifically, the Distinguished Name, Subject
// Alternative Name, SubjectKeyID and other relevant extensions must be kept.
// The resulting certificate must have a distinct Serial Number and the
// AuthorityKeyID set to the CAProvider's current signing key as well as the
// Issuer related fields changed as necessary. The resulting certificate is
// returned as a PEM formatted string.
CrossSignCA(*x509.Certificate) (string, error)
// Cleanup performs any necessary cleanup that should happen when the provider
// is shut down permanently, such as removing a temporary PKI backend in Vault

View File

@ -80,11 +80,22 @@ func (s *ConnectCA) ConfigurationSet(
return fmt.Errorf("could not initialize provider: %v", err)
}
newActiveRoot, err := newProvider.ActiveRoot()
newRootPEM, err := newProvider.ActiveRoot()
if err != nil {
return err
}
id, err := connect.ParseCertFingerprint(newRootPEM)
if err != nil {
return fmt.Errorf("error parsing root fingerprint: %v", err)
}
newActiveRoot := &structs.CARoot{
ID: id,
Name: fmt.Sprintf("%s CA Root Cert", config.Provider),
RootCert: newRootPEM,
Active: true,
}
// Compare the new provider's root CA ID to the current one. If they
// match, just update the existing provider with the new config.
// If they don't match, begin the root rotation process.
@ -121,13 +132,19 @@ func (s *ConnectCA) ConfigurationSet(
// to get the cross-signed intermediate
// 3. Get the active root for the new provider, append the intermediate from step 3
// to its list of intermediates
_, csr, err := newProvider.GenerateIntermediate()
intermediatePEM, err := newProvider.GenerateIntermediate()
if err != nil {
return err
}
intermediateCA, err := connect.ParseCert(intermediatePEM)
if err != nil {
return err
}
// Have the old provider cross-sign the new intermediate
oldProvider := s.srv.getCAProvider()
xcCert, err := oldProvider.SignCA(csr)
xcCert, err := oldProvider.CrossSignCA(intermediateCA)
if err != nil {
return err
}
@ -237,21 +254,11 @@ func (s *ConnectCA) Sign(
return err
}
// Parse the SPIFFE ID
spiffeId, err := connect.ParseCertURI(csr.URIs[0])
if err != nil {
return err
}
serviceId, ok := spiffeId.(*connect.SpiffeIDService)
if !ok {
return fmt.Errorf("SPIFFE ID in CSR must be a service ID")
}
// todo(kyhavlov): more validation on the CSR before signing
provider := s.srv.getCAProvider()
cert, err := provider.Sign(serviceId, csr)
cert, err := provider.Sign(csr)
if err != nil {
return err
}

View File

@ -16,7 +16,6 @@ import (
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
uuid "github.com/hashicorp/go-uuid"
"github.com/mitchellh/mapstructure"
)
@ -96,12 +95,16 @@ func NewConsulCAProvider(rawConfig map[string]interface{}, srv *Server) (*Consul
newState.PrivateKey = conf.PrivateKey
}
// Generate the root CA
ca, err := provider.generateCA(newState.PrivateKey, conf.RootCert, idx+1)
if err != nil {
return nil, fmt.Errorf("error generating CA: %v", err)
// Generate the root CA if necessary
if conf.RootCert == "" {
ca, err := provider.generateCA(newState.PrivateKey, idx+1)
if err != nil {
return nil, fmt.Errorf("error generating CA: %v", err)
}
newState.RootCert = ca
} else {
newState.RootCert = conf.RootCert
}
newState.CARoot = ca
// Write the provider state
args := &structs.CARequest{
@ -133,68 +136,33 @@ func decodeConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) {
}
// Return the active root CA and generate a new one if needed
func (c *ConsulCAProvider) ActiveRoot() (*structs.CARoot, error) {
func (c *ConsulCAProvider) ActiveRoot() (string, error) {
state := c.srv.fsm.State()
_, providerState, err := state.CAProviderState(c.id)
if err != nil {
return nil, err
return "", err
}
return providerState.CARoot, nil
return providerState.RootCert, nil
}
// We aren't maintaining separate root/intermediate CAs for the builtin
// provider, so just return the root.
func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
func (c *ConsulCAProvider) ActiveIntermediate() (string, error) {
return c.ActiveRoot()
}
// We aren't maintaining separate root/intermediate CAs for the builtin
// provider, so just generate a CSR for the active root.
func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, *x509.CertificateRequest, error) {
func (c *ConsulCAProvider) GenerateIntermediate() (string, error) {
ca, err := c.ActiveIntermediate()
if err != nil {
return nil, nil, err
return "", err
}
state := c.srv.fsm.State()
_, providerState, err := state.CAProviderState(c.id)
if err != nil {
return nil, nil, err
}
_, config, err := state.CAConfig()
if err != nil {
return nil, nil, err
}
// todo(kyhavlov): make a new intermediate here
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterID, Domain: "consul"}
template := &x509.CertificateRequest{
URIs: []*url.URL{id.URI()},
}
signer, err := connect.ParseSigner(providerState.PrivateKey)
if err != nil {
return nil, nil, err
}
// Create the CSR itself
var csrBuf bytes.Buffer
bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
if err != nil {
return nil, nil, fmt.Errorf("error creating CSR: %s", err)
}
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
if err != nil {
return nil, nil, fmt.Errorf("error encoding CSR: %s", err)
}
csr, err := connect.ParseCSR(csrBuf.String())
if err != nil {
return nil, nil, err
}
return ca, csr, err
return ca, err
}
// Remove the state store entry for this provider instance.
@ -216,7 +184,7 @@ func (c *ConsulCAProvider) Cleanup() error {
// Sign returns a new certificate valid for the given SpiffeIDService
// using the current CA.
func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.CertificateRequest) (*structs.IssuedCert, error) {
func (c *ConsulCAProvider) Sign(csr *x509.CertificateRequest) (*structs.IssuedCert, error) {
// Lock during the signing so we don't use the same index twice
// for different cert serial numbers.
c.Lock()
@ -242,8 +210,18 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce
return nil, err
}
// Parse the SPIFFE ID
spiffeId, err := connect.ParseCertURI(csr.URIs[0])
if err != nil {
return nil, err
}
serviceId, ok := spiffeId.(*connect.SpiffeIDService)
if !ok {
return nil, fmt.Errorf("SPIFFE ID in CSR must be a service ID")
}
// Parse the CA cert
caCert, err := connect.ParseCert(providerState.CARoot.RootCert)
caCert, err := connect.ParseCert(providerState.RootCert)
if err != nil {
return nil, fmt.Errorf("error parsing CA cert: %s", err)
}
@ -312,8 +290,8 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce
}, nil
}
// SignCA returns an intermediate CA cert signed by the current active root.
func (c *ConsulCAProvider) SignCA(csr *x509.CertificateRequest) (string, error) {
// CrossSignCA returns the given intermediate CA cert signed by the current active root.
func (c *ConsulCAProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
c.Lock()
defer c.Unlock()
@ -329,44 +307,28 @@ func (c *ConsulCAProvider) SignCA(csr *x509.CertificateRequest) (string, error)
return "", fmt.Errorf("error parsing private key %q: %v", providerState.PrivateKey, err)
}
name := fmt.Sprintf("Consul cross-signed CA %d", providerState.LeafIndex+1)
// The URI (SPIFFE compatible) for the cert
_, config, err := state.CAConfig()
rootCA, err := connect.ParseCert(providerState.RootCert)
if err != nil {
return "", err
}
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterID, Domain: "consul"}
keyId, err := connect.KeyId(privKey.Public())
if err != nil {
return "", err
}
// Create the CA cert
// Create the cross-signing template from the existing root CA
serialNum := &big.Int{}
serialNum.SetUint64(providerState.LeafIndex + 1)
template := x509.Certificate{
SerialNumber: serialNum,
Subject: pkix.Name{CommonName: name},
URIs: csr.URIs,
Signature: csr.Signature,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
PublicKey: csr.PublicKey,
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{id.URI().Hostname()},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign |
x509.KeyUsageCRLSign |
x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now(),
AuthorityKeyId: keyId,
SubjectKeyId: keyId,
}
template := *cert
template.SerialNumber = serialNum
template.Subject = rootCA.Subject
template.SignatureAlgorithm = rootCA.SignatureAlgorithm
template.SubjectKeyId = keyId
template.AuthorityKeyId = keyId
bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, privKey.Public(), privKey)
rand.Reader, &template, rootCA, cert.PublicKey, privKey)
if err != nil {
return "", fmt.Errorf("error generating CA certificate: %s", err)
}
@ -405,75 +367,58 @@ func generatePrivateKey() (string, error) {
}
// generateCA makes a new root CA using the current private key
func (c *ConsulCAProvider) generateCA(privateKey, contents string, sn uint64) (*structs.CARoot, error) {
func (c *ConsulCAProvider) generateCA(privateKey string, sn uint64) (string, error) {
state := c.srv.fsm.State()
_, config, err := state.CAConfig()
if err != nil {
return nil, err
return "", err
}
privKey, err := connect.ParseSigner(privateKey)
if err != nil {
return nil, fmt.Errorf("error parsing private key %q: %v", privateKey, err)
return "", fmt.Errorf("error parsing private key %q: %v", privateKey, err)
}
name := fmt.Sprintf("Consul CA %d", sn)
pemContents := contents
if pemContents == "" {
// The URI (SPIFFE compatible) for the cert
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterID, Domain: "consul"}
keyId, err := connect.KeyId(privKey.Public())
if err != nil {
return nil, err
}
// Create the CA cert
serialNum := &big.Int{}
serialNum.SetUint64(sn)
template := x509.Certificate{
SerialNumber: serialNum,
Subject: pkix.Name{CommonName: name},
URIs: []*url.URL{id.URI()},
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{id.URI().Hostname()},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign |
x509.KeyUsageCRLSign |
x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now(),
AuthorityKeyId: keyId,
SubjectKeyId: keyId,
}
bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, privKey.Public(), privKey)
if err != nil {
return nil, fmt.Errorf("error generating CA certificate: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
return nil, fmt.Errorf("error encoding private key: %s", err)
}
pemContents = buf.String()
}
// Generate an ID for the new CA cert
rootId, err := uuid.GenerateUUID()
// The URI (SPIFFE compatible) for the cert
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterID, Domain: "consul"}
keyId, err := connect.KeyId(privKey.Public())
if err != nil {
return nil, err
return "", err
}
return &structs.CARoot{
ID: rootId,
Name: name,
RootCert: pemContents,
Active: true,
}, nil
// Create the CA cert
serialNum := &big.Int{}
serialNum.SetUint64(sn)
template := x509.Certificate{
SerialNumber: serialNum,
Subject: pkix.Name{CommonName: name},
URIs: []*url.URL{id.URI()},
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{id.URI().Hostname()},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign |
x509.KeyUsageCRLSign |
x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now(),
AuthorityKeyId: keyId,
SubjectKeyId: keyId,
}
bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, privKey.Public(), privKey)
if err != nil {
return "", fmt.Errorf("error generating CA certificate: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
return "", fmt.Errorf("error encoding private key: %s", err)
}
return buf.String(), nil
}

View File

@ -214,7 +214,9 @@ func (s *Server) establishLeadership() error {
s.autopilot.Start()
// todo(kyhavlov): start a goroutine here for handling periodic CA rotation
s.initializeCA()
if err := s.initializeCA(); err != nil {
return err
}
s.setConsistentReadReady()
return nil
@ -232,6 +234,8 @@ func (s *Server) revokeLeadership() error {
return err
}
s.setCAProvider(nil)
s.resetConsistentReadReady()
s.autopilot.Stop()
return nil
@ -412,22 +416,33 @@ func (s *Server) initializeCA() error {
s.setCAProvider(provider)
// Get the active root cert from the CA
trustedCA, err := provider.ActiveRoot()
rootPEM, err := provider.ActiveRoot()
if err != nil {
return fmt.Errorf("error getting root cert: %v", err)
}
id, err := connect.ParseCertFingerprint(rootPEM)
if err != nil {
return fmt.Errorf("error parsing root fingerprint: %v", err)
}
rootCA := &structs.CARoot{
ID: id,
Name: fmt.Sprintf("%s CA Root Cert", conf.Provider),
RootCert: rootPEM,
Active: true,
}
// Check if the CA root is already initialized and exit if it is.
// Every change to the CA after this initial bootstrapping should
// be done through the rotation process.
state := s.fsm.State()
_, root, err := state.CARootActive(nil)
_, activeRoot, err := state.CARootActive(nil)
if err != nil {
return err
}
if root != nil {
if root.ID != trustedCA.ID {
s.logger.Printf("[WARN] connect: CA root %q is not the active root (%q)", trustedCA.ID, root.ID)
if activeRoot != nil {
if activeRoot.ID != rootCA.ID {
s.logger.Printf("[WARN] connect: CA root %q is not the active root (%q)", rootCA.ID, activeRoot.ID)
}
return nil
}
@ -442,7 +457,7 @@ func (s *Server) initializeCA() error {
resp, err := s.raftApply(structs.ConnectCARequestType, &structs.CARequest{
Op: structs.CAOpSetRoots,
Index: idx,
Roots: []*structs.CARoot{trustedCA},
Roots: []*structs.CARoot{rootCA},
})
if err != nil {
s.logger.Printf("[ERR] connect: Apply failed %v", err)

View File

@ -62,7 +62,7 @@ func caRootTableSchema() *memdb.TableSchema {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Indexer: &memdb.StringFieldIndex{
Field: "ID",
},
},

View File

@ -164,7 +164,7 @@ type CAConfiguration struct {
type CAConsulProviderState struct {
ID string
PrivateKey string
CARoot *CARoot
RootCert string
LeafIndex uint64
RaftIndex