ca: examine the full chain in newCARoot

make TestNewCARoot much more strict
compare the full result instead of only a few fields.
add a test case with 2 and 3 certificates in the pem
This commit is contained in:
Daniel Nephin 2022-01-05 18:21:04 -05:00
parent 71f3ae04e2
commit 42ec34d101
14 changed files with 376 additions and 124 deletions

View File

@ -135,6 +135,7 @@ type PrimaryProvider interface {
// the active intermediate. If multiple intermediates are needed to complete // the active intermediate. If multiple intermediates are needed to complete
// the chain from the signing certificate back to the active root, they should // the chain from the signing certificate back to the active root, they should
// all by bundled here. // all by bundled here.
// TODO: replace with GenerateLeafSigningCert
GenerateIntermediate() (string, error) GenerateIntermediate() (string, error)
// SignIntermediate will validate the CSR to ensure the trust domain in the // SignIntermediate will validate the CSR to ensure the trust domain in the
@ -193,7 +194,12 @@ type SecondaryProvider interface {
// //
// TODO: rename this struct // TODO: rename this struct
type RootResult struct { type RootResult struct {
// PEM encoded certificate that will be used as the primary CA. // PEM encoded bundle of CA certificates. The first certificate must be the
// primary CA used to sign intermediates for secondary datacenters, and the
// last certificate must be the trusted CA.
//
// If there is only a single certificate in the bundle then it will be used
// as both the primary CA and the trusted CA.
PEM string PEM string
} }

View File

@ -279,7 +279,7 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
if err != nil { if err != nil {
return RootResult{}, err return RootResult{}, err
} }
_, err = v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{ resp, err := v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary), "common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
"uri_sans": v.spiffeID.URI().String(), "uri_sans": v.spiffeID.URI().String(),
"key_type": v.config.PrivateKeyType, "key_type": v.config.PrivateKeyType,
@ -288,12 +288,10 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
if err != nil { if err != nil {
return RootResult{}, err return RootResult{}, err
} }
var ok bool
// retrieve the newly generated cert so that we can return it rootPEM, ok = resp.Data["certificate"].(string)
// TODO: is this already available from the Local().Write() above? if !ok {
rootPEM, err = v.getCA(v.config.RootPKIPath) return RootResult{}, fmt.Errorf("unexpected response from Vault: %v", resp.Data["certificate"])
if err != nil {
return RootResult{}, err
} }
default: default:
@ -302,7 +300,18 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
} }
} }
return RootResult{PEM: rootPEM}, nil rootChain, err := v.getCAChain(v.config.RootPKIPath)
if err != nil {
return RootResult{}, err
}
// Workaround for a bug in the Vault PKI API.
// See https://github.com/hashicorp/vault/issues/13489
if rootChain == "" {
rootChain = rootPEM
}
return RootResult{PEM: rootChain}, nil
} }
// GenerateIntermediateCSR creates a private key and generates a CSR // GenerateIntermediateCSR creates a private key and generates a CSR
@ -467,6 +476,29 @@ func (v *VaultProvider) getCA(path string) (string, error) {
return root, nil return root, nil
} }
// TODO: refactor to remove duplication with getCA
func (v *VaultProvider) getCAChain(path string) (string, error) {
req := v.client.NewRequest("GET", "/v1/"+path+"/ca_chain")
resp, err := v.client.RawRequest(req)
if resp != nil {
defer resp.Body.Close()
}
if resp != nil && resp.StatusCode == http.StatusNotFound {
return "", ErrBackendNotMounted
}
if err != nil {
return "", err
}
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
root := EnsureTrailingNewline(string(raw))
return root, nil
}
// GenerateIntermediate mounts the configured intermediate PKI backend if // GenerateIntermediate mounts the configured intermediate PKI backend if
// necessary, then generates and signs a new CA CSR using the root PKI backend // necessary, then generates and signs a new CA CSR using the root PKI backend
// and updates the intermediate backend to use that new certificate. // and updates the intermediate backend to use that new certificate.
@ -571,6 +603,7 @@ func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string,
// CrossSignCA takes a CA certificate and cross-signs it to form a trust chain // CrossSignCA takes a CA certificate and cross-signs it to form a trust chain
// back to our active root. // back to our active root.
func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) { func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
// TODO: is this necessary? Doesn't vault check this for us?
rootPEM, err := v.getCA(v.config.RootPKIPath) rootPEM, err := v.getCA(v.config.RootPKIPath)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -20,11 +20,11 @@ const (
DefaultIntermediateCertTTL = 24 * 365 * time.Hour DefaultIntermediateCertTTL = 24 * 365 * time.Hour
) )
func pemEncodeKey(key []byte, blockType string) (string, error) { func pemEncode(value []byte, blockType string) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: key}); err != nil { if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: value}); err != nil {
return "", fmt.Errorf("error encoding private key: %s", err) return "", fmt.Errorf("error encoding value %v: %s", blockType, err)
} }
return buf.String(), nil return buf.String(), nil
} }
@ -38,7 +38,7 @@ func generateRSAKey(keyBits int) (crypto.Signer, string, error) {
} }
bs := x509.MarshalPKCS1PrivateKey(pk) bs := x509.MarshalPKCS1PrivateKey(pk)
pemBlock, err := pemEncodeKey(bs, "RSA PRIVATE KEY") pemBlock, err := pemEncode(bs, "RSA PRIVATE KEY")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -73,7 +73,7 @@ func generateECDSAKey(keyBits int) (crypto.Signer, string, error) {
return nil, "", fmt.Errorf("error marshaling ECDSA private key: %s", err) return nil, "", fmt.Errorf("error marshaling ECDSA private key: %s", err)
} }
pemBlock, err := pemEncodeKey(bs, "EC PRIVATE KEY") pemBlock, err := pemEncode(bs, "EC PRIVATE KEY")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }

