mirror of https://github.com/hashicorp/consul
Fix support for RSA CA keys in Connect. (#6638)
* Allow RSA CA certs for consul and vault providers to correctly sign EC leaf certs. * Ensure key type ad bits are populated from CA cert and clean up tests * Add integration test and fix error when initializing secondary CA with RSA key. * Add more tests, fix review feedback * Update docs with key type config and output * Apply suggestions from code review Co-Authored-By: R.B. Boyer <rb@hashicorp.com>pull/6423/head
parent
5ff8fa9918
commit
87699eca2f
|
@ -525,6 +525,17 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new private key
|
// Create a new private key
|
||||||
|
|
||||||
|
// TODO: for now we always generate EC keys on clients regardless of the key
|
||||||
|
// type being used by the active CA. This is fine and allowed in TLS1.2 and
|
||||||
|
// signing EC CSRs with an RSA key is supported by all current CA providers so
|
||||||
|
// it's OK. IFF we ever need to support a CA provider that refuses to sign a
|
||||||
|
// CSR with a different signature algorithm, or if we have compatibility
|
||||||
|
// issues with external PKI systems that require EC certs be signed with ECDSA
|
||||||
|
// from the CA (this was required in TLS1.1 but not in 1.2) then we can
|
||||||
|
// instead intelligently pick the key type we generate here based on the key
|
||||||
|
// type of the active signing CA. We already have that loaded since we need
|
||||||
|
// the trust domain.
|
||||||
pk, pkPEM, err := connect.GeneratePrivateKey()
|
pk, pkPEM, err := connect.GeneratePrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
|
|
|
@ -341,11 +341,14 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) {
|
||||||
// cluster. A minute is more than enough for typical DC clock drift.
|
// cluster. A minute is more than enough for typical DC clock drift.
|
||||||
effectiveNow := time.Now().Add(-1 * time.Minute)
|
effectiveNow := time.Now().Add(-1 * time.Minute)
|
||||||
template := x509.Certificate{
|
template := x509.Certificate{
|
||||||
SerialNumber: sn,
|
SerialNumber: sn,
|
||||||
Subject: pkix.Name{CommonName: subject},
|
Subject: pkix.Name{CommonName: subject},
|
||||||
URIs: csr.URIs,
|
URIs: csr.URIs,
|
||||||
Signature: csr.Signature,
|
Signature: csr.Signature,
|
||||||
SignatureAlgorithm: csr.SignatureAlgorithm,
|
// We use the correct signature algorithm for the CA key we are signing with
|
||||||
|
// regardless of the algorithm used to sign the CSR signature above since
|
||||||
|
// the leaf might use a different key type.
|
||||||
|
SignatureAlgorithm: connect.SigAlgoForKey(signer),
|
||||||
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
||||||
PublicKey: csr.PublicKey,
|
PublicKey: csr.PublicKey,
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
|
@ -413,7 +416,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
subjectKeyId, err := connect.KeyId(csr.PublicKey)
|
subjectKeyID, err := connect.KeyId(csr.PublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -436,7 +439,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string,
|
||||||
Subject: csr.Subject,
|
Subject: csr.Subject,
|
||||||
URIs: csr.URIs,
|
URIs: csr.URIs,
|
||||||
Signature: csr.Signature,
|
Signature: csr.Signature,
|
||||||
SignatureAlgorithm: csr.SignatureAlgorithm,
|
SignatureAlgorithm: connect.SigAlgoForKey(signer),
|
||||||
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
||||||
PublicKey: csr.PublicKey,
|
PublicKey: csr.PublicKey,
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
|
@ -447,7 +450,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string,
|
||||||
MaxPathLenZero: true,
|
MaxPathLenZero: true,
|
||||||
NotAfter: effectiveNow.AddDate(1, 0, 0),
|
NotAfter: effectiveNow.AddDate(1, 0, 0),
|
||||||
NotBefore: effectiveNow,
|
NotBefore: effectiveNow,
|
||||||
SubjectKeyId: subjectKeyId,
|
SubjectKeyId: subjectKeyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the certificate, PEM encode it and return that value.
|
// Create the certificate, PEM encode it and return that value.
|
||||||
|
|
|
@ -62,7 +62,7 @@ func newMockDelegate(t *testing.T, conf *structs.CAConfiguration) *consulCAMockD
|
||||||
|
|
||||||
func testConsulCAConfig() *structs.CAConfiguration {
|
func testConsulCAConfig() *structs.CAConfiguration {
|
||||||
return &structs.CAConfiguration{
|
return &structs.CAConfiguration{
|
||||||
ClusterID: "asdf",
|
ClusterID: connect.TestClusterID,
|
||||||
Provider: "consul",
|
Provider: "consul",
|
||||||
Config: map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
// Tests duration parsing after msgpack type mangling during raft apply.
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
||||||
|
@ -128,120 +128,138 @@ func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) {
|
||||||
func TestConsulCAProvider_SignLeaf(t *testing.T) {
|
func TestConsulCAProvider_SignLeaf(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
require := require.New(t)
|
for _, tc := range KeyTestCases {
|
||||||
conf := testConsulCAConfig()
|
tc := tc
|
||||||
conf.Config["LeafCertTTL"] = "1h"
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
delegate := newMockDelegate(t, conf)
|
require := require.New(t)
|
||||||
|
conf := testConsulCAConfig()
|
||||||
|
conf.Config["LeafCertTTL"] = "1h"
|
||||||
|
conf.Config["PrivateKeyType"] = tc.KeyType
|
||||||
|
conf.Config["PrivateKeyBits"] = tc.KeyBits
|
||||||
|
delegate := newMockDelegate(t, conf)
|
||||||
|
|
||||||
provider := &ConsulProvider{Delegate: delegate}
|
provider := &ConsulProvider{Delegate: delegate}
|
||||||
require.NoError(provider.Configure(conf.ClusterID, true, conf.Config))
|
require.NoError(provider.Configure(conf.ClusterID, true, conf.Config))
|
||||||
require.NoError(provider.GenerateRoot())
|
require.NoError(provider.GenerateRoot())
|
||||||
|
|
||||||
spiffeService := &connect.SpiffeIDService{
|
spiffeService := &connect.SpiffeIDService{
|
||||||
Host: "node1",
|
Host: "node1",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Service: "foo",
|
Service: "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a leaf cert for the service.
|
||||||
|
{
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeService)
|
||||||
|
|
||||||
|
csr, err := connect.ParseCSR(raw)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
cert, err := provider.Sign(csr)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
parsed, err := connect.ParseCert(cert)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
||||||
|
require.Equal(parsed.Subject.CommonName, "foo")
|
||||||
|
require.Equal(uint64(2), parsed.SerialNumber.Uint64())
|
||||||
|
requireNotEncoded(t, parsed.SubjectKeyId)
|
||||||
|
requireNotEncoded(t, parsed.AuthorityKeyId)
|
||||||
|
|
||||||
|
// Ensure the cert is valid now and expires within the correct limit.
|
||||||
|
now := time.Now()
|
||||||
|
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
||||||
|
require.True(parsed.NotBefore.Before(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new cert for another service and make sure
|
||||||
|
// the serial number is incremented.
|
||||||
|
spiffeService.Service = "bar"
|
||||||
|
{
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeService)
|
||||||
|
|
||||||
|
csr, err := connect.ParseCSR(raw)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
cert, err := provider.Sign(csr)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
parsed, err := connect.ParseCert(cert)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
||||||
|
require.Equal(parsed.Subject.CommonName, "bar")
|
||||||
|
require.Equal(parsed.SerialNumber.Uint64(), uint64(2))
|
||||||
|
requireNotEncoded(t, parsed.SubjectKeyId)
|
||||||
|
requireNotEncoded(t, parsed.AuthorityKeyId)
|
||||||
|
|
||||||
|
// Ensure the cert is valid now and expires within the correct limit.
|
||||||
|
require.True(time.Until(parsed.NotAfter) < 3*24*time.Hour)
|
||||||
|
require.True(parsed.NotBefore.Before(time.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
spiffeAgent := &connect.SpiffeIDAgent{
|
||||||
|
Host: "node1",
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Agent: "uuid",
|
||||||
|
}
|
||||||
|
// Generate a leaf cert for an agent.
|
||||||
|
{
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeAgent)
|
||||||
|
|
||||||
|
csr, err := connect.ParseCSR(raw)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
cert, err := provider.Sign(csr)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
parsed, err := connect.ParseCert(cert)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(spiffeAgent.URI(), parsed.URIs[0])
|
||||||
|
require.Equal("uuid", parsed.Subject.CommonName)
|
||||||
|
require.Equal(uint64(2), parsed.SerialNumber.Uint64())
|
||||||
|
requireNotEncoded(t, parsed.SubjectKeyId)
|
||||||
|
requireNotEncoded(t, parsed.AuthorityKeyId)
|
||||||
|
|
||||||
|
// Ensure the cert is valid now and expires within the correct limit.
|
||||||
|
now := time.Now()
|
||||||
|
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
||||||
|
require.True(parsed.NotBefore.Before(now))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a leaf cert for the service.
|
|
||||||
{
|
|
||||||
raw, _ := connect.TestCSR(t, spiffeService)
|
|
||||||
|
|
||||||
csr, err := connect.ParseCSR(raw)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
cert, err := provider.Sign(csr)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
parsed, err := connect.ParseCert(cert)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(parsed.URIs[0], spiffeService.URI())
|
|
||||||
require.Equal(parsed.Subject.CommonName, "foo")
|
|
||||||
require.Equal(uint64(2), parsed.SerialNumber.Uint64())
|
|
||||||
requireNotEncoded(t, parsed.SubjectKeyId)
|
|
||||||
requireNotEncoded(t, parsed.AuthorityKeyId)
|
|
||||||
|
|
||||||
// Ensure the cert is valid now and expires within the correct limit.
|
|
||||||
now := time.Now()
|
|
||||||
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
|
||||||
require.True(parsed.NotBefore.Before(now))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new cert for another service and make sure
|
|
||||||
// the serial number is incremented.
|
|
||||||
spiffeService.Service = "bar"
|
|
||||||
{
|
|
||||||
raw, _ := connect.TestCSR(t, spiffeService)
|
|
||||||
|
|
||||||
csr, err := connect.ParseCSR(raw)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
cert, err := provider.Sign(csr)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
parsed, err := connect.ParseCert(cert)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(parsed.URIs[0], spiffeService.URI())
|
|
||||||
require.Equal(parsed.Subject.CommonName, "bar")
|
|
||||||
require.Equal(parsed.SerialNumber.Uint64(), uint64(2))
|
|
||||||
requireNotEncoded(t, parsed.SubjectKeyId)
|
|
||||||
requireNotEncoded(t, parsed.AuthorityKeyId)
|
|
||||||
|
|
||||||
// Ensure the cert is valid now and expires within the correct limit.
|
|
||||||
require.True(time.Until(parsed.NotAfter) < 3*24*time.Hour)
|
|
||||||
require.True(parsed.NotBefore.Before(time.Now()))
|
|
||||||
}
|
|
||||||
|
|
||||||
spiffeAgent := &connect.SpiffeIDAgent{
|
|
||||||
Host: "node1",
|
|
||||||
Datacenter: "dc1",
|
|
||||||
Agent: "uuid",
|
|
||||||
}
|
|
||||||
// Generate a leaf cert for an agent.
|
|
||||||
{
|
|
||||||
raw, _ := connect.TestCSR(t, spiffeAgent)
|
|
||||||
|
|
||||||
csr, err := connect.ParseCSR(raw)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
cert, err := provider.Sign(csr)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
parsed, err := connect.ParseCert(cert)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(spiffeAgent.URI(), parsed.URIs[0])
|
|
||||||
require.Equal("uuid", parsed.Subject.CommonName)
|
|
||||||
require.Equal(uint64(2), parsed.SerialNumber.Uint64())
|
|
||||||
requireNotEncoded(t, parsed.SubjectKeyId)
|
|
||||||
requireNotEncoded(t, parsed.AuthorityKeyId)
|
|
||||||
|
|
||||||
// Ensure the cert is valid now and expires within the correct limit.
|
|
||||||
now := time.Now()
|
|
||||||
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
|
||||||
require.True(parsed.NotBefore.Before(now))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConsulCAProvider_CrossSignCA(t *testing.T) {
|
func TestConsulCAProvider_CrossSignCA(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
conf1 := testConsulCAConfig()
|
tests := CASigningKeyTypeCases()
|
||||||
delegate1 := newMockDelegate(t, conf1)
|
|
||||||
provider1 := &ConsulProvider{Delegate: delegate1}
|
|
||||||
require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config))
|
|
||||||
require.NoError(provider1.GenerateRoot())
|
|
||||||
|
|
||||||
conf2 := testConsulCAConfig()
|
for _, tc := range tests {
|
||||||
conf2.CreateIndex = 10
|
tc := tc
|
||||||
delegate2 := newMockDelegate(t, conf2)
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
provider2 := &ConsulProvider{Delegate: delegate2}
|
require := require.New(t)
|
||||||
require.NoError(provider2.Configure(conf2.ClusterID, true, conf2.Config))
|
|
||||||
require.NoError(provider2.GenerateRoot())
|
|
||||||
|
|
||||||
testCrossSignProviders(t, provider1, provider2)
|
conf1 := testConsulCAConfig()
|
||||||
|
delegate1 := newMockDelegate(t, conf1)
|
||||||
|
provider1 := &ConsulProvider{Delegate: delegate1}
|
||||||
|
conf1.Config["PrivateKeyType"] = tc.SigningKeyType
|
||||||
|
conf1.Config["PrivateKeyBits"] = tc.SigningKeyBits
|
||||||
|
require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config))
|
||||||
|
require.NoError(provider1.GenerateRoot())
|
||||||
|
|
||||||
|
conf2 := testConsulCAConfig()
|
||||||
|
conf2.CreateIndex = 10
|
||||||
|
delegate2 := newMockDelegate(t, conf2)
|
||||||
|
provider2 := &ConsulProvider{Delegate: delegate2}
|
||||||
|
conf2.Config["PrivateKeyType"] = tc.CSRKeyType
|
||||||
|
conf2.Config["PrivateKeyBits"] = tc.CSRKeyBits
|
||||||
|
require.NoError(provider2.Configure(conf2.ClusterID, true, conf2.Config))
|
||||||
|
require.NoError(provider2.GenerateRoot())
|
||||||
|
|
||||||
|
testCrossSignProviders(t, provider1, provider2)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) {
|
func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) {
|
||||||
|
@ -328,21 +346,34 @@ func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) {
|
||||||
|
|
||||||
func TestConsulProvider_SignIntermediate(t *testing.T) {
|
func TestConsulProvider_SignIntermediate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
conf1 := testConsulCAConfig()
|
tests := CASigningKeyTypeCases()
|
||||||
delegate1 := newMockDelegate(t, conf1)
|
|
||||||
provider1 := &ConsulProvider{Delegate: delegate1}
|
|
||||||
require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config))
|
|
||||||
require.NoError(provider1.GenerateRoot())
|
|
||||||
|
|
||||||
conf2 := testConsulCAConfig()
|
for _, tc := range tests {
|
||||||
conf2.CreateIndex = 10
|
tc := tc
|
||||||
delegate2 := newMockDelegate(t, conf2)
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
provider2 := &ConsulProvider{Delegate: delegate2}
|
require := require.New(t)
|
||||||
require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config))
|
|
||||||
|
conf1 := testConsulCAConfig()
|
||||||
|
delegate1 := newMockDelegate(t, conf1)
|
||||||
|
provider1 := &ConsulProvider{Delegate: delegate1}
|
||||||
|
conf1.Config["PrivateKeyType"] = tc.SigningKeyType
|
||||||
|
conf1.Config["PrivateKeyBits"] = tc.SigningKeyBits
|
||||||
|
require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config))
|
||||||
|
require.NoError(provider1.GenerateRoot())
|
||||||
|
|
||||||
|
conf2 := testConsulCAConfig()
|
||||||
|
conf2.CreateIndex = 10
|
||||||
|
delegate2 := newMockDelegate(t, conf2)
|
||||||
|
provider2 := &ConsulProvider{Delegate: delegate2}
|
||||||
|
conf2.Config["PrivateKeyType"] = tc.CSRKeyType
|
||||||
|
conf2.Config["PrivateKeyBits"] = tc.CSRKeyBits
|
||||||
|
require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config))
|
||||||
|
|
||||||
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSignIntermediateCrossDC(t *testing.T, provider1, provider2 Provider) {
|
func testSignIntermediateCrossDC(t *testing.T, provider1, provider2 Provider) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ca
|
package ca
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
@ -83,6 +84,22 @@ func TestVaultCAProvider_Bootstrap(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertCorrectKeyType(t *testing.T, want, certPEM string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cert, err := connect.ParseCert(certPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
switch want {
|
||||||
|
case "ec":
|
||||||
|
require.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
||||||
|
case "rsa":
|
||||||
|
require.Equal(t, x509.RSA, cert.PublicKeyAlgorithm)
|
||||||
|
default:
|
||||||
|
t.Fatal("test doesn't support key type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVaultCAProvider_SignLeaf(t *testing.T) {
|
func TestVaultCAProvider_SignLeaf(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -90,61 +107,82 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
require := require.New(t)
|
for _, tc := range KeyTestCases {
|
||||||
provider, testVault := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
tc := tc
|
||||||
"LeafCertTTL": "1h",
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
})
|
require := require.New(t)
|
||||||
defer testVault.Stop()
|
provider, testVault := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
||||||
|
"LeafCertTTL": "1h",
|
||||||
|
"PrivateKeyType": tc.KeyType,
|
||||||
|
"PrivateKeyBits": tc.KeyBits,
|
||||||
|
})
|
||||||
|
defer testVault.Stop()
|
||||||
|
|
||||||
spiffeService := &connect.SpiffeIDService{
|
spiffeService := &connect.SpiffeIDService{
|
||||||
Host: "node1",
|
Host: "node1",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Service: "foo",
|
Service: "foo",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a leaf cert for the service.
|
rootPEM, err := provider.ActiveRoot()
|
||||||
var firstSerial uint64
|
require.NoError(err)
|
||||||
{
|
assertCorrectKeyType(t, tc.KeyType, rootPEM)
|
||||||
raw, _ := connect.TestCSR(t, spiffeService)
|
|
||||||
|
|
||||||
csr, err := connect.ParseCSR(raw)
|
intPEM, err := provider.ActiveIntermediate()
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
assertCorrectKeyType(t, tc.KeyType, intPEM)
|
||||||
|
|
||||||
cert, err := provider.Sign(csr)
|
// Generate a leaf cert for the service.
|
||||||
require.NoError(err)
|
var firstSerial uint64
|
||||||
|
{
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeService)
|
||||||
|
|
||||||
parsed, err := connect.ParseCert(cert)
|
csr, err := connect.ParseCSR(raw)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(parsed.URIs[0], spiffeService.URI())
|
|
||||||
firstSerial = parsed.SerialNumber.Uint64()
|
|
||||||
|
|
||||||
// Ensure the cert is valid now and expires within the correct limit.
|
cert, err := provider.Sign(csr)
|
||||||
now := time.Now()
|
require.NoError(err)
|
||||||
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
|
||||||
require.True(parsed.NotBefore.Before(now))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new cert for another service and make sure
|
parsed, err := connect.ParseCert(cert)
|
||||||
// the serial number is unique.
|
require.NoError(err)
|
||||||
spiffeService.Service = "bar"
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
||||||
{
|
firstSerial = parsed.SerialNumber.Uint64()
|
||||||
raw, _ := connect.TestCSR(t, spiffeService)
|
|
||||||
|
|
||||||
csr, err := connect.ParseCSR(raw)
|
// Ensure the cert is valid now and expires within the correct limit.
|
||||||
require.NoError(err)
|
now := time.Now()
|
||||||
|
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
||||||
|
require.True(parsed.NotBefore.Before(now))
|
||||||
|
|
||||||
cert, err := provider.Sign(csr)
|
// Make sure we can validate the cert as expected.
|
||||||
require.NoError(err)
|
require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
||||||
|
}
|
||||||
|
|
||||||
parsed, err := connect.ParseCert(cert)
|
// Generate a new cert for another service and make sure
|
||||||
require.NoError(err)
|
// the serial number is unique.
|
||||||
require.Equal(parsed.URIs[0], spiffeService.URI())
|
spiffeService.Service = "bar"
|
||||||
require.NotEqual(firstSerial, parsed.SerialNumber.Uint64())
|
{
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeService)
|
||||||
|
|
||||||
// Ensure the cert is valid now and expires within the correct limit.
|
csr, err := connect.ParseCSR(raw)
|
||||||
require.True(time.Until(parsed.NotAfter) < time.Hour)
|
require.NoError(err)
|
||||||
require.True(parsed.NotBefore.Before(time.Now()))
|
|
||||||
|
cert, err := provider.Sign(csr)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
parsed, err := connect.ParseCert(cert)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
||||||
|
require.NotEqual(firstSerial, parsed.SerialNumber.Uint64())
|
||||||
|
|
||||||
|
// Ensure the cert is valid now and expires within the correct limit.
|
||||||
|
require.True(time.Until(parsed.NotAfter) < time.Hour)
|
||||||
|
require.True(parsed.NotBefore.Before(time.Now()))
|
||||||
|
|
||||||
|
// Make sure we can validate the cert as expected.
|
||||||
|
require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,13 +193,54 @@ func TestVaultCAProvider_CrossSignCA(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
provider1, testVault1 := testVaultProvider(t)
|
tests := CASigningKeyTypeCases()
|
||||||
defer testVault1.Stop()
|
|
||||||
|
|
||||||
provider2, testVault2 := testVaultProvider(t)
|
for _, tc := range tests {
|
||||||
defer testVault2.Stop()
|
tc := tc
|
||||||
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
testCrossSignProviders(t, provider1, provider2)
|
if tc.SigningKeyType != tc.CSRKeyType {
|
||||||
|
// See https://github.com/hashicorp/vault/issues/7709
|
||||||
|
t.Skip("Vault doesn't support cross-signing different key types yet.")
|
||||||
|
}
|
||||||
|
provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
||||||
|
"LeafCertTTL": "1h",
|
||||||
|
"PrivateKeyType": tc.SigningKeyType,
|
||||||
|
"PrivateKeyBits": tc.SigningKeyBits,
|
||||||
|
})
|
||||||
|
defer testVault1.Stop()
|
||||||
|
|
||||||
|
{
|
||||||
|
rootPEM, err := provider1.ActiveRoot()
|
||||||
|
require.NoError(err)
|
||||||
|
assertCorrectKeyType(t, tc.SigningKeyType, rootPEM)
|
||||||
|
|
||||||
|
intPEM, err := provider1.ActiveIntermediate()
|
||||||
|
require.NoError(err)
|
||||||
|
assertCorrectKeyType(t, tc.SigningKeyType, intPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider2, testVault2 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
||||||
|
"LeafCertTTL": "1h",
|
||||||
|
"PrivateKeyType": tc.CSRKeyType,
|
||||||
|
"PrivateKeyBits": tc.CSRKeyBits,
|
||||||
|
})
|
||||||
|
defer testVault2.Stop()
|
||||||
|
|
||||||
|
{
|
||||||
|
rootPEM, err := provider2.ActiveRoot()
|
||||||
|
require.NoError(err)
|
||||||
|
assertCorrectKeyType(t, tc.CSRKeyType, rootPEM)
|
||||||
|
|
||||||
|
intPEM, err := provider2.ActiveIntermediate()
|
||||||
|
require.NoError(err)
|
||||||
|
assertCorrectKeyType(t, tc.CSRKeyType, intPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCrossSignProviders(t, provider1, provider2)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVaultProvider_SignIntermediate(t *testing.T) {
|
func TestVaultProvider_SignIntermediate(t *testing.T) {
|
||||||
|
@ -171,13 +250,28 @@ func TestVaultProvider_SignIntermediate(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
provider1, testVault1 := testVaultProvider(t)
|
tests := CASigningKeyTypeCases()
|
||||||
defer testVault1.Stop()
|
|
||||||
|
|
||||||
provider2, testVault2 := testVaultProviderWithConfig(t, false, nil)
|
for _, tc := range tests {
|
||||||
defer testVault2.Stop()
|
tc := tc
|
||||||
|
t.Run(tc.Desc, func(t *testing.T) {
|
||||||
|
provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
||||||
|
"LeafCertTTL": "1h",
|
||||||
|
"PrivateKeyType": tc.SigningKeyType,
|
||||||
|
"PrivateKeyBits": tc.SigningKeyBits,
|
||||||
|
})
|
||||||
|
defer testVault1.Stop()
|
||||||
|
|
||||||
testSignIntermediateCrossDC(t, provider1, provider2)
|
provider2, testVault2 := testVaultProviderWithConfig(t, false, map[string]interface{}{
|
||||||
|
"LeafCertTTL": "1h",
|
||||||
|
"PrivateKeyType": tc.CSRKeyType,
|
||||||
|
"PrivateKeyBits": tc.CSRKeyBits,
|
||||||
|
})
|
||||||
|
defer testVault2.Stop()
|
||||||
|
|
||||||
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVaultProvider_SignIntermediateConsul(t *testing.T) {
|
func TestVaultProvider_SignIntermediateConsul(t *testing.T) {
|
||||||
|
@ -241,7 +335,7 @@ func testVaultProviderWithConfig(t *testing.T, isRoot bool, rawConf map[string]i
|
||||||
|
|
||||||
provider := &VaultProvider{}
|
provider := &VaultProvider{}
|
||||||
|
|
||||||
if err := provider.Configure("asdf", isRoot, conf); err != nil {
|
if err := provider.Configure(connect.TestClusterID, isRoot, conf); err != nil {
|
||||||
testVault.Stop()
|
testVault.Stop()
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyTestCases is a list of the important CA key types that we should test
|
||||||
|
// against when signing. For now leaf keys are always EC P256 but CA can be EC
|
||||||
|
// (any NIST curve) or RSA (2048, 4096). Providers must be able to complete all
|
||||||
|
// signing operations with both types that includes:
|
||||||
|
// - Sign must be able to sign EC P256 leaf with all these types of CA key
|
||||||
|
// - CrossSignCA must be able to sign all these types of new CA key with all
|
||||||
|
// these types of old CA key.
|
||||||
|
// - SignIntermediate muse bt able to sign all the types of secondary
|
||||||
|
// intermediate CA key with all these types of primary CA key
|
||||||
|
var KeyTestCases = []struct {
|
||||||
|
Desc string
|
||||||
|
KeyType string
|
||||||
|
KeyBits int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Desc: "Default Key Type (EC 256)",
|
||||||
|
KeyType: connect.DefaultPrivateKeyType,
|
||||||
|
KeyBits: connect.DefaultPrivateKeyBits,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "RSA 2048",
|
||||||
|
KeyType: "rsa",
|
||||||
|
KeyBits: 2048,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASigningKeyTypes is a struct with params for tests that sign one CA CSR with
|
||||||
|
// another CA key.
|
||||||
|
type CASigningKeyTypes struct {
|
||||||
|
Desc string
|
||||||
|
SigningKeyType string
|
||||||
|
SigningKeyBits int
|
||||||
|
CSRKeyType string
|
||||||
|
CSRKeyBits int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASigningKeyTypeCases returns the cross-product of the important supported CA
|
||||||
|
// key types for generating table tests for CA signing tests (CrossSignCA and
|
||||||
|
// SignIntermediate).
|
||||||
|
func CASigningKeyTypeCases() []CASigningKeyTypes {
|
||||||
|
cases := make([]CASigningKeyTypes, 0, len(KeyTestCases)*len(KeyTestCases))
|
||||||
|
for _, outer := range KeyTestCases {
|
||||||
|
for _, inner := range KeyTestCases {
|
||||||
|
cases = append(cases, CASigningKeyTypes{
|
||||||
|
Desc: fmt.Sprintf("%s-%d signing %s-%d", outer.KeyType, outer.KeyBits,
|
||||||
|
inner.KeyType, inner.KeyBits),
|
||||||
|
SigningKeyType: outer.KeyType,
|
||||||
|
SigningKeyBits: outer.KeyBits,
|
||||||
|
CSRKeyType: inner.KeyType,
|
||||||
|
CSRKeyBits: inner.KeyBits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cases
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
@ -11,12 +12,41 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SigAlgoForKey returns the preferred x509.SignatureAlgorithm for a given key
|
||||||
|
// based on it's type. If the key type is not supported we return
|
||||||
|
// ECDSAWithSHA256 on the basis that it will fail anyway and we've already type
|
||||||
|
// checked keys by the time we call this in general.
|
||||||
|
func SigAlgoForKey(key crypto.Signer) x509.SignatureAlgorithm {
|
||||||
|
if _, ok := key.(*rsa.PrivateKey); ok {
|
||||||
|
return x509.SHA256WithRSA
|
||||||
|
}
|
||||||
|
// We default to ECDSA but don't bother detecting invalid key types as we do
|
||||||
|
// that in lots of other places and it will fail anyway if we try to sign with
|
||||||
|
// an incompatible type.
|
||||||
|
return x509.ECDSAWithSHA256
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigAlgoForKeyType returns the preferred x509.SignatureAlgorithm for a given
|
||||||
|
// key type string from configuration or an existing cert. If the key type is
|
||||||
|
// not supported we return ECDSAWithSHA256 on the basis that it will fail anyway
|
||||||
|
// and we've already type checked config by the time we call this in general.
|
||||||
|
func SigAlgoForKeyType(keyType string) x509.SignatureAlgorithm {
|
||||||
|
switch keyType {
|
||||||
|
case "rsa":
|
||||||
|
return x509.SHA256WithRSA
|
||||||
|
case "ec":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return x509.ECDSAWithSHA256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCSR returns a CSR to sign the given service along with the PEM-encoded
|
// CreateCSR returns a CSR to sign the given service along with the PEM-encoded
|
||||||
// private key for this certificate.
|
// private key for this certificate.
|
||||||
func CreateCSR(uri CertURI, privateKey crypto.Signer, extensions ...pkix.Extension) (string, error) {
|
func CreateCSR(uri CertURI, privateKey crypto.Signer, extensions ...pkix.Extension) (string, error) {
|
||||||
template := &x509.CertificateRequest{
|
template := &x509.CertificateRequest{
|
||||||
URIs: []*url.URL{uri.URI()},
|
URIs: []*url.URL{uri.URI()},
|
||||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
SignatureAlgorithm: SigAlgoForKey(privateKey),
|
||||||
ExtraExtensions: extensions,
|
ExtraExtensions: extensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,8 @@ func TestValidateBadConfigs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests the ability of a CA to sign a CSR using a different key type. If the key types differ, the test should fail.
|
// Tests the ability of a CA to sign a CSR using a different key type. This is
|
||||||
|
// allowed by TLS 1.2 and should succeed in all combinations.
|
||||||
func TestSignatureMismatches(t *testing.T) {
|
func TestSignatureMismatches(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
r := require.New(t)
|
r := require.New(t)
|
||||||
|
@ -135,15 +136,11 @@ func TestSignatureMismatches(t *testing.T) {
|
||||||
r.Equal(p1.keyType, ca.PrivateKeyType)
|
r.Equal(p1.keyType, ca.PrivateKeyType)
|
||||||
r.Equal(p1.keyBits, ca.PrivateKeyBits)
|
r.Equal(p1.keyBits, ca.PrivateKeyBits)
|
||||||
certPEM, keyPEM, err := testLeaf(t, "foobar.service.consul", ca, p2.keyType, p2.keyBits)
|
certPEM, keyPEM, err := testLeaf(t, "foobar.service.consul", ca, p2.keyType, p2.keyBits)
|
||||||
if p1.keyType == p2.keyType {
|
r.NoError(err)
|
||||||
r.NoError(err)
|
_, err = ParseCert(certPEM)
|
||||||
_, err := ParseCert(certPEM)
|
r.NoError(err)
|
||||||
r.NoError(err)
|
_, err = ParseSigner(keyPEM)
|
||||||
_, err = ParseSigner(keyPEM)
|
r.NoError(err)
|
||||||
r.NoError(err)
|
|
||||||
} else {
|
|
||||||
r.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,3 +206,16 @@ func IsHexString(input []byte) bool {
|
||||||
_, err := hex.DecodeString(s)
|
_, err := hex.DecodeString(s)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyInfoFromCert returns the key type and key bit length for the key used by
|
||||||
|
// the certificate.
|
||||||
|
func KeyInfoFromCert(cert *x509.Certificate) (keyType string, keyBits int, err error) {
|
||||||
|
switch k := cert.PublicKey.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return "ec", k.Curve.Params().BitSize, nil
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return "rsa", k.N.BitLen(), nil
|
||||||
|
default:
|
||||||
|
return "", 0, fmt.Errorf("unsupported key type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,35 @@ const TestClusterID = "11111111-2222-3333-4444-555555555555"
|
||||||
// unique names for the CA certs.
|
// unique names for the CA certs.
|
||||||
var testCACounter uint64
|
var testCACounter uint64
|
||||||
|
|
||||||
|
// ValidateLeaf is a convenience helper that returns an error if the certificate
|
||||||
|
// provided in leadPEM does not validate against the CAs provided. If there is
|
||||||
|
// an intermediate CA then it's cert must be in caPEMs as well as the root.
|
||||||
|
func ValidateLeaf(caPEM string, leafPEM string, intermediatePEMs []string) error {
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
ok := roots.AppendCertsFromPEM([]byte(caPEM))
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed to add root CA")
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediates := x509.NewCertPool()
|
||||||
|
for idx, ca := range intermediatePEMs {
|
||||||
|
ok := intermediates.AppendCertsFromPEM([]byte(ca))
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed to add intermediate CA at index %d to pool", idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf, err := ParseCert(leafPEM)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = leaf.Verify(x509.VerifyOptions{
|
||||||
|
Roots: roots,
|
||||||
|
Intermediates: intermediates,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *structs.CARoot {
|
func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *structs.CARoot {
|
||||||
var result structs.CARoot
|
var result structs.CARoot
|
||||||
result.Active = true
|
result.Active = true
|
||||||
|
@ -36,8 +65,6 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *struc
|
||||||
signer, keyPEM := testPrivateKey(t, keyType, keyBits)
|
signer, keyPEM := testPrivateKey(t, keyType, keyBits)
|
||||||
result.SigningKey = keyPEM
|
result.SigningKey = keyPEM
|
||||||
result.SigningKeyID = EncodeSigningKeyID(testKeyID(t, signer.Public()))
|
result.SigningKeyID = EncodeSigningKeyID(testKeyID(t, signer.Public()))
|
||||||
result.PrivateKeyType = keyType
|
|
||||||
result.PrivateKeyBits = keyBits
|
|
||||||
|
|
||||||
// The serial number for the cert
|
// The serial number for the cert
|
||||||
sn, err := testSerialNumber()
|
sn, err := testSerialNumber()
|
||||||
|
@ -83,6 +110,8 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *struc
|
||||||
result.SerialNumber = uint64(sn.Int64())
|
result.SerialNumber = uint64(sn.Int64())
|
||||||
result.NotBefore = template.NotBefore.UTC()
|
result.NotBefore = template.NotBefore.UTC()
|
||||||
result.NotAfter = template.NotAfter.UTC()
|
result.NotAfter = template.NotAfter.UTC()
|
||||||
|
result.PrivateKeyType = keyType
|
||||||
|
result.PrivateKeyBits = keyBits
|
||||||
|
|
||||||
// If there is a prior CA to cross-sign with, then we need to create that
|
// If there is a prior CA to cross-sign with, then we need to create that
|
||||||
// and set it as the signing cert.
|
// and set it as the signing cert.
|
||||||
|
@ -174,9 +203,9 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string,
|
||||||
return "", "", fmt.Errorf("failed to generate private key: %s", err)
|
return "", "", fmt.Errorf("failed to generate private key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sigAlgo := x509.ECDSAWithSHA256
|
rootKeyType, _, err := KeyInfoFromCert(caCert)
|
||||||
if keyType == "rsa" {
|
if err != nil {
|
||||||
sigAlgo = x509.SHA256WithRSA
|
return "", "", fmt.Errorf("error getting CA key type: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cert template for generation
|
// Cert template for generation
|
||||||
|
@ -184,7 +213,7 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string,
|
||||||
SerialNumber: sn,
|
SerialNumber: sn,
|
||||||
Subject: pkix.Name{CommonName: service},
|
Subject: pkix.Name{CommonName: service},
|
||||||
URIs: []*url.URL{spiffeId.URI()},
|
URIs: []*url.URL{spiffeId.URI()},
|
||||||
SignatureAlgorithm: sigAlgo,
|
SignatureAlgorithm: SigAlgoForKeyType(rootKeyType),
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageDataEncipherment |
|
KeyUsage: x509.KeyUsageDataEncipherment |
|
||||||
x509.KeyUsageKeyAgreement |
|
x509.KeyUsageKeyAgreement |
|
||||||
|
@ -218,7 +247,12 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string,
|
||||||
// TestLeaf returns a valid leaf certificate and it's private key for the named
|
// TestLeaf returns a valid leaf certificate and it's private key for the named
|
||||||
// service with the given CA Root.
|
// service with the given CA Root.
|
||||||
func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) {
|
func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) {
|
||||||
certPEM, keyPEM, err := testLeaf(t, service, root, root.PrivateKeyType, root.PrivateKeyBits)
|
// Currently we only support EC leaf keys and certs even if the CA is using
|
||||||
|
// RSA. We might allow Leafs to follow the signing CA key type later if we
|
||||||
|
// need to for compatibility sake but this is allowed by TLS 1.2 and works with
|
||||||
|
// both openssl verify (which we use as a sanity check in our tests of this
|
||||||
|
// package) and Go's TLS verification.
|
||||||
|
certPEM, keyPEM, err := testLeaf(t, service, root, DefaultPrivateKeyType, DefaultPrivateKeyBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -325,8 +359,6 @@ func testCAConfigSet(t testing.T, a TestAgentRPC,
|
||||||
"PrivateKey": ca.SigningKey,
|
"PrivateKey": ca.SigningKey,
|
||||||
"RootCert": ca.RootCert,
|
"RootCert": ca.RootCert,
|
||||||
"RotationPeriod": 180 * 24 * time.Hour,
|
"RotationPeriod": 180 * 24 * time.Hour,
|
||||||
"PrivateKeyType": ca.PrivateKeyType,
|
|
||||||
"PrivateKeyBits": ca.PrivateKeyBits,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
args := &structs.CARequest{
|
args := &structs.CARequest{
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hasOpenSSL is used to determine if the openssl CLI exists for unit tests.
|
// hasOpenSSL is used to determine if the openssl CLI exists for unit tests.
|
||||||
|
@ -35,7 +36,7 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assert := assert.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
// Create the certs
|
// Create the certs
|
||||||
ca := TestCAWithKeyType(t, nil, keyType, keyBits)
|
ca := TestCAWithKeyType(t, nil, keyType, keyBits)
|
||||||
|
@ -43,12 +44,12 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) {
|
||||||
|
|
||||||
// Create a temporary directory for storing the certs
|
// Create a temporary directory for storing the certs
|
||||||
td, err := ioutil.TempDir("", "consul")
|
td, err := ioutil.TempDir("", "consul")
|
||||||
assert.Nil(err)
|
require.NoError(err)
|
||||||
defer os.RemoveAll(td)
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
// Write the cert
|
// Write the cert
|
||||||
assert.Nil(ioutil.WriteFile(filepath.Join(td, "ca.pem"), []byte(ca.RootCert), 0644))
|
require.NoError(ioutil.WriteFile(filepath.Join(td, "ca.pem"), []byte(ca.RootCert), 0644))
|
||||||
assert.Nil(ioutil.WriteFile(filepath.Join(td, "leaf.pem"), []byte(leaf), 0644))
|
require.NoError(ioutil.WriteFile(filepath.Join(td, "leaf.pem"), []byte(leaf[:]), 0644))
|
||||||
|
|
||||||
// Use OpenSSL to verify so we have an external, known-working process
|
// Use OpenSSL to verify so we have an external, known-working process
|
||||||
// that can verify this outside of our own implementations.
|
// that can verify this outside of our own implementations.
|
||||||
|
@ -56,8 +57,11 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) {
|
||||||
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf.pem")
|
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf.pem")
|
||||||
cmd.Dir = td
|
cmd.Dir = td
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
t.Log(string(output))
|
t.Log("STDOUT:", string(output))
|
||||||
assert.Nil(err)
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
t.Log("STDERR:", string(ee.Stderr))
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test cross-signing.
|
// Test cross-signing.
|
||||||
|
|
|
@ -297,56 +297,73 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
|
||||||
func TestConnectCASign(t *testing.T) {
|
func TestConnectCASign(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
assert := assert.New(t)
|
tests := []struct {
|
||||||
require := require.New(t)
|
caKeyType string
|
||||||
dir1, s1 := testServer(t)
|
caKeyBits int
|
||||||
defer os.RemoveAll(dir1)
|
}{
|
||||||
defer s1.Shutdown()
|
{
|
||||||
codec := rpcClient(t, s1)
|
caKeyType: connect.DefaultPrivateKeyType,
|
||||||
defer codec.Close()
|
caKeyBits: connect.DefaultPrivateKeyBits,
|
||||||
|
},
|
||||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
{
|
||||||
|
// Ensure that an RSA Keyed CA can sign EC leaves and they validate.
|
||||||
// Generate a CSR and request signing
|
caKeyType: "rsa",
|
||||||
spiffeId := connect.TestSpiffeIDService(t, "web")
|
caKeyBits: 2048,
|
||||||
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))
|
|
||||||
|
|
||||||
// Generate a second CSR and request signing
|
|
||||||
spiffeId2 := connect.TestSpiffeIDService(t, "web2")
|
|
||||||
csr, _ = connect.TestCSR(t, spiffeId2)
|
|
||||||
args = &structs.CASignRequest{
|
|
||||||
Datacenter: "dc1",
|
|
||||||
CSR: csr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var reply2 structs.IssuedCert
|
for _, tt := range tests {
|
||||||
require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply2))
|
t.Run(fmt.Sprintf("%s-%d", tt.caKeyType, tt.caKeyBits), func(t *testing.T) {
|
||||||
require.True(reply2.ModifyIndex > reply.ModifyIndex)
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
dir1, s1 := testServerWithConfig(t, func(cfg *Config) {
|
||||||
|
cfg.CAConfig.Config["PrivateKeyType"] = tt.caKeyType
|
||||||
|
cfg.CAConfig.Config["PrivateKeyBits"] = tt.caKeyBits
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
// Get the current CA
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
state := s1.fsm.State()
|
|
||||||
_, ca, err := state.CARootActive(nil)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
// Verify that the cert is signed by the CA
|
// Generate a CSR and request signing
|
||||||
roots := x509.NewCertPool()
|
spiffeId := connect.TestSpiffeIDService(t, "web")
|
||||||
assert.True(roots.AppendCertsFromPEM([]byte(ca.RootCert)))
|
|
||||||
leaf, err := connect.ParseCert(reply.CertPEM)
|
|
||||||
require.NoError(err)
|
|
||||||
_, err = leaf.Verify(x509.VerifyOptions{
|
|
||||||
Roots: roots,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
// Verify other fields
|
// TestCSR will always generate a CSR with an EC key currently.
|
||||||
assert.Equal("web", reply.Service)
|
csr, _ := connect.TestCSR(t, spiffeId)
|
||||||
assert.Equal(spiffeId.URI().String(), reply.ServiceURI)
|
args := &structs.CASignRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
CSR: csr,
|
||||||
|
}
|
||||||
|
var reply structs.IssuedCert
|
||||||
|
require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply))
|
||||||
|
|
||||||
|
// Generate a second CSR and request signing
|
||||||
|
spiffeId2 := connect.TestSpiffeIDService(t, "web2")
|
||||||
|
csr, _ = connect.TestCSR(t, spiffeId2)
|
||||||
|
args = &structs.CASignRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
CSR: csr,
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply2 structs.IssuedCert
|
||||||
|
require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply2))
|
||||||
|
require.True(reply2.ModifyIndex > reply.ModifyIndex)
|
||||||
|
|
||||||
|
// Get the current CA
|
||||||
|
state := s1.fsm.State()
|
||||||
|
_, ca, err := state.CARootActive(nil)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Verify that the cert is signed by the CA
|
||||||
|
require.NoError(connect.ValidateLeaf(ca.RootCert, reply.CertPEM, nil))
|
||||||
|
|
||||||
|
// Verify other fields
|
||||||
|
assert.Equal("web", reply.Service)
|
||||||
|
assert.Equal(spiffeId.URI().String(), reply.ServiceURI)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bench how long Signing RPC takes. This was used to ballpark reasonable
|
// Bench how long Signing RPC takes. This was used to ballpark reasonable
|
||||||
|
|
|
@ -81,6 +81,10 @@ func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing root cert: %v", err)
|
return nil, fmt.Errorf("error parsing root cert: %v", err)
|
||||||
}
|
}
|
||||||
|
keyType, keyBits, err := connect.KeyInfoFromCert(rootCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting root key info: %v", err)
|
||||||
|
}
|
||||||
return &structs.CARoot{
|
return &structs.CARoot{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: fmt.Sprintf("%s CA Root Cert", strings.Title(provider)),
|
Name: fmt.Sprintf("%s CA Root Cert", strings.Title(provider)),
|
||||||
|
@ -90,6 +94,8 @@ func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error)
|
||||||
NotBefore: rootCert.NotBefore,
|
NotBefore: rootCert.NotBefore,
|
||||||
NotAfter: rootCert.NotAfter,
|
NotAfter: rootCert.NotAfter,
|
||||||
RootCert: pemValue,
|
RootCert: pemValue,
|
||||||
|
PrivateKeyType: keyType,
|
||||||
|
PrivateKeyBits: keyBits,
|
||||||
Active: true,
|
Active: true,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -224,13 +230,6 @@ func (s *Server) initializeRootCA(provider ca.Provider, conf *structs.CAConfigur
|
||||||
return fmt.Errorf("error getting intermediate cert: %v", err)
|
return fmt.Errorf("error getting intermediate cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
commonConfig, err := conf.GetCommonConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rootCA.PrivateKeyType = commonConfig.PrivateKeyType
|
|
||||||
rootCA.PrivateKeyBits = commonConfig.PrivateKeyBits
|
|
||||||
|
|
||||||
// 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,
|
||||||
// adding on any existing intermediate certs since they aren't directly
|
// adding on any existing intermediate certs since they aren't directly
|
||||||
// tied to the provider.
|
// tied to the provider.
|
||||||
|
|
|
@ -2,7 +2,10 @@ package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -24,109 +27,118 @@ import (
|
||||||
func TestLeader_SecondaryCA_Initialize(t *testing.T) {
|
func TestLeader_SecondaryCA_Initialize(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
masterToken := "8a85f086-dd95-4178-b128-e10902767c5c"
|
tests := []struct {
|
||||||
|
keyType string
|
||||||
// Initialize primary as the primary DC
|
keyBits int
|
||||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
}{
|
||||||
c.Datacenter = "primary"
|
{connect.DefaultPrivateKeyType, connect.DefaultPrivateKeyBits},
|
||||||
c.ACLDatacenter = "primary"
|
{"rsa", 2048},
|
||||||
c.Build = "1.6.0"
|
|
||||||
c.ACLsEnabled = true
|
|
||||||
c.ACLMasterToken = masterToken
|
|
||||||
c.ACLDefaultPolicy = "deny"
|
|
||||||
})
|
|
||||||
defer os.RemoveAll(dir1)
|
|
||||||
defer s1.Shutdown()
|
|
||||||
|
|
||||||
s1.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig)
|
|
||||||
|
|
||||||
testrpc.WaitForLeader(t, s1.RPC, "primary")
|
|
||||||
|
|
||||||
// secondary as a secondary DC
|
|
||||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
|
||||||
c.Datacenter = "secondary"
|
|
||||||
c.ACLDatacenter = "primary"
|
|
||||||
c.Build = "1.6.0"
|
|
||||||
c.ACLsEnabled = true
|
|
||||||
c.ACLDefaultPolicy = "deny"
|
|
||||||
c.ACLTokenReplication = true
|
|
||||||
})
|
|
||||||
defer os.RemoveAll(dir2)
|
|
||||||
defer s2.Shutdown()
|
|
||||||
|
|
||||||
s2.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig)
|
|
||||||
s2.tokens.UpdateReplicationToken(masterToken, token.TokenSourceConfig)
|
|
||||||
|
|
||||||
testrpc.WaitForLeader(t, s2.RPC, "secondary")
|
|
||||||
|
|
||||||
// Create the WAN link
|
|
||||||
joinWAN(t, s2, s1)
|
|
||||||
|
|
||||||
waitForNewACLs(t, s1)
|
|
||||||
waitForNewACLs(t, s2)
|
|
||||||
|
|
||||||
// Ensure s2 is authoritative.
|
|
||||||
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens, 1, 1, 0)
|
|
||||||
|
|
||||||
// Wait until the providers are fully bootstrapped.
|
|
||||||
var (
|
|
||||||
caRoot *structs.CARoot
|
|
||||||
secondaryProvider ca.Provider
|
|
||||||
intermediatePEM string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
retry.Run(t, func(r *retry.R) {
|
|
||||||
_, caRoot = s1.getCAProvider()
|
|
||||||
secondaryProvider, _ = s2.getCAProvider()
|
|
||||||
intermediatePEM, err = secondaryProvider.ActiveIntermediate()
|
|
||||||
require.NoError(r, err)
|
|
||||||
|
|
||||||
// Verify the root lists are equal in each DC's state store.
|
|
||||||
state1 := s1.fsm.State()
|
|
||||||
_, roots1, err := state1.CARoots(nil)
|
|
||||||
require.NoError(r, err)
|
|
||||||
|
|
||||||
state2 := s2.fsm.State()
|
|
||||||
_, roots2, err := state2.CARoots(nil)
|
|
||||||
require.NoError(r, err)
|
|
||||||
require.Len(r, roots1, 1)
|
|
||||||
require.Len(r, roots1, 1)
|
|
||||||
require.Equal(r, roots1[0].ID, roots2[0].ID)
|
|
||||||
require.Equal(r, roots1[0].RootCert, roots2[0].RootCert)
|
|
||||||
require.Empty(r, roots1[0].IntermediateCerts)
|
|
||||||
require.NotEmpty(r, roots2[0].IntermediateCerts)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Have secondary sign a leaf cert and make sure the chain is correct.
|
|
||||||
spiffeService := &connect.SpiffeIDService{
|
|
||||||
Host: "node1",
|
|
||||||
Namespace: "default",
|
|
||||||
Datacenter: "primary",
|
|
||||||
Service: "foo",
|
|
||||||
}
|
}
|
||||||
raw, _ := connect.TestCSR(t, spiffeService)
|
|
||||||
|
|
||||||
leafCsr, err := connect.ParseCSR(raw)
|
for _, tc := range tests {
|
||||||
require.NoError(t, err)
|
tc := tc
|
||||||
|
t.Run(fmt.Sprintf("%s-%d", tc.keyType, tc.keyBits), func(t *testing.T) {
|
||||||
|
masterToken := "8a85f086-dd95-4178-b128-e10902767c5c"
|
||||||
|
|
||||||
leafPEM, err := secondaryProvider.Sign(leafCsr)
|
// Initialize primary as the primary DC
|
||||||
require.NoError(t, err)
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
|
c.Datacenter = "primary"
|
||||||
|
c.ACLDatacenter = "primary"
|
||||||
|
c.Build = "1.6.0"
|
||||||
|
c.ACLsEnabled = true
|
||||||
|
c.ACLMasterToken = masterToken
|
||||||
|
c.ACLDefaultPolicy = "deny"
|
||||||
|
c.CAConfig.Config["PrivateKeyType"] = tc.keyType
|
||||||
|
c.CAConfig.Config["PrivateKeyBits"] = tc.keyBits
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
|
||||||
cert, err := connect.ParseCert(leafPEM)
|
s1.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Check that the leaf signed by the new cert can be verified using the
|
testrpc.WaitForLeader(t, s1.RPC, "primary")
|
||||||
// returned cert chain (signed intermediate + remote root).
|
|
||||||
intermediatePool := x509.NewCertPool()
|
|
||||||
intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM))
|
|
||||||
rootPool := x509.NewCertPool()
|
|
||||||
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))
|
|
||||||
|
|
||||||
_, err = cert.Verify(x509.VerifyOptions{
|
// secondary as a secondary DC
|
||||||
Intermediates: intermediatePool,
|
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||||
Roots: rootPool,
|
c.Datacenter = "secondary"
|
||||||
})
|
c.ACLDatacenter = "primary"
|
||||||
require.NoError(t, err)
|
c.Build = "1.6.0"
|
||||||
|
c.ACLsEnabled = true
|
||||||
|
c.ACLDefaultPolicy = "deny"
|
||||||
|
c.ACLTokenReplication = true
|
||||||
|
c.CAConfig.Config["PrivateKeyType"] = tc.keyType
|
||||||
|
c.CAConfig.Config["PrivateKeyBits"] = tc.keyBits
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir2)
|
||||||
|
defer s2.Shutdown()
|
||||||
|
|
||||||
|
s2.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig)
|
||||||
|
s2.tokens.UpdateReplicationToken(masterToken, token.TokenSourceConfig)
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s2.RPC, "secondary")
|
||||||
|
|
||||||
|
// Create the WAN link
|
||||||
|
joinWAN(t, s2, s1)
|
||||||
|
|
||||||
|
waitForNewACLs(t, s1)
|
||||||
|
waitForNewACLs(t, s2)
|
||||||
|
|
||||||
|
// Ensure s2 is authoritative.
|
||||||
|
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens, 1, 1, 0)
|
||||||
|
|
||||||
|
// Wait until the providers are fully bootstrapped.
|
||||||
|
var (
|
||||||
|
caRoot *structs.CARoot
|
||||||
|
secondaryProvider ca.Provider
|
||||||
|
intermediatePEM string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
|
_, caRoot = s1.getCAProvider()
|
||||||
|
secondaryProvider, _ = s2.getCAProvider()
|
||||||
|
intermediatePEM, err = secondaryProvider.ActiveIntermediate()
|
||||||
|
require.NoError(r, err)
|
||||||
|
|
||||||
|
// Sanity check CA is using the correct key type
|
||||||
|
require.Equal(r, tc.keyType, caRoot.PrivateKeyType)
|
||||||
|
require.Equal(r, tc.keyBits, caRoot.PrivateKeyBits)
|
||||||
|
|
||||||
|
// Verify the root lists are equal in each DC's state store.
|
||||||
|
state1 := s1.fsm.State()
|
||||||
|
_, roots1, err := state1.CARoots(nil)
|
||||||
|
require.NoError(r, err)
|
||||||
|
|
||||||
|
state2 := s2.fsm.State()
|
||||||
|
_, roots2, err := state2.CARoots(nil)
|
||||||
|
require.NoError(r, err)
|
||||||
|
require.Len(r, roots1, 1)
|
||||||
|
require.Len(r, roots2, 1)
|
||||||
|
require.Equal(r, roots1[0].ID, roots2[0].ID)
|
||||||
|
require.Equal(r, roots1[0].RootCert, roots2[0].RootCert)
|
||||||
|
require.Empty(r, roots1[0].IntermediateCerts)
|
||||||
|
require.NotEmpty(r, roots2[0].IntermediateCerts)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Have secondary sign a leaf cert and make sure the chain is correct.
|
||||||
|
spiffeService := &connect.SpiffeIDService{
|
||||||
|
Host: "node1",
|
||||||
|
Namespace: "default",
|
||||||
|
Datacenter: "primary",
|
||||||
|
Service: "foo",
|
||||||
|
}
|
||||||
|
raw, _ := connect.TestCSR(t, spiffeService)
|
||||||
|
|
||||||
|
leafCsr, err := connect.ParseCSR(raw)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leafPEM, err := secondaryProvider.Sign(leafCsr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the leaf signed by the new cert can be verified using the
|
||||||
|
// returned cert chain (signed intermediate + remote root).
|
||||||
|
require.NoError(t, connect.ValidateLeaf(caRoot.RootCert, leafPEM, []string{intermediatePEM}))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLeader_SecondaryCA_IntermediateRefresh(t *testing.T) {
|
func TestLeader_SecondaryCA_IntermediateRefresh(t *testing.T) {
|
||||||
|
@ -1202,44 +1214,83 @@ func TestLeader_PersistIntermediateCAs(t *testing.T) {
|
||||||
|
|
||||||
func TestLeader_ParseCARoot(t *testing.T) {
|
func TestLeader_ParseCARoot(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
pem string
|
name string
|
||||||
expectedError bool
|
pem string
|
||||||
|
wantSerial uint64
|
||||||
|
wantSigningKeyID string
|
||||||
|
wantKeyType string
|
||||||
|
wantKeyBits int
|
||||||
|
wantErr bool
|
||||||
}
|
}
|
||||||
|
// Test certs generated with
|
||||||
|
// go run connect/certgen/certgen.go -out-dir /tmp/connect-certs -key-type ec -key-bits 384
|
||||||
|
// for various key types. This does limit the exposure to formats that might
|
||||||
|
// exist in external certificates which can be used as Connect CAs.
|
||||||
|
// Specifically many other certs will have serial numbers that don't fit into
|
||||||
|
// 64 bits but for reasons we truncate down to 64 bits which means our
|
||||||
|
// `SerialNumber` will not match the one reported by openssl. We should
|
||||||
|
// probably fix that at some point as it seems like a big footgun but it would
|
||||||
|
// be a breaking API change to change the type to not be a JSON number and
|
||||||
|
// JSON numbers don't even support the full range of a uint64...
|
||||||
tests := []test{
|
tests := []test{
|
||||||
{"", true},
|
{"no cert", "", 0, "", "", 0, true},
|
||||||
{`-----BEGIN CERTIFICATE-----
|
{
|
||||||
MIIDHDCCAsKgAwIBAgIQS+meruRVzrmVwEhXNrtk9jAKBggqhkjOPQQDAjCBuTEL
|
name: "default cert",
|
||||||
MAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv
|
// Watchout for indentations they will break PEM format
|
||||||
MRowGAYDVQQJExExMDEgU2Vjb25kIFN0cmVldDEOMAwGA1UEERMFOTQxMDUxFzAV
|
pem: readTestData(t, "cert-with-ec-256-key.pem"),
|
||||||
BgNVBAoTDkhhc2hpQ29ycCBJbmMuMUAwPgYDVQQDEzdDb25zdWwgQWdlbnQgQ0Eg
|
// Based on `openssl x509 -noout -text` report from the cert
|
||||||
MTkzNzYxNzQwMjcxNzUxOTkyMzAyMzE1NDkxNjUzODYyMzAwNzE3MB4XDTE5MDQx
|
wantSerial: 8341954965092507701,
|
||||||
MjA5MTg0NVoXDTIwMDQxMTA5MTg0NVowHDEaMBgGA1UEAxMRY2xpZW50LmRjMS5j
|
wantSigningKeyID: "97:4D:17:81:64:F8:B4:AF:05:E8:6C:79:C5:40:3B:0E:3E:8B:C0:AE:38:51:54:8A:2F:05:DB:E3:E8:E4:24:EC",
|
||||||
b25zdWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS2UroGUh5k7eR//iPsn9ne
|
wantKeyType: "ec",
|
||||||
CMCVsERnjqQnK6eDWnM5kTXgXcPPe5pcAS9xs0g8BZ+oVsJSc7sH6RYvX+gw6bCl
|
wantKeyBits: 256,
|
||||||
o4IBRjCCAUIwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr
|
wantErr: false,
|
||||||
BgEFBQcDATAMBgNVHRMBAf8EAjAAMGgGA1UdDgRhBF84NDphNDplZjoxYTpjODo1
|
},
|
||||||
MzoxMDo1YTpjNTplYTpjZTphYTowZDo2ZjpjOTozODozZDphZjo0NTphZTo5OTo4
|
{
|
||||||
YzpiYjoyNzpiYzpiMzpmYTpmMDozMToxNDo4ZTozNDBqBgNVHSMEYzBhgF8yYTox
|
name: "ec 384 cert",
|
||||||
MjpjYTo0Mzo0NzowODpiZjoxYTo0Yjo4MTpkNDo2MzowNTo1ODowZToxYzo3Zjoy
|
// Watchout for indentations they will break PEM format
|
||||||
NTo0ZjozNDpmNDozYjpmYzo5YTpkNzo4Mjo2YjpkYzpmODo3YjphMTo5ZDAtBgNV
|
pem: readTestData(t, "cert-with-ec-384-key.pem"),
|
||||||
HREEJjAkghFjbGllbnQuZGMxLmNvbnN1bIIJbG9jYWxob3N0hwR/AAABMAoGCCqG
|
// Based on `openssl x509 -noout -text` report from the cert
|
||||||
SM49BAMCA0gAMEUCIHcLS74KSQ7RA+edwOprmkPTh1nolwXz9/y9CJ5nMVqEAiEA
|
wantSerial: 2935109425518279965,
|
||||||
h1IHCbxWsUT3AiARwj5/D/CUppy6BHIFkvcpOCQoVyo=
|
wantSigningKeyID: "0B:A0:88:9B:DC:95:31:51:2E:3D:D4:F9:42:D0:6A:A0:62:46:82:D2:7C:22:E7:29:A9:AA:E8:A5:8C:CF:C7:42",
|
||||||
-----END CERTIFICATE-----`, false},
|
wantKeyType: "ec",
|
||||||
|
wantKeyBits: 384,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rsa 4096 cert",
|
||||||
|
// Watchout for indentations they will break PEM format
|
||||||
|
pem: readTestData(t, "cert-with-rsa-4096-key.pem"),
|
||||||
|
// Based on `openssl x509 -noout -text` report from the cert
|
||||||
|
wantSerial: 5186695743100577491,
|
||||||
|
wantSigningKeyID: "92:FA:CC:97:57:1E:31:84:A2:33:DD:9B:6A:A8:7C:FC:BE:E2:94:CA:AC:B3:33:17:39:3B:B8:67:9B:DC:C1:08",
|
||||||
|
wantKeyType: "rsa",
|
||||||
|
wantKeyBits: 4096,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, tt := range tests {
|
||||||
root, err := parseCARoot(test.pem, "consul", "cluster")
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err == nil && test.expectedError {
|
require := require.New(t)
|
||||||
require.Error(t, err)
|
root, err := parseCARoot(tt.pem, "consul", "cluster")
|
||||||
}
|
if tt.wantErr {
|
||||||
if test.pem != "" {
|
require.Error(err)
|
||||||
rootCert, err := connect.ParseCert(test.pem)
|
return
|
||||||
require.NoError(t, err)
|
}
|
||||||
|
require.NoError(err)
|
||||||
// just to make sure these two are not the same
|
require.Equal(tt.wantSerial, root.SerialNumber)
|
||||||
require.NotEqual(t, rootCert.AuthorityKeyId, rootCert.SubjectKeyId)
|
require.Equal(strings.ToLower(tt.wantSigningKeyID), root.SigningKeyID)
|
||||||
|
require.Equal(tt.wantKeyType, root.PrivateKeyType)
|
||||||
require.Equal(t, connect.EncodeSigningKeyID(rootCert.SubjectKeyId), root.SigningKeyID)
|
require.Equal(tt.wantKeyBits, root.PrivateKeyBits)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readTestData(t *testing.T, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join("testdata", name)
|
||||||
|
bs, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed reading fixture file %s: %s", name, err)
|
||||||
|
}
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB3DCCAYKgAwIBAgIIc8ST19qlIDUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ
|
||||||
|
VGVzdCBDQSAxMB4XDTE5MTAxNzExNDYyOVoXDTI5MTAxNzExNDYyOVowFDESMBAG
|
||||||
|
A1UEAxMJVGVzdCBDQSAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErA61DUlq
|
||||||
|
qDnXAcHIHVJKBUtyDYoQmZB1T1H7NHTn4XezkF23RjL9Ha8DghMR/bwz7YhZ1Tv6
|
||||||
|
UnYSq5r28P6b06OBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zApBgNVHQ4EIgQgl00XgWT4tK8F6Gx5xUA7Dj6LwK44UVSKLwXb4+jkJOwwKwYD
|
||||||
|
VR0jBCQwIoAgl00XgWT4tK8F6Gx5xUA7Dj6LwK44UVSKLwXb4+jkJOwwPwYDVR0R
|
||||||
|
BDgwNoY0c3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1
|
||||||
|
NTU1LmNvbnN1bDAKBggqhkjOPQQDAgNIADBFAiEA/x2MeYU5vCk2hwP7zlrv7bx3
|
||||||
|
9zx5YSbn04sgP6sNK30CIEPfjxDGy6K2dPDckATboYkZVQ4CJpPd6WrgwQaHpWC9
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICGTCCAZ+gAwIBAgIIKLuaeLzq/R0wCgYIKoZIzj0EAwMwFDESMBAGA1UEAxMJ
|
||||||
|
VGVzdCBDQSAxMB4XDTE5MTAxNzExNTUxOFoXDTI5MTAxNzExNTUxOFowFDESMBAG
|
||||||
|
A1UEAxMJVGVzdCBDQSAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFWtdx2o3c9qv
|
||||||
|
ka8vxPO2Iv9NAbiwh3cl1a90miRzhQMP7s6wycXfl1xKE02PRxiLQtuukKwE6ohv
|
||||||
|
Ha5h4kkqGB+YdOT+18JS+ixJwmmSZL5pkAh6SMEGby3wf5sap5F/o4G9MIG6MA4G
|
||||||
|
A1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MCkGA1UdDgQiBCALoIib3JUx
|
||||||
|
US491PlC0GqgYkaC0nwi5ympquiljM/HQjArBgNVHSMEJDAigCALoIib3JUxUS49
|
||||||
|
1PlC0GqgYkaC0nwi5ympquiljM/HQjA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMTEx
|
||||||
|
MTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49
|
||||||
|
BAMDA2gAMGUCMBT0orKHSATvulb6nRxVHq3OWOfmVgHu8VUCq9yuyAu1AAy/przY
|
||||||
|
/U0ury3g8T4jhwIxAIoCqYwWSJMFb13DZAR3XY+aFssVP5+vzhlaulqtg+YqjpKP
|
||||||
|
KzuCBpS3yUyAwWDphg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,31 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFaDCCA1CgAwIBAgIIR/rYTE2KZtMwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UE
|
||||||
|
AxMJVGVzdCBDQSAxMB4XDTE5MTAxNzExNTMxNVoXDTI5MTAxNzExNTMxNVowFDES
|
||||||
|
MBAGA1UEAxMJVGVzdCBDQSAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
|
||||||
|
AgEAlIsTpCearMRx0fW2BKmGXHUYCf1ATALcRn31VQCuzszyt77RRbQn+v/XB+Q5
|
||||||
|
Td+/o3ZkpKvCTAF31oZI0ihXEEq7o13pERGPZdD//jNvgePIoauzxjkWfEX5bQGx
|
||||||
|
PxY3mGWakmiunQHm3ls8bm+ZYBfDM8rJTaKFBUCaNe6xi5znQ953+YELV0YHpSXm
|
||||||
|
H0lP93/RVeua9094+YNadUfB1nOWvlayn/YX3oXPAwQsoT1DlqRlGFDAp3MPFNkC
|
||||||
|
RvymdigxAf1HrsyZ+MlD99Htgx2YkmHRXidRW9OzMmVSTe5gT6AOCvC4Zd5udH2H
|
||||||
|
bH+DUNuyJr2YC0VB7RBWbqCvpmxItLaDoSHLndRjP0wY9chMDqrKKRIzupeaYfVK
|
||||||
|
isFdSwj39EnNQEfm7ZJT/8aRNtF8pVFYHO0fZ+2eObpteWyQj0iOJjHuRpUWI2LT
|
||||||
|
ZMEPkj0N2I1tAvg1g/RYasXKwme+L7ejxzyYnC8kqDf4/C4tNaDURXP5fWn8aVsI
|
||||||
|
Sp8gMZHxXD8e8FAMPuDQi4y0FZu9bY9mJ71OSqcR5u0DHKzaCfAwr+qheFxCCly0
|
||||||
|
UnP7FAOSg96mRYAzgYGTFM5/HSmj0vvnsfAAebuiloE4IKfeBzp9kPuauEbcGbTa
|
||||||
|
4bBbaoamqdEUT3bFqRXWCXCQ6kT1xos+YcfjBkpfmdOrrdUCAwEAAaOBvTCBujAO
|
||||||
|
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zApBgNVHQ4EIgQgkvrMl1ce
|
||||||
|
MYSiM92baqh8/L7ilMqsszMXOTu4Z5vcwQgwKwYDVR0jBCQwIoAgkvrMl1ceMYSi
|
||||||
|
M92baqh8/L7ilMqsszMXOTu4Z5vcwQgwPwYDVR0RBDgwNoY0c3BpZmZlOi8vMTEx
|
||||||
|
MTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1NTU1LmNvbnN1bDANBgkqhkiG
|
||||||
|
9w0BAQsFAAOCAgEAaGorq69i6S6RIsOXx6zou2gr2ez6siVFkxoJEXx8lZKS8UXs
|
||||||
|
mmAxJXXKePv921zFeUB5jcdxIaBlxyEHiJGr9KUDYWnkAY0g8343JueY8epPOGpb
|
||||||
|
u1QoTpCRSJBdOyakK3ZhYqRa28G0fP0eQQ+mF54X2YA4jtg7pb30wahQd9U0M2ey
|
||||||
|
A6VLw4xdmqC7KOfJSvQcCZJPi5Wkqv/pf55WmS28zhxrwMC9U4vaKVRsGKJi/p/X
|
||||||
|
WUmyPpOUM41nEylcpFvs/Xx8eKUSlWRaMTWYHUKTdaXKYL/1op2PhuyoYkH384aj
|
||||||
|
P7RzLkC1fRC8MPXo9L1YSoK7vxL5K/zLkJFb3KopDzujGx1cZbgO8QREe16bVN4b
|
||||||
|
gFLwIzW+VzywmtoSur5V9ythVfvRT1XesmsK/G2ySxWvipYIDYYJrwgKR5b0ikfz
|
||||||
|
RPw2YW6oqaMmZ9Uehxym8RDWqyyFPg9S0C73MTK7FitIROLW88hWKSpDDhFck/32
|
||||||
|
FVaRL8cC0KVlMCFByL/o6u0AsRNCOux1q3BJEdmAh7VI84+SPgztHFkptR4VnlHZ
|
||||||
|
kKTj2Mj/OylHHwhe6AU9pbtAGM6DtcqSjmd4wrkRX8WJDd/F3RlYZ8WhOToOj9gP
|
||||||
|
ra4mUhGz/OlDg6vN9TSeVlb5Ap7c38KoCmmt2n+F/KUpe6V4L1QA5yfz0S8=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -102,8 +102,14 @@ type CARoot struct {
|
||||||
// active root.
|
// active root.
|
||||||
RotatedOutAt time.Time `json:"-"`
|
RotatedOutAt time.Time `json:"-"`
|
||||||
|
|
||||||
// Type of private key used to create the CA cert.
|
// PrivateKeyType is the type of the private key used to sign certificates. It
|
||||||
|
// may be "rsa" or "ec". This is provided as a convenience to avoid parsing
|
||||||
|
// the public key to from the certificate to infer the type.
|
||||||
PrivateKeyType string
|
PrivateKeyType string
|
||||||
|
|
||||||
|
// PrivateKeyBits is the length of the private key used to sign certificates.
|
||||||
|
// This is provided as a convenience to avoid parsing the public key from the
|
||||||
|
// certificate to infer the type.
|
||||||
PrivateKeyBits int
|
PrivateKeyBits int
|
||||||
|
|
||||||
RaftIndex
|
RaftIndex
|
||||||
|
@ -282,7 +288,18 @@ type CommonCAProviderConfig struct {
|
||||||
// is used. This is ignored if CSRMaxPerSecond is non-zero.
|
// is used. This is ignored if CSRMaxPerSecond is non-zero.
|
||||||
CSRMaxConcurrent int
|
CSRMaxConcurrent int
|
||||||
|
|
||||||
|
// PrivateKeyType specifies which type of key the CA should generate. It only
|
||||||
|
// applies when the provider is generating its own key and is ignored if the
|
||||||
|
// provider already has a key or an external key is provided. Supported values
|
||||||
|
// are "ec" or "rsa". "ec" is the default and will generate a NIST P-256
|
||||||
|
// Elliptic key.
|
||||||
PrivateKeyType string
|
PrivateKeyType string
|
||||||
|
|
||||||
|
// PrivateKeyBits specifies the number of bits the CA's private key should
|
||||||
|
// use. For RSA, supported values are 2048 and 4096. For EC, supported values
|
||||||
|
// are 224, 256, 384 and 521 and correspond to the NIST P-* curve of the same
|
||||||
|
// name. As with PrivateKeyType this is only relevant whan the provier is
|
||||||
|
// generating new CA keys (root or intermediate).
|
||||||
PrivateKeyBits int
|
PrivateKeyBits int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,14 +319,14 @@ func (c CommonCAProviderConfig) Validate() error {
|
||||||
switch c.PrivateKeyType {
|
switch c.PrivateKeyType {
|
||||||
case "ec":
|
case "ec":
|
||||||
if c.PrivateKeyBits != 224 && c.PrivateKeyBits != 256 && c.PrivateKeyBits != 384 && c.PrivateKeyBits != 521 {
|
if c.PrivateKeyBits != 224 && c.PrivateKeyBits != 256 && c.PrivateKeyBits != 384 && c.PrivateKeyBits != 521 {
|
||||||
return fmt.Errorf("ECDSA key length must be one of (224, 256, 384, 521) bits")
|
return fmt.Errorf("EC key length must be one of (224, 256, 384, 521) bits")
|
||||||
}
|
}
|
||||||
case "rsa":
|
case "rsa":
|
||||||
if c.PrivateKeyBits != 2048 && c.PrivateKeyBits != 4096 {
|
if c.PrivateKeyBits != 2048 && c.PrivateKeyBits != 4096 {
|
||||||
return fmt.Errorf("RSA key length must be 2048 or 4096 bits")
|
return fmt.Errorf("RSA key length must be 2048 or 4096 bits")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("private key type must be either 'ecdsa' or 'rsa'")
|
return fmt.Errorf("private key type must be either 'ec' or 'rsa'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
// You can verify a given leaf with a given root using:
|
// You can verify a given leaf with a given root using:
|
||||||
//
|
//
|
||||||
// $ openssl verify -verbose -CAfile ca2-ca.cert.pem ca1-svc-db.cert.pem
|
// $ openssl verify -verbose -CAfile ca1-ca.cert.pem ca1-svc-db.cert.pem
|
||||||
//
|
//
|
||||||
// Note that to verify via the cross-signed intermediate, openssl requires it to
|
// Note that to verify via the cross-signed intermediate, openssl requires it to
|
||||||
// be bundled with the _root_ CA bundle and will ignore the cert if it's passed
|
// be bundled with the _root_ CA bundle and will ignore the cert if it's passed
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
bind_addr = "0.0.0.0"
|
||||||
|
advertise_addr = "{{ GetInterfaceIP \"eth0\" }}"
|
|
@ -0,0 +1,7 @@
|
||||||
|
connect {
|
||||||
|
enabled = true
|
||||||
|
ca_config {
|
||||||
|
private_key_type = "rsa"
|
||||||
|
private_key_bits = 2048
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
snapshot_envoy_admin localhost:19000 s1 primary || true
|
||||||
|
snapshot_envoy_admin localhost:19001 s2 secondary || true
|
|
@ -0,0 +1,17 @@
|
||||||
|
services {
|
||||||
|
name = "s1"
|
||||||
|
port = 8080
|
||||||
|
connect {
|
||||||
|
sidecar_service {
|
||||||
|
proxy {
|
||||||
|
upstreams = [
|
||||||
|
{
|
||||||
|
destination_name = "s2"
|
||||||
|
datacenter = "secondary"
|
||||||
|
local_bind_port = 5000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# We don't want an s2 service in the primary dc
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eEuo pipefail
|
||||||
|
|
||||||
|
gen_envoy_bootstrap s1 19000 primary
|
||||||
|
retry_default docker_consul primary curl -s "http://localhost:8500/v1/catalog/service/consul?dc=secondary" >/dev/null
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
@test "s1 proxy is running correct version" {
|
||||||
|
assert_envoy_version 19000
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 proxy admin is up on :19000" {
|
||||||
|
retry_default curl -f -s localhost:19000/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 proxy listener should be up and have right cert" {
|
||||||
|
assert_proxy_presents_cert_uri localhost:21000 s1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 upstream should have healthy endpoints for s2" {
|
||||||
|
assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.secondary HEALTHY 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 upstream should be able to connect to s2" {
|
||||||
|
run retry_default curl -s -f -d hello localhost:5000
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "hello" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 upstream made 1 connection" {
|
||||||
|
assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.secondary.*cx_total" 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ca key should be RSA" {
|
||||||
|
run retry_default curl -f -s 127.0.0.1:8500/v1/connect/ca/roots
|
||||||
|
|
||||||
|
echo "$status"
|
||||||
|
echo "OUTPUT: $output"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
KEY_TYPE=$(echo "$output" | jq -r '.Roots[0].PrivateKeyType')
|
||||||
|
echo "KEY_TYPE: $KEY_TYPE"
|
||||||
|
|
||||||
|
[ "$KEY_TYPE" == "rsa" ]
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
retry_join_wan = ["consul-primary"]
|
|
@ -0,0 +1 @@
|
||||||
|
# we don't want an s1 service in the secondary dc
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eEuo pipefail
|
||||||
|
|
||||||
|
gen_envoy_bootstrap s2 19001 secondary
|
||||||
|
retry_default docker_consul secondary curl -s "http://localhost:8500/v1/catalog/service/consul?dc=primary" >/dev/null
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
@test "s2 proxy is running correct version" {
|
||||||
|
assert_envoy_version 19001
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy admin is up on :19001" {
|
||||||
|
retry_default curl -f -s localhost:19001/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy listener should be up and have right cert" {
|
||||||
|
assert_proxy_presents_cert_uri localhost:21000 s2 secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy should be healthy" {
|
||||||
|
assert_service_has_healthy_instances s2 1 secondary
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2-secondary s2-sidecar-proxy-secondary"
|
||||||
|
export REQUIRE_SECONDARY=1
|
|
@ -17,7 +17,7 @@ FILTER_TESTS=${FILTER_TESTS:-}
|
||||||
STOP_ON_FAIL=${STOP_ON_FAIL:-}
|
STOP_ON_FAIL=${STOP_ON_FAIL:-}
|
||||||
|
|
||||||
# ENVOY_VERSIONS is the list of envoy versions to run each test against
|
# ENVOY_VERSIONS is the list of envoy versions to run each test against
|
||||||
ENVOY_VERSIONS=${ENVOY_VERSIONS:-"1.10.0 1.9.1 1.8.0 1.11.1"}
|
ENVOY_VERSIONS=${ENVOY_VERSIONS:-"1.10.0 1.9.1 1.8.0 1.11.2"}
|
||||||
|
|
||||||
if [ ! -z "$DEBUG" ] ; then
|
if [ ! -z "$DEBUG" ] ; then
|
||||||
set -x
|
set -x
|
||||||
|
|
|
@ -135,8 +135,8 @@ $ curl \
|
||||||
"RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjExNjMzMjhaFw0yODA1MTgxNjMzMjhaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER0qlxjnRcMEr\niSGlH7G7dYU7lzBEmLUSMZkyBbClmyV8+e8WANemjn+PLnCr40If9cmpr7RnC9Qk\nGTaLnLiF16OCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1OTpjMjpmYTo0ZTo3\nNTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToyNDpiMDowNDpiMzpl\nODo5Nzo1Yjo3ZTBqBgNVHSMEYzBhgF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1\nOTpjMjpmYTo0ZTo3NTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToy\nNDpiMDowNDpiMzplODo5Nzo1Yjo3ZTA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMjRk\nZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgisxMjRkZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIQDzkkI7R+0U12a+zq2EQhP/n2mHmta+\nfs2hBxWIELGwTAIgLdO7RRw+z9nnxCIA6kNl//mIQb+PGItespiHZKAz74Q=\n-----END CERTIFICATE-----\n",
|
"RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjExNjMzMjhaFw0yODA1MTgxNjMzMjhaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER0qlxjnRcMEr\niSGlH7G7dYU7lzBEmLUSMZkyBbClmyV8+e8WANemjn+PLnCr40If9cmpr7RnC9Qk\nGTaLnLiF16OCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1OTpjMjpmYTo0ZTo3\nNTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToyNDpiMDowNDpiMzpl\nODo5Nzo1Yjo3ZTBqBgNVHSMEYzBhgF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1\nOTpjMjpmYTo0ZTo3NTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToy\nNDpiMDowNDpiMzplODo5Nzo1Yjo3ZTA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMjRk\nZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgisxMjRkZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIQDzkkI7R+0U12a+zq2EQhP/n2mHmta+\nfs2hBxWIELGwTAIgLdO7RRw+z9nnxCIA6kNl//mIQb+PGItespiHZKAz74Q=\n-----END CERTIFICATE-----\n",
|
||||||
"IntermediateCerts": null,
|
"IntermediateCerts": null,
|
||||||
"Active": true,
|
"Active": true,
|
||||||
"PrivateKeyType": "",
|
"PrivateKeyType": "ec",
|
||||||
"PrivateKeyBits": 0,
|
"PrivateKeyBits": 256,
|
||||||
"CreateIndex": 8,
|
"CreateIndex": 8,
|
||||||
"ModifyIndex": 8
|
"ModifyIndex": 8
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,8 +56,8 @@ $ curl \
|
||||||
"RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjUyMTM5MjNaFw0yODA1MjIyMTM5MjNaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq4S32Pu0/VL4\nG75gvdyQuAhqMZFsfBRwD3pgvblgZMeJc9KDosxnPR+W34NXtMD/860NNVJIILln\n9lLhIjWPQqOCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDplMzo4ODpiYjo5Yzpl\nMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2MzpmMzo3NDo1NTpj\nYTpiMDpiNTo2NTBqBgNVHSMEYzBhgF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDpl\nMzo4ODpiYjo5YzplMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2\nMzpmMzo3NDo1NTpjYTpiMDpiNTo2NTA/BgNVHREEODA2hjRzcGlmZmU6Ly83ZjQy\nZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgis3ZjQyZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIBBBDOWXWApx4S6bHJ49AW87Nw8uQ/gJ\nJ6lvm3HzEQw2AiEA4PVqWt+z8fsQht0cACM42kghL97SgDSf8rgCqfLYMng=\n-----END CERTIFICATE-----\n",
|
"RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjUyMTM5MjNaFw0yODA1MjIyMTM5MjNaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq4S32Pu0/VL4\nG75gvdyQuAhqMZFsfBRwD3pgvblgZMeJc9KDosxnPR+W34NXtMD/860NNVJIILln\n9lLhIjWPQqOCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDplMzo4ODpiYjo5Yzpl\nMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2MzpmMzo3NDo1NTpj\nYTpiMDpiNTo2NTBqBgNVHSMEYzBhgF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDpl\nMzo4ODpiYjo5YzplMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2\nMzpmMzo3NDo1NTpjYTpiMDpiNTo2NTA/BgNVHREEODA2hjRzcGlmZmU6Ly83ZjQy\nZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgis3ZjQyZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIBBBDOWXWApx4S6bHJ49AW87Nw8uQ/gJ\nJ6lvm3HzEQw2AiEA4PVqWt+z8fsQht0cACM42kghL97SgDSf8rgCqfLYMng=\n-----END CERTIFICATE-----\n",
|
||||||
"IntermediateCerts": null,
|
"IntermediateCerts": null,
|
||||||
"Active": true,
|
"Active": true,
|
||||||
"PrivateKeyType": "",
|
"PrivateKeyType": "ec",
|
||||||
"PrivateKeyBits": 0,
|
"PrivateKeyBits": 256,
|
||||||
"CreateIndex": 8,
|
"CreateIndex": 8,
|
||||||
"ModifyIndex": 8
|
"ModifyIndex": 8
|
||||||
}
|
}
|
||||||
|
|
|
@ -914,19 +914,18 @@ default will automatically work with some tooling.
|
||||||
|
|
||||||
<p>There are also a number of common configuration options supported by all providers:</p>
|
<p>There are also a number of common configuration options supported by all providers:</p>
|
||||||
|
|
||||||
* <a name="ca_leaf_cert_ttl"></a><a href="#ca_leaf_cert_ttl">`leaf_cert_ttl`</a> The upper bound on the
|
* <a name="ca_csr_max_concurrent"></a><a
|
||||||
lease duration of a leaf certificate issued for a service. In most
|
href="#ca_csr_max_concurrent">`csr_max_concurrent`</a> Sets a limit
|
||||||
cases a new leaf certificate will be requested by a proxy before this
|
on how many Certificate Signing Requests will be processed
|
||||||
limit is reached. This is also the effective limit on how long a
|
concurrently. Defaults to 0 (disabled). This is useful when you have
|
||||||
server outage can last (with no leader) before network connections
|
more than one or two cores available to the server. For example on an
|
||||||
will start being rejected, and as a result the defaults is `72h` to
|
8 core server, setting this to 1 will ensure that even during a CA
|
||||||
last through a weekend without intervention. This value cannot be
|
rotation no more than one server core on the leader will be consumed
|
||||||
lower than 1 hour or higher than 1 year.
|
at a time with generating new certificates. Setting this is
|
||||||
|
recommended _instead_ of `csr_max_per_second` where you know there are
|
||||||
This value is also used when rotating out old root certificates from
|
multiple cores available since it is simpler to reason about limiting
|
||||||
the cluster. When a root certificate has been inactive (rotated out)
|
CSR resources this way without artificially slowing down rotations.
|
||||||
for more than twice the *current* `leaf_cert_ttl`, it will be removed
|
Added in 1.4.1.
|
||||||
from the trusted list.
|
|
||||||
|
|
||||||
* <a name="ca_csr_max_per_second"></a><a
|
* <a name="ca_csr_max_per_second"></a><a
|
||||||
href="#ca_csr_max_per_second">`csr_max_per_second`</a> Sets a rate
|
href="#ca_csr_max_per_second">`csr_max_per_second`</a> Sets a rate
|
||||||
|
@ -941,18 +940,58 @@ default will automatically work with some tooling.
|
||||||
`csr_max_concurrent` instead if servers have more than one core.
|
`csr_max_concurrent` instead if servers have more than one core.
|
||||||
Setting this to zero disables rate limiting. Added in 1.4.1.
|
Setting this to zero disables rate limiting. Added in 1.4.1.
|
||||||
|
|
||||||
* <a name="ca_csr_max_concurrent"></a><a
|
* <a name="ca_leaf_cert_ttl"></a><a href="#ca_leaf_cert_ttl">`leaf_cert_ttl`</a> The upper bound on the
|
||||||
href="#ca_csr_max_concurrent">`csr_max_concurrent`</a> Sets a limit
|
lease duration of a leaf certificate issued for a service. In most
|
||||||
on how many Certificate Signing Requests will be processed
|
cases a new leaf certificate will be requested by a proxy before this
|
||||||
concurrently. Defaults to 0 (disabled). This is useful when you have
|
limit is reached. This is also the effective limit on how long a
|
||||||
more than one or two cores available to the server. For example on an
|
server outage can last (with no leader) before network connections
|
||||||
8 core server, setting this to 1 will ensure that even during a CA
|
will start being rejected, and as a result the defaults is `72h` to
|
||||||
rotation no more than one server core on the leader will be consumed
|
last through a weekend without intervention. This value cannot be
|
||||||
at a time with generating new certificates. Setting this is
|
lower than 1 hour or higher than 1 year.
|
||||||
recommended _instead_ of `csr_max_per_second` where you know there are
|
|
||||||
multiple cores available since it is simpler to reason about limiting
|
This value is also used when rotating out old root certificates from
|
||||||
CSR resources this way without artificially slowing down rotations.
|
the cluster. When a root certificate has been inactive (rotated out)
|
||||||
Added in 1.4.1.
|
for more than twice the *current* `leaf_cert_ttl`, it will be removed
|
||||||
|
from the trusted list.
|
||||||
|
|
||||||
|
* <a name="ca_private_key_type"></a><a
|
||||||
|
href="#ca_private_key_type">`private_key_type`</a> The type of key to
|
||||||
|
generate for this CA. This is only used when the provider is
|
||||||
|
generating a new key. If `private_key` is set for the Consul provider,
|
||||||
|
or existing root or intermediate PKI paths given for Vault then this
|
||||||
|
will be ignored. Currently supported options are `ec` or `rsa`.
|
||||||
|
Default is `ec`.
|
||||||
|
|
||||||
|
It is required that all servers in a Datacenter have
|
||||||
|
the same config for the CA. It is recommended that servers in
|
||||||
|
different Datacenters have the same CA config for key type and size
|
||||||
|
although the built-in CA and Vault provider will both allow mixed CA
|
||||||
|
key types.
|
||||||
|
|
||||||
|
Some CA providers (currently Vault) will not allow cross-signing a
|
||||||
|
new CA certificate with a different key type. This means that if you
|
||||||
|
migrate from an RSA-keyed Vault CA to an EC-keyed CA from any
|
||||||
|
provider, you may have to proceed without cross-signing which risks
|
||||||
|
temporary connection issues for workloads during the new certificate
|
||||||
|
rollout. We highly recommend testing this outside of production to
|
||||||
|
understand the impact and suggest sticking to same key type where
|
||||||
|
possible.
|
||||||
|
|
||||||
|
Note that this only affects _CA_ keys generated by the provider.
|
||||||
|
Leaf certificate keys are always EC 256 regardless of the CA
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
* <a name="ca_private_key_bits"></a><a
|
||||||
|
href="#ca_private_key_bits">`private_key_bits`</a> The length of key
|
||||||
|
to generate for this CA. This is only used when the provider is
|
||||||
|
generating a new key. If `private_key` is set for the Consul provider,
|
||||||
|
or existing root or intermediate PKI paths given for Vault then this
|
||||||
|
will be ignored.
|
||||||
|
|
||||||
|
Currently supported values are:
|
||||||
|
- `private_key_type = ec` (default): `224, 256, 384, 521`
|
||||||
|
corresponding to the NIST P-* curves of the same name.
|
||||||
|
- `private_key_type = rsa`: `2048, 4096`
|
||||||
|
|
||||||
* <a name="datacenter"></a><a href="#datacenter">`datacenter`</a> Equivalent to the
|
* <a name="datacenter"></a><a href="#datacenter">`datacenter`</a> Equivalent to the
|
||||||
[`-datacenter` command-line flag](#_datacenter).
|
[`-datacenter` command-line flag](#_datacenter).
|
||||||
|
|
Loading…
Reference in New Issue