connect/ca: update Consul provider to use new cross-sign CSR method

This commit is contained in:
Kyle Havlovitz 2018-06-15 17:59:08 -07:00 committed by Jack Pearkes
parent 675555c4ff
commit cea94d0bcf
10 changed files with 127 additions and 66 deletions

View File

@ -935,6 +935,7 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
// Validate the given Connect CA provider config
validCAProviders := map[string]bool{
"": true,
structs.ConsulCAProvider: true,
structs.VaultCAProvider: true,
}

View File

@ -113,7 +113,6 @@ func DevSource() Source {
connect = {
enabled = true
ca_provider = "consul"
}
performance = {
raft_multiplier = 1

View File

@ -2484,10 +2484,9 @@ func TestFullConfig(t *testing.T) {
"check_update_interval": "16507s",
"client_addr": "93.83.18.19",
"connect": {
"ca_provider": "b8j4ynx9",
"ca_provider": "consul",
"ca_config": {
"g4cvJyys": "IRLXE9Ds",
"hyMy9Oxn": "XeBp4Sis"
"RotationPeriod": "90h"
},
"enabled": true,
"proxy_defaults": {
@ -2946,10 +2945,9 @@ func TestFullConfig(t *testing.T) {
check_update_interval = "16507s"
client_addr = "93.83.18.19"
connect {
ca_provider = "b8j4ynx9"
ca_provider = "consul"
ca_config {
"g4cvJyys" = "IRLXE9Ds"
"hyMy9Oxn" = "XeBp4Sis"
"RotationPeriod" = "90h"
}
enabled = true
proxy_defaults {
@ -3550,10 +3548,9 @@ func TestFullConfig(t *testing.T) {
ConnectEnabled: true,
ConnectProxyBindMinPort: 2000,
ConnectProxyBindMaxPort: 3000,
ConnectCAProvider: "b8j4ynx9",
ConnectCAProvider: "consul",
ConnectCAConfig: map[string]interface{}{
"g4cvJyys": "IRLXE9Ds",
"hyMy9Oxn": "XeBp4Sis",
"RotationPeriod": "90h",
},
ConnectProxyAllowManagedRoot: false,
ConnectProxyAllowManagedAPIRegistration: false,

View File

@ -242,8 +242,53 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) {
return buf.String(), nil
}
// GetCrossSigningCSR creates a CSR from our root CA certificate to be signed
// by another CA provider.
func (c *ConsulProvider) GetCrossSigningCSR() (*x509.CertificateRequest, error) {
c.RLock()
defer c.RUnlock()
// Get the provider state
state := c.delegate.State()
_, providerState, err := state.CAProviderState(c.id)
if err != nil {
return nil, err
}
privKey, err := connect.ParseSigner(providerState.PrivateKey)
if err != nil {
return nil, err
}
rootCA, err := connect.ParseCert(providerState.RootCert)
if err != nil {
return nil, err
}
// Create the CSR
template := &x509.CertificateRequest{
DNSNames: rootCA.DNSNames,
EmailAddresses: rootCA.EmailAddresses,
IPAddresses: rootCA.IPAddresses,
URIs: rootCA.URIs,
SignatureAlgorithm: rootCA.SignatureAlgorithm,
Subject: rootCA.Subject,
Extensions: rootCA.Extensions,
ExtraExtensions: rootCA.ExtraExtensions,
}
bs, err := x509.CreateCertificateRequest(rand.Reader, template, privKey)
if err != nil {
return nil, err
}
csr, err := x509.ParseCertificateRequest(bs)
if err != nil {
return nil, err
}
return csr, nil
}
// CrossSignCA returns the given intermediate CA cert signed by the current active root.
func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
func (c *ConsulProvider) CrossSignCA(csr *x509.CertificateRequest) (string, error) {
c.Lock()
defer c.Unlock()
@ -264,7 +309,12 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
return "", err
}
keyId, err := connect.KeyId(privKey.Public())
authKeyId, err := connect.KeyId(privKey.Public())
if err != nil {
return "", err
}
subjKeyId, err := connect.KeyId(csr.PublicKey)
if err != nil {
return "", err
}
@ -272,11 +322,26 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
// Create the cross-signing template from the existing root CA
serialNum := &big.Int{}
serialNum.SetUint64(idx + 1)
template := *cert
template.SerialNumber = serialNum
template.SignatureAlgorithm = rootCA.SignatureAlgorithm
template.SubjectKeyId = keyId
template.AuthorityKeyId = keyId
template := &x509.Certificate{
SerialNumber: serialNum,
SignatureAlgorithm: rootCA.SignatureAlgorithm,
DNSNames: csr.DNSNames,
EmailAddresses: csr.EmailAddresses,
IPAddresses: csr.IPAddresses,
URIs: csr.URIs,
SubjectKeyId: subjKeyId,
AuthorityKeyId: authKeyId,
Subject: csr.Subject,
Extensions: csr.Extensions,
ExtraExtensions: csr.ExtraExtensions,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign |
x509.KeyUsageCRLSign |
x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().Add(7 * 24 * time.Hour),
NotBefore: time.Now(),
}
// Sign the certificate valid from 1 minute in the past, this helps it be
// accepted right away even when nodes are not in close time sync accross the
@ -290,7 +355,7 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
template.NotAfter = effectiveNow.Add(7 * 24 * time.Hour)
bs, err := x509.CreateCertificate(
rand.Reader, &template, rootCA, cert.PublicKey, privKey)
rand.Reader, template, rootCA, csr.PublicKey, privKey)
if err != nil {
return "", fmt.Errorf("error generating CA certificate: %s", err)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type consulCAMockDelegate struct {
@ -52,7 +53,7 @@ func newMockDelegate(t *testing.T, conf *structs.CAConfiguration) *consulCAMockD
if s == nil {
t.Fatalf("missing state store")
}
if err := s.CASetConfig(0, conf); err != nil {
if err := s.CASetConfig(conf.RaftIndex.CreateIndex, conf); err != nil {
t.Fatalf("err: %s", err)
}
@ -177,37 +178,44 @@ func TestConsulCAProvider_SignLeaf(t *testing.T) {
func TestConsulCAProvider_CrossSignCA(t *testing.T) {
t.Parallel()
assert := assert.New(t)
conf := testConsulCAConfig()
delegate := newMockDelegate(t, conf)
provider, err := NewConsulProvider(conf.Config, delegate)
assert.NoError(err)
require := require.New(t)
// Make a new CA cert to get cross-signed.
rootCA := connect.TestCA(t, nil)
rootPEM, err := provider.ActiveRoot()
assert.NoError(err)
root, err := connect.ParseCert(rootPEM)
assert.NoError(err)
conf1 := testConsulCAConfig()
delegate1 := newMockDelegate(t, conf1)
provider1, err := NewConsulProvider(conf1.Config, delegate1)
conf2 := testConsulCAConfig()
conf2.CreateIndex = 10
delegate2 := newMockDelegate(t, conf2)
provider2, err := NewConsulProvider(conf2.Config, delegate2)
require.NoError(err)
require.NoError(err)
// Have provider2 generate a cross-signing CSR
csr, err := provider2.GetCrossSigningCSR()
require.NoError(err)
oldSubject := csr.Subject.CommonName
// Have the provider cross sign our new CA cert.
cert, err := connect.ParseCert(rootCA.RootCert)
assert.NoError(err)
oldSubject := cert.Subject.CommonName
xcPEM, err := provider.CrossSignCA(cert)
assert.NoError(err)
xcPEM, err := provider1.CrossSignCA(csr)
require.NoError(err)
xc, err := connect.ParseCert(xcPEM)
assert.NoError(err)
require.NoError(err)
// AuthorityKeyID and SubjectKeyID should be the signing root's.
assert.Equal(root.AuthorityKeyId, xc.AuthorityKeyId)
assert.Equal(root.SubjectKeyId, xc.SubjectKeyId)
rootPEM, err := provider1.ActiveRoot()
require.NoError(err)
root, err := connect.ParseCert(rootPEM)
require.NoError(err)
// AuthorityKeyID should be the signing root's, SubjectKeyId should be different.
require.Equal(root.AuthorityKeyId, xc.AuthorityKeyId)
require.NotEqual(root.SubjectKeyId, xc.SubjectKeyId)
// Subject name should not have changed.
assert.NotEqual(root.Subject.CommonName, xc.Subject.CommonName)
assert.Equal(oldSubject, xc.Subject.CommonName)
require.NotEqual(root.Subject.CommonName, xc.Subject.CommonName)
require.Equal(oldSubject, xc.Subject.CommonName)
// Issuer should be the signing root.
assert.Equal(root.Issuer.CommonName, xc.Issuer.CommonName)
require.Equal(root.Issuer.CommonName, xc.Issuer.CommonName)
}

View File

@ -3,7 +3,6 @@ package connect
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
@ -98,7 +97,6 @@ func ParseCSR(pemValue string) (*x509.CertificateRequest, error) {
func KeyId(raw interface{}) ([]byte, error) {
switch raw.(type) {
case *ecdsa.PublicKey:
case *rsa.PublicKey:
default:
return nil, fmt.Errorf("invalid key type: %T", raw)
}

View File

@ -82,15 +82,15 @@ func fixupConfig(conf *structs.CAConfiguration) {
for k, v := range conf.Config {
if raw, ok := v.([]uint8); ok {
conf.Config[k] = ca.Uint8ToString(raw)
}
switch conf.Provider {
case structs.ConsulCAProvider:
if v, ok := conf.Config["PrivateKey"]; ok && v != "" {
conf.Config["PrivateKey"] = "hidden"
}
case structs.VaultCAProvider:
if v, ok := conf.Config["Token"]; ok && v != "" {
conf.Config["Token"] = "hidden"
switch conf.Provider {
case structs.ConsulCAProvider:
if k == "PrivateKey" && ca.Uint8ToString(raw) != "" {
conf.Config["PrivateKey"] = "hidden"
}
case structs.VaultCAProvider:
if k == "Token" && ca.Uint8ToString(raw) != "" {
conf.Config["Token"] = "hidden"
}
}
}
}

View File

@ -140,22 +140,14 @@ 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
intermediatePEM, err := newProvider.GenerateIntermediate()
if err != nil {
return err
}
intermediateCA, err := connect.ParseCert(intermediatePEM)
if err != nil {
return err
}
csr, err := newProvider.GetCrossSigningCSR()
// Have the old provider cross-sign the new intermediate
oldProvider, _ := s.srv.getCAProvider()
if oldProvider == nil {
return fmt.Errorf("internal error: CA provider is nil")
}
xcCert, err := oldProvider.CrossSignCA(intermediateCA)
xcCert, err := oldProvider.CrossSignCA(csr)
if err != nil {
return err
}

View File

@ -210,10 +210,10 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
oldRootCert := testParseCert(t, oldRoot.RootCert)
newRootCert := testParseCert(t, r.RootCert)
// Should have the authority/subject key IDs and signature algo of the
// Should have the authority key ID and signature algo of the
// (old) signing CA.
assert.Equal(xc.AuthorityKeyId, oldRootCert.AuthorityKeyId)
assert.Equal(xc.SubjectKeyId, oldRootCert.SubjectKeyId)
assert.NotEqual(xc.SubjectKeyId, oldRootCert.SubjectKeyId)
assert.Equal(xc.SignatureAlgorithm, oldRootCert.SignatureAlgorithm)
// The common name and SAN should not have changed.

View File

@ -80,6 +80,7 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
verify.Values(r, "", parsed, expected)
// Change a config value and update
conf.Config["PrivateKey"] = ""
conf.Config["RotationPeriod"] = 120 * 24 * time.Hour
_, err = connect.CASetConfig(conf, nil)
r.Check(err)