View File

@ -56,6 +56,21 @@ func ParseLeafCerts(pemValue string) (*x509.Certificate, *x509.CertPool, error)
return leaf, intermediates, nil return leaf, intermediates, nil
} }
// CertSubjects can be used in debugging to return the subject of each
// certificate in the PEM bundle. Each subject is separated by a newline.
func CertSubjects(pem string) string {
certs, err := parseCerts(pem)
if err != nil {
return err.Error()
}
var buf strings.Builder
for _, cert := range certs {
buf.WriteString(cert.Subject.String())
buf.WriteString("\n")
}
return buf.String()
}
// ParseCerts parses the all x509 certificates from a PEM-encoded value. // ParseCerts parses the all x509 certificates from a PEM-encoded value.
// The first returned cert is a leaf cert and any other ones are intermediates. // The first returned cert is a leaf cert and any other ones are intermediates.
// //
@ -90,21 +105,10 @@ func parseCerts(pemValue string) ([]*x509.Certificate, error) {
return out, nil return out, nil
} }
// CalculateCertFingerprint parses the x509 certificate from a PEM-encoded value // CalculateCertFingerprint calculates the SHA-1 fingerprint from the cert bytes.
// and calculates the SHA-1 fingerprint. func CalculateCertFingerprint(cert []byte) string {
func CalculateCertFingerprint(pemValue string) (string, error) { hash := sha1.Sum(cert)
// The _ result below is not an error but the remaining PEM bytes. return HexString(hash[:])
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return "", fmt.Errorf("no PEM-encoded data found")
}
if block.Type != "CERTIFICATE" {
return "", fmt.Errorf("first PEM-block should be CERTIFICATE type")
}
hash := sha1.Sum(block.Bytes)
return HexString(hash[:]), nil
} }
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key // ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key

View File

@ -112,10 +112,7 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int, ttl ti
t.Fatalf("error encoding private key: %s", err) t.Fatalf("error encoding private key: %s", err)
} }
result.RootCert = buf.String() result.RootCert = buf.String()
result.ID, err = CalculateCertFingerprint(result.RootCert) result.ID = CalculateCertFingerprint(bs)
if err != nil {
t.Fatalf("error generating CA ID fingerprint: %s", err)
}
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()

View File

@ -253,28 +253,24 @@ func (c *CAManager) initializeCAConfig() (*structs.CAConfiguration, error) {
return config, nil return config, nil
} }
// parseCARoot returns a filled-in structs.CARoot from a raw PEM value. // newCARoot returns a filled-in structs.CARoot from a raw PEM value.
func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) { func newCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) {
id, err := connect.CalculateCertFingerprint(pemValue) primaryCert, err := connect.ParseCert(pemValue)
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing root fingerprint: %v", err) return nil, err
} }
rootCert, err := connect.ParseCert(pemValue) keyType, keyBits, err := connect.KeyInfoFromCert(primaryCert)
if err != nil {
return nil, fmt.Errorf("error parsing root cert: %v", err)
}
keyType, keyBits, err := connect.KeyInfoFromCert(rootCert)
if err != nil { if err != nil {
return nil, fmt.Errorf("error extracting root key info: %v", err) return nil, fmt.Errorf("error extracting root key info: %v", err)
} }
return &structs.CARoot{ return &structs.CARoot{
ID: id, ID: connect.CalculateCertFingerprint(primaryCert.Raw),
Name: fmt.Sprintf("%s CA Root Cert", strings.Title(provider)), Name: fmt.Sprintf("%s CA Primary Cert", strings.Title(provider)),
SerialNumber: rootCert.SerialNumber.Uint64(), SerialNumber: primaryCert.SerialNumber.Uint64(),
SigningKeyID: connect.EncodeSigningKeyID(rootCert.SubjectKeyId), SigningKeyID: connect.EncodeSigningKeyID(primaryCert.SubjectKeyId),
ExternalTrustDomain: clusterID, ExternalTrustDomain: clusterID,
NotBefore: rootCert.NotBefore, NotBefore: primaryCert.NotBefore,
NotAfter: rootCert.NotAfter, NotAfter: primaryCert.NotAfter,
RootCert: pemValue, RootCert: pemValue,
PrivateKeyType: keyType, PrivateKeyType: keyType,
PrivateKeyBits: keyBits, PrivateKeyBits: keyBits,
@ -435,7 +431,7 @@ func (c *CAManager) secondaryInitialize(provider ca.Provider, conf *structs.CACo
} }
var roots structs.IndexedCARoots var roots structs.IndexedCARoots
if err := c.delegate.forwardDC("ConnectCA.Roots", c.serverConf.PrimaryDatacenter, &args, &roots); err != nil { if err := c.delegate.forwardDC("ConnectCA.Roots", c.serverConf.PrimaryDatacenter, &args, &roots); err != nil {
return err return fmt.Errorf("failed to get CA roots from primary DC: %w", err)
} }
c.secondarySetPrimaryRoots(roots) c.secondarySetPrimaryRoots(roots)
@ -487,12 +483,12 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
return fmt.Errorf("error generating CA root certificate: %v", err) return fmt.Errorf("error generating CA root certificate: %v", err)
} }
rootCA, err := parseCARoot(root.PEM, conf.Provider, conf.ClusterID) rootCA, err := newCARoot(root.PEM, conf.Provider, conf.ClusterID)
if err != nil { if err != nil {
return err return err
} }
// Also create the intermediate CA, which is the one that actually signs leaf certs // TODO: delete this
interPEM, err := provider.GenerateIntermediate() interPEM, err := provider.GenerateIntermediate()
if err != nil { if err != nil {
return fmt.Errorf("error generating intermediate cert: %v", err) return fmt.Errorf("error generating intermediate cert: %v", err)
@ -887,7 +883,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
} }
newRootPEM := providerRoot.PEM newRootPEM := providerRoot.PEM
newActiveRoot, err := parseCARoot(newRootPEM, args.Config.Provider, args.Config.ClusterID) newActiveRoot, err := newCARoot(newRootPEM, args.Config.Provider, args.Config.ClusterID)
if err != nil { if err != nil {
return err return err
} }
@ -940,7 +936,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
// get a cross-signed certificate. // get a cross-signed certificate.
// 3. Take the active root for the new provider and append the intermediate from step 2 // 3. Take the active root for the new provider and append the intermediate from step 2
// to its list of intermediates. // to its list of intermediates.
// TODO: this cert is already parsed once in parseCARoot, could we remove the second parse? // TODO: this cert is already parsed once in newCARoot, could we remove the second parse?
newRoot, err := connect.ParseCert(newRootPEM) newRoot, err := connect.ParseCert(newRootPEM)
if err != nil { if err != nil {
return err return err
@ -980,6 +976,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
} }
} }
// TODO: delete this
intermediate, err := newProvider.GenerateIntermediate() intermediate, err := newProvider.GenerateIntermediate()
if err != nil { if err != nil {
return err return err

View File

@ -618,7 +618,7 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
generateExternalRootCA(t, vclient) generateExternalRootCA(t, vclient)
meshRootPath := "pki-root" meshRootPath := "pki-root"
primaryCert := setupPrimaryCA(t, vclient, meshRootPath) primaryCert := setupPrimaryCA(t, vclient, meshRootPath, "")
_, s1 := testServerWithConfig(t, func(c *Config) { _, s1 := testServerWithConfig(t, func(c *Config) {
c.CAConfig = &structs.CAConfiguration{ c.CAConfig = &structs.CAConfiguration{
@ -635,7 +635,6 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
}, },
} }
}) })
defer s1.Shutdown()
runStep(t, "check primary DC", func(t *testing.T) { runStep(t, "check primary DC", func(t *testing.T) {
testrpc.WaitForTestAgent(t, s1.RPC, "dc1") testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
@ -704,10 +703,90 @@ func getLeafCert(t *testing.T, codec rpc.ClientCodec, trustDomain string, dc str
cert := structs.IssuedCert{} cert := structs.IssuedCert{}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert) err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert)
require.NoError(t, err) require.NoError(t, err)
return cert.CertPEM return cert.CertPEM
} }
func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
ca.SkipIfVaultNotPresent(t)
vault := ca.NewTestVaultServer(t)
vclient := vault.Client()
rootPEM := generateExternalRootCA(t, vclient)
primaryCAPath := "pki-primary"
primaryCert := setupPrimaryCA(t, vclient, primaryCAPath, rootPEM)
_, s1 := testServerWithConfig(t, func(c *Config) {
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": primaryCAPath,
"IntermediatePKIPath": "pki-intermediate/",
// TODO: there are failures to init the CA system if these are not set
// to the values of the already initialized CA.
"PrivateKeyType": "ec",
"PrivateKeyBits": 256,
},
}
})
runStep(t, "check primary DC", func(t *testing.T) {
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
codec := rpcClient(t, s1)
roots := structs.IndexedCARoots{}
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
require.Equal(t, primaryCert, roots.Roots[0].RootCert)
leafCertPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1")
verifyLeafCert(t, roots.Roots[0], leafCertPEM)
})
// TODO: renew primary leaf signing cert
// TODO: rotate root
runStep(t, "run secondary DC", func(t *testing.T) {
_, sDC2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.PrimaryDatacenter = "dc1"
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": primaryCAPath,
"IntermediatePKIPath": "pki-secondary/",
// TODO: there are failures to init the CA system if these are not set
// to the values of the already initialized CA.
"PrivateKeyType": "ec",
"PrivateKeyBits": 256,
},
}
})
defer sDC2.Shutdown()
joinWAN(t, sDC2, s1)
testrpc.WaitForActiveCARoot(t, sDC2.RPC, "dc2", nil)
codec := rpcClient(t, sDC2)
roots := structs.IndexedCARoots{}
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
leafCertPEM := getLeafCert(t, codec, roots.TrustDomain, "dc2")
verifyLeafCert(t, roots.Roots[0], leafCertPEM)
// TODO: renew secondary leaf signing cert
})
}
func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string { func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string {
t.Helper() t.Helper()
err := client.Sys().Mount("corp", &vaultapi.MountInput{ err := client.Sys().Mount("corp", &vaultapi.MountInput{
@ -725,10 +804,10 @@ func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string {
"ttl": "2400h", "ttl": "2400h",
}) })
require.NoError(t, err, "failed to generate root") require.NoError(t, err, "failed to generate root")
return resp.Data["certificate"].(string) return ca.EnsureTrailingNewline(resp.Data["certificate"].(string))
} }
func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string) string { func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string, rootPEM string) string {
t.Helper() t.Helper()
err := client.Sys().Mount(path, &vaultapi.MountInput{ err := client.Sys().Mount(path, &vaultapi.MountInput{
Type: "pki", Type: "pki",

View File

@ -15,11 +15,13 @@ import (
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc" msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
uuid "github.com/hashicorp/go-uuid" uuid "github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/connect/ca" "github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
) )
@ -1246,74 +1248,122 @@ func TestConnectCA_ConfigurationSet_PersistsRoots(t *testing.T) {
}) })
} }
func TestParseCARoot(t *testing.T) { func TestNewCARoot(t *testing.T) {
type test struct { type testCase struct {
name string name string
pem string pem string
wantSerial uint64 expected *structs.CARoot
wantSigningKeyID string expectedErr 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 run := func(t *testing.T, tc testCase) {
// for various key types. This does limit the exposure to formats that might root, err := newCARoot(tc.pem, "provider-name", "cluster-id")
// exist in external certificates which can be used as Connect CAs. if tc.expectedErr != "" {
// Specifically many other certs will have serial numbers that don't fit into testutil.RequireErrorContains(t, err, tc.expectedErr)
// 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{
{"no cert", "", 0, "", "", 0, true},
{
name: "default cert",
// Watchout for indentations they will break PEM format
pem: readTestData(t, "cert-with-ec-256-key.pem"),
// Based on `openssl x509 -noout -text` report from the cert
wantSerial: 8341954965092507701,
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",
wantKeyType: "ec",
wantKeyBits: 256,
wantErr: false,
},
{
name: "ec 384 cert",
// Watchout for indentations they will break PEM format
pem: readTestData(t, "cert-with-ec-384-key.pem"),
// Based on `openssl x509 -noout -text` report from the cert
wantSerial: 2935109425518279965,
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",
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 _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root, err := parseCARoot(tt.pem, "consul", "cluster")
if tt.wantErr {
require.Error(t, err)
return return
} }
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.wantSerial, root.SerialNumber) assert.DeepEqual(t, root, tc.expected)
require.Equal(t, strings.ToLower(tt.wantSigningKeyID), root.SigningKeyID) }
require.Equal(t, tt.wantKeyType, root.PrivateKeyType)
require.Equal(t, tt.wantKeyBits, root.PrivateKeyBits) // Test certs can be generated with
// go run connect/certgen/certgen.go -out-dir /tmp/connect-certs -key-type ec -key-bits 384
// serial generated with:
// openssl x509 -noout -text
testCases := []testCase{
{
name: "no cert",
expectedErr: "no PEM-encoded data found",
},
{
name: "type=ec bits=256",
pem: readTestData(t, "cert-with-ec-256-key.pem"),
expected: &structs.CARoot{
ID: "c9:1b:24:e0:89:63:1a:ba:22:01:f4:cf:bc:f1:c0:36:b2:6b:6c:3d",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 8341954965092507701,
SigningKeyID: "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",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 46, 29, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 46, 29, 0, time.UTC),
RootCert: readTestData(t, "cert-with-ec-256-key.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
{
name: "type=ec bits=384",
pem: readTestData(t, "cert-with-ec-384-key.pem"),
expected: &structs.CARoot{
ID: "29:69:c4:0f:aa:8f:bd:07:31:0d:51:3b:45:62:3d:c0:b2:fc:c6:3f",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 2935109425518279965,
SigningKeyID: "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",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 55, 18, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 55, 18, 0, time.UTC),
RootCert: readTestData(t, "cert-with-ec-384-key.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 384,
},
},
{
name: "type=rsa bits=4096",
pem: readTestData(t, "cert-with-rsa-4096-key.pem"),
expected: &structs.CARoot{
ID: "3a:6a:e3:e2:2d:44:85:5a:e9:44:3b:ef:d2:90:78:83:7f:61:a2:84",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 5186695743100577491,
SigningKeyID: "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",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 53, 15, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 53, 15, 0, time.UTC),
RootCert: readTestData(t, "cert-with-rsa-4096-key.pem"),
Active: true,
PrivateKeyType: "rsa",
PrivateKeyBits: 4096,
},
},
{
name: "two certs in pem",
pem: readTestData(t, "pem-with-two-certs.pem"),
expected: &structs.CARoot{
ID: "42:43:10:1f:71:6b:21:21:d1:10:49:d1:f0:41:78:8c:0a:77:ef:c0",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 17692800288680335732,
SigningKeyID: "9d:5c:27:43:ce:58:7b:ca:3e:7d:c4:fb:b6:2e:b7:13:e9:a1:68:3e",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2022, 1, 5, 23, 22, 12, 0, time.UTC),
NotAfter: time.Date(2022, 4, 7, 15, 22, 42, 0, time.UTC),
RootCert: readTestData(t, "pem-with-two-certs.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
{
name: "three certs in pem",
pem: readTestData(t, "pem-with-three-certs.pem"),
expected: &structs.CARoot{
ID: "42:43:10:1f:71:6b:21:21:d1:10:49:d1:f0:41:78:8c:0a:77:ef:c0",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 17692800288680335732,
SigningKeyID: "9d:5c:27:43:ce:58:7b:ca:3e:7d:c4:fb:b6:2e:b7:13:e9:a1:68:3e",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2022, 1, 5, 23, 22, 12, 0, time.UTC),
NotAfter: time.Date(2022, 4, 7, 15, 22, 42, 0, time.UTC),
RootCert: readTestData(t, "pem-with-three-certs.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
}) })
} }
} }

View File

@ -0,0 +1,48 @@
-----BEGIN CERTIFICATE-----
MIICUjCCATqgAwIBAgIUQjOIDzaM7bGW8bU69Yl0H0C4WXQwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQwNzE1MjI0MlowFTETMBEGA1UEAxMKcHJpbWFyeSBDQTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABEIcOmVSobge9pLDGh6rfyFg2+ilTFmo2ICv5vrgUfIZhi8O
fwYz5WGb7qBPRdMw9kP8BWH/lCrn2W3Ax3x2E+2jYzBhMA4GA1UdDwEB/wQEAwIB
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdXCdDzlh7yj59xPu2LrcT6aFo
PjAfBgNVHSMEGDAWgBS+x+IFMFb+hCJy1OQzcdzJuwDVhDANBgkqhkiG9w0BAQsF
AAOCAQEAWWBBoygbjUEtoueGuC+jHAlr+VOBwiQPJLQA+xtbCvWSn8yIx/M1RyhY
0/6WLMzhYA1lQAIze8CgKzqoGXXIcHif3PRZ3mRUMNdV/qGUv0oHZBzTKZVySOIm
MLIoq7WvyVdVNxyvRalhHxiQA1Hrh+zQKjXhVPM6dpG0duTNYit9kJCCeNDzRjWc
a/GgFyeeYMTheU3eBR6Vp2A8hy2h5xw82ul8YLwX0bCtcP12XAUzj3jFqwt6RLxW
Wc7rvsLfgimEfulQwo2WLPWZw8bJdnPvNcUFX8f2Zvqy0Jg6fELnxO+AdHnAnI9J
WtJr0ImA95Hw8gGTzmXOddYVGHuGLA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUDaEOI5nsEt9abNBbJibbQt+VZQIwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQxNTIzMjI0MlowFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAut/Gbr3MvypzEmRDTl7HGaSoVIydNEZNPqDD
jh1lqMFywB4DujTmkWLYcPJJ0RTT2NsSakteti/e1DHCuBSU0t3Q3K1paTh8aVLx
eK0IKNlCWqX5d1aYzCNZsRjJuQgPX6p/xcNGS+RS27jmRWPpvm6n1JfMvYRa7fF+
HnKhGNO+hDbhkQO4s0V1U+unNhshKDhTW3mBLmAEb2OHLOEaUZtYSbqr1E9tYXgU
DiYRkeWUpQXJ6pE91fmcaZFG0SxkqWnhe7GUa6wbb/vROWph4A1ZVHympBtOYwoJ
eibcJjBZLrugZdix8kl8NDI7SuIM/P0x0m9WkNfhJ9vSgQXlaQIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvsfiBTBW
/oQictTkM3HcybsA1YQwHwYDVR0jBBgwFoAUvsfiBTBW/oQictTkM3HcybsA1YQw
DQYJKoZIhvcNAQELBQADggEBALUJWitOV4xAvfNB8Z20AQ+/hdXkWVgj1VBbd++v
+X88q1TnueKAExU5o87MCjh9jMxalZqSVN9MUbQ4Xa+tmkjayizdpFaw6TbbaMIB
Tgqq5ATXMnOdZd46QC764Po9R9+k9hk4dNIr5gk1ifXZDMy/7jSOVARvpwzr0cTx
flRCTgZbcK10freoU7a74/YjEpG0wggGlR4aRWfm90Im9JM3aI55zAYQFzduf56c
HXJDLgBtbOx/ceqVrkPdvYwP9Q34tKAMiheQ0G3tTxP3Xc87gh4UEDV02oHhcbqw
WSm+8zTfGUlchowPRdqKE66urWTep+BA9c8zUqDdoq5lE9s=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICGTCCAZ+gAwIBAgIIJhC6ZZyZ/lQwCgYIKoZIzj0EAwMwFDESMBAGA1UEAxMJ
VGVzdCBDQSAxMB4XDTIyMDEwNTIzNDMyNVoXDTMyMDEwNTIzNDMyNVowFDESMBAG
A1UEAxMJVGVzdCBDQSAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETEyAhuLLOcxy
z2UHI7ePcB5AXL1o6mLwVfzyeaGfqUevzrFcLQ7WPiypZJW1KhOW5Q2bRgcjE8y3
fN+B8D+KT4fPtaRLtUVX6aZ0LCROFdgWjVo2DCvCq5VQnCGjW8r0o4G9MIG6MA4G
A1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MCkGA1UdDgQiBCBU/reewmUW
iduB8xxfW5clyUmrMewrWwtJuWPA/tFvTTArBgNVHSMEJDAigCBU/reewmUWiduB
8xxfW5clyUmrMewrWwtJuWPA/tFvTTA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMTEx
MTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49
BAMDA2gAMGUCMA4V/Iemelne4ZB+0glmxoKV6OPQ64oKkkrcy+vo1t1RZ+7jntRx
mxAnY3S2m35boQIxAOARpY+qfR3U3JM+vMW9KO0/KqM+y1/uvIaOA0bQex2w8bfN
V+QjFUDmjTT1dLpc7A==
-----END CERTIFICATE-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIICUjCCATqgAwIBAgIUQjOIDzaM7bGW8bU69Yl0H0C4WXQwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQwNzE1MjI0MlowFTETMBEGA1UEAxMKcHJpbWFyeSBDQTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABEIcOmVSobge9pLDGh6rfyFg2+ilTFmo2ICv5vrgUfIZhi8O
fwYz5WGb7qBPRdMw9kP8BWH/lCrn2W3Ax3x2E+2jYzBhMA4GA1UdDwEB/wQEAwIB
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdXCdDzlh7yj59xPu2LrcT6aFo
PjAfBgNVHSMEGDAWgBS+x+IFMFb+hCJy1OQzcdzJuwDVhDANBgkqhkiG9w0BAQsF
AAOCAQEAWWBBoygbjUEtoueGuC+jHAlr+VOBwiQPJLQA+xtbCvWSn8yIx/M1RyhY
0/6WLMzhYA1lQAIze8CgKzqoGXXIcHif3PRZ3mRUMNdV/qGUv0oHZBzTKZVySOIm
MLIoq7WvyVdVNxyvRalhHxiQA1Hrh+zQKjXhVPM6dpG0duTNYit9kJCCeNDzRjWc
a/GgFyeeYMTheU3eBR6Vp2A8hy2h5xw82ul8YLwX0bCtcP12XAUzj3jFqwt6RLxW
Wc7rvsLfgimEfulQwo2WLPWZw8bJdnPvNcUFX8f2Zvqy0Jg6fELnxO+AdHnAnI9J
WtJr0ImA95Hw8gGTzmXOddYVGHuGLA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUDaEOI5nsEt9abNBbJibbQt+VZQIwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQxNTIzMjI0MlowFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAut/Gbr3MvypzEmRDTl7HGaSoVIydNEZNPqDD
jh1lqMFywB4DujTmkWLYcPJJ0RTT2NsSakteti/e1DHCuBSU0t3Q3K1paTh8aVLx
eK0IKNlCWqX5d1aYzCNZsRjJuQgPX6p/xcNGS+RS27jmRWPpvm6n1JfMvYRa7fF+
HnKhGNO+hDbhkQO4s0V1U+unNhshKDhTW3mBLmAEb2OHLOEaUZtYSbqr1E9tYXgU
DiYRkeWUpQXJ6pE91fmcaZFG0SxkqWnhe7GUa6wbb/vROWph4A1ZVHympBtOYwoJ
eibcJjBZLrugZdix8kl8NDI7SuIM/P0x0m9WkNfhJ9vSgQXlaQIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvsfiBTBW
/oQictTkM3HcybsA1YQwHwYDVR0jBBgwFoAUvsfiBTBW/oQictTkM3HcybsA1YQw
DQYJKoZIhvcNAQELBQADggEBALUJWitOV4xAvfNB8Z20AQ+/hdXkWVgj1VBbd++v
+X88q1TnueKAExU5o87MCjh9jMxalZqSVN9MUbQ4Xa+tmkjayizdpFaw6TbbaMIB
Tgqq5ATXMnOdZd46QC764Po9R9+k9hk4dNIr5gk1ifXZDMy/7jSOVARvpwzr0cTx
flRCTgZbcK10freoU7a74/YjEpG0wggGlR4aRWfm90Im9JM3aI55zAYQFzduf56c
HXJDLgBtbOx/ceqVrkPdvYwP9Q34tKAMiheQ0G3tTxP3Xc87gh4UEDV02oHhcbqw
WSm+8zTfGUlchowPRdqKE66urWTep+BA9c8zUqDdoq5lE9s=
-----END CERTIFICATE-----

View File

@ -66,14 +66,15 @@ func (r IndexedCARoots) Active() *CARoot {
// CARoot represents a root CA certificate that is trusted. // CARoot represents a root CA certificate that is trusted.
type CARoot struct { type CARoot struct {
// ID is a globally unique ID (UUID) representing this CA root. // ID is a globally unique ID (UUID) representing this CA chain. It is
// calculated from the SHA1 of the primary CA certificate.
ID string ID string
// Name is a human-friendly name for this CA root. This value is // Name is a human-friendly name for this CA root. This value is
// opaque to Consul and is not used for anything internally. // opaque to Consul and is not used for anything internally.
Name string Name string
// SerialNumber is the x509 serial number of the certificate. // SerialNumber is the x509 serial number of the primary CA certificate.
SerialNumber uint64 SerialNumber uint64
// SigningKeyID is the connect.HexString encoded id of the public key that // SigningKeyID is the connect.HexString encoded id of the public key that
@ -96,8 +97,11 @@ type CARoot struct {
// future flexibility. // future flexibility.
ExternalTrustDomain string ExternalTrustDomain string
// Time validity bounds. // NotBefore is the x509.Certificate.NotBefore value of the primary CA
// certificate. This value should generally be a time in the past.
NotBefore time.Time NotBefore time.Time
// NotAfter is the x509.Certificate.NotAfter value of the primary CA
// certificate. This is the time when the certificate will expire.
NotAfter time.Time NotAfter time.Time
// RootCert is the PEM-encoded public certificate for the root CA. The // RootCert is the PEM-encoded public certificate for the root CA. The