Actually return Intermediate certificates bundled with a leaf!

This commit is contained in:
Paul Banks 2018-06-20 12:37:36 +01:00 committed by Jack Pearkes
parent 9f8b87cdda
commit e514570dfa
5 changed files with 98 additions and 22 deletions

View File

@ -13,15 +13,23 @@ type Provider interface {
// ActiveIntermediate() // ActiveIntermediate()
ActiveRoot() (string, error) ActiveRoot() (string, error)
// ActiveIntermediate returns the current signing cert used by this // ActiveIntermediate returns the current signing cert used by this provider
// provider for generating SPIFFE leaf certs. // for generating SPIFFE leaf certs. Note that this must not change except
// when Consul requests the change via GenerateIntermediate. Changing the
// signing cert will break Consul's assumptions about which validation paths
// are active.
ActiveIntermediate() (string, error) ActiveIntermediate() (string, error)
// GenerateIntermediate returns a new intermediate signing cert and // GenerateIntermediate returns a new intermediate signing cert and sets it to
// sets it to the active intermediate. // the active intermediate. If multiple intermediates are needed to complete
// the chain from the signing certificate back to the active root, they should
// all by bundled here.
GenerateIntermediate() (string, error) GenerateIntermediate() (string, error)
// Sign signs a leaf certificate used by Connect proxies from a CSR. // Sign signs a leaf certificate used by Connect proxies from a CSR. The PEM
// returned should include only the leaf certificate as all Intermediates
// needed to validate it will be added by Consul based on the active
// intemediate and any cross-signed intermediates managed by Consul.
Sign(*x509.CertificateRequest) (string, error) Sign(*x509.CertificateRequest) (string, error)
// CrossSignCA must accept a CA certificate signed by another CA's key // CrossSignCA must accept a CA certificate signed by another CA's key

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
@ -122,7 +123,7 @@ func (s *ConnectCA) ConfigurationSet(
} }
// If the config has been committed, update the local provider instance // If the config has been committed, update the local provider instance
s.srv.setCAProvider(newProvider) s.srv.setCAProvider(newProvider, newActiveRoot)
s.srv.logger.Printf("[INFO] connect: CA provider config updated") s.srv.logger.Printf("[INFO] connect: CA provider config updated")
@ -150,7 +151,7 @@ func (s *ConnectCA) ConfigurationSet(
} }
// Have the old provider cross-sign the new intermediate // Have the old provider cross-sign the new intermediate
oldProvider := s.srv.getCAProvider() oldProvider, _ := s.srv.getCAProvider()
if oldProvider == nil { if oldProvider == nil {
return fmt.Errorf("internal error: CA provider is nil") return fmt.Errorf("internal error: CA provider is nil")
} }
@ -191,7 +192,7 @@ func (s *ConnectCA) ConfigurationSet(
// If the config has been committed, update the local provider instance // If the config has been committed, update the local provider instance
// and call teardown on the old provider // and call teardown on the old provider
s.srv.setCAProvider(newProvider) s.srv.setCAProvider(newProvider, newActiveRoot)
if err := oldProvider.Cleanup(); err != nil { if err := oldProvider.Cleanup(); err != nil {
s.srv.logger.Printf("[WARN] connect: failed to clean up old provider %q", config.Provider) s.srv.logger.Printf("[WARN] connect: failed to clean up old provider %q", config.Provider)
@ -309,7 +310,7 @@ func (s *ConnectCA) Sign(
return fmt.Errorf("SPIFFE ID in CSR must be a service ID") return fmt.Errorf("SPIFFE ID in CSR must be a service ID")
} }
provider := s.srv.getCAProvider() provider, caRoot := s.srv.getCAProvider()
if provider == nil { if provider == nil {
return fmt.Errorf("internal error: CA provider is nil") return fmt.Errorf("internal error: CA provider is nil")
} }
@ -348,6 +349,11 @@ func (s *ConnectCA) Sign(
return err return err
} }
// Append any intermediates needed by this root.
for _, p := range caRoot.IntermediateCerts {
pem = strings.TrimSpace(pem) + "\n" + p
}
// TODO(banks): when we implement IssuedCerts table we can use the insert to // TODO(banks): when we implement IssuedCerts table we can use the insert to
// that as the raft index to return in response. Right now we can rely on only // that as the raft index to return in response. Right now we can rely on only
// the built-in provider being supported and the implementation detail that we // the built-in provider being supported and the implementation detail that we

View File

@ -2,6 +2,7 @@ package consul
import ( import (
"crypto/x509" "crypto/x509"
"encoding/pem"
"fmt" "fmt"
"os" "os"
"testing" "testing"
@ -138,6 +139,7 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) assert := assert.New(t)
require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
@ -151,7 +153,7 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
} }
var rootList structs.IndexedCARoots var rootList structs.IndexedCARoots
assert.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList)) require.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList))
assert.Len(rootList.Roots, 1) assert.Len(rootList.Roots, 1)
oldRoot := rootList.Roots[0] oldRoot := rootList.Roots[0]
@ -174,17 +176,18 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
} }
var reply interface{} var reply interface{}
assert.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply)) require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
} }
// Make sure the new root has been added along with an intermediate // Make sure the new root has been added along with an intermediate
// cross-signed by the old root. // cross-signed by the old root.
var newRootPEM string
{ {
args := &structs.DCSpecificRequest{ args := &structs.DCSpecificRequest{
Datacenter: "dc1", Datacenter: "dc1",
} }
var reply structs.IndexedCARoots var reply structs.IndexedCARoots
assert.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply))
assert.Len(reply.Roots, 2) assert.Len(reply.Roots, 2)
for _, r := range reply.Roots { for _, r := range reply.Roots {
@ -197,6 +200,7 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
assert.Equal(r.SigningCert, oldRoot.SigningCert) assert.Equal(r.SigningCert, oldRoot.SigningCert)
assert.Equal(r.IntermediateCerts, oldRoot.IntermediateCerts) assert.Equal(r.IntermediateCerts, oldRoot.IntermediateCerts)
} else { } else {
newRootPEM = r.RootCert
// The new root should have a valid cross-signed cert from the old // The new root should have a valid cross-signed cert from the old
// root as an intermediate. // root as an intermediate.
assert.True(r.Active) assert.True(r.Active)
@ -225,15 +229,65 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
} }
var reply structs.CAConfiguration var reply structs.CAConfiguration
assert.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)) require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))
actual, err := ca.ParseConsulCAConfig(reply.Config) actual, err := ca.ParseConsulCAConfig(reply.Config)
assert.NoError(err) require.NoError(err)
expected, err := ca.ParseConsulCAConfig(newConfig.Config) expected, err := ca.ParseConsulCAConfig(newConfig.Config)
assert.NoError(err) require.NoError(err)
assert.Equal(reply.Provider, newConfig.Provider) assert.Equal(reply.Provider, newConfig.Provider)
assert.Equal(actual, expected) assert.Equal(actual, expected)
} }
// Verify that new leaf certs get the cross-signed intermediate bundled
{
// Generate a CSR and request signing
spiffeId := connect.TestSpiffeIDService(t, "web")
csr, _ := connect.TestCSR(t, spiffeId)
args := &structs.CASignRequest{
Datacenter: "dc1",
CSR: csr,
}
var reply structs.IssuedCert
require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply))
// Verify that the cert is signed by the new CA
{
roots := x509.NewCertPool()
require.True(roots.AppendCertsFromPEM([]byte(newRootPEM)))
leaf, err := connect.ParseCert(reply.CertPEM)
require.NoError(err)
_, err = leaf.Verify(x509.VerifyOptions{
Roots: roots,
})
require.NoError(err)
}
// And that it validates via the intermediate
{
roots := x509.NewCertPool()
assert.True(roots.AppendCertsFromPEM([]byte(oldRoot.RootCert)))
leaf, err := connect.ParseCert(reply.CertPEM)
require.NoError(err)
// Make sure the intermediate was returned as well as leaf
_, rest := pem.Decode([]byte(reply.CertPEM))
require.NotEmpty(rest)
intermediates := x509.NewCertPool()
require.True(intermediates.AppendCertsFromPEM(rest))
_, err = leaf.Verify(x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
})
require.NoError(err)
}
// Verify other fields
assert.Equal("web", reply.Service)
assert.Equal(spiffeId.URI().String(), reply.ServiceURI)
}
} }
// Test CA signing // Test CA signing

View File

@ -236,7 +236,7 @@ func (s *Server) revokeLeadership() error {
return err return err
} }
s.setCAProvider(nil) s.setCAProvider(nil, nil)
s.resetConsistentReadReady() s.resetConsistentReadReady()
s.autopilot.Stop() s.autopilot.Stop()
@ -422,8 +422,6 @@ func (s *Server) initializeCA() error {
return err return err
} }
s.setCAProvider(provider)
// Get the active root cert from the CA // Get the active root cert from the CA
rootPEM, err := provider.ActiveRoot() rootPEM, err := provider.ActiveRoot()
if err != nil { if err != nil {
@ -435,6 +433,8 @@ func (s *Server) initializeCA() error {
return err return err
} }
s.setCAProvider(provider, rootCA)
// Check if the CA root is already initialized and exit if it is. // Check if the CA root is already initialized and exit if it is.
// Every change to the CA after this initial bootstrapping should // Every change to the CA after this initial bootstrapping should
// be done through the rotation process. // be done through the rotation process.
@ -507,12 +507,14 @@ func (s *Server) createCAProvider(conf *structs.CAConfiguration) (ca.Provider, e
} }
} }
func (s *Server) getCAProvider() ca.Provider { func (s *Server) getCAProvider() (ca.Provider, *structs.CARoot) {
retries := 0 retries := 0
var result ca.Provider var result ca.Provider
var resultRoot *structs.CARoot
for result == nil { for result == nil {
s.caProviderLock.RLock() s.caProviderLock.RLock()
result = s.caProvider result = s.caProvider
resultRoot = s.caProviderRoot
s.caProviderLock.RUnlock() s.caProviderLock.RUnlock()
// In cases where an agent is started with managed proxies, we may ask // In cases where an agent is started with managed proxies, we may ask
@ -527,13 +529,14 @@ func (s *Server) getCAProvider() ca.Provider {
break break
} }
return result return result, resultRoot
} }
func (s *Server) setCAProvider(newProvider ca.Provider) { func (s *Server) setCAProvider(newProvider ca.Provider, root *structs.CARoot) {
s.caProviderLock.Lock() s.caProviderLock.Lock()
defer s.caProviderLock.Unlock() defer s.caProviderLock.Unlock()
s.caProvider = newProvider s.caProvider = newProvider
s.caProviderRoot = root
} }
// reconcileReaped is used to reconcile nodes that have failed and been reaped // reconcileReaped is used to reconcile nodes that have failed and been reaped

View File

@ -100,6 +100,11 @@ type Server struct {
// caProvider is the current CA provider in use for Connect. This is // caProvider is the current CA provider in use for Connect. This is
// only non-nil when we are the leader. // only non-nil when we are the leader.
caProvider ca.Provider caProvider ca.Provider
// caProviderRoot is the CARoot that was stored along with the ca.Provider
// active. It's only updated in lock-step with the caProvider. This prevents
// races between state updates to active roots and the fetch of the provider
// instance.
caProviderRoot *structs.CARoot
caProviderLock sync.RWMutex caProviderLock sync.RWMutex
// Consul configuration // Consul configuration