Add flags to support CA generation for Connect (#9585)

This commit is contained in:
Hans Hasselberg 2021-01-27 08:52:15 +01:00 committed by GitHub
parent 38d630e2e9
commit 623aab5880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 130 additions and 96 deletions

3
.changelog/9585.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Add new `-cluster-id` and `common-name` to `consul tls ca create` to support creating a CA for Consul Connect.
```

View File

@ -2,9 +2,6 @@ package consul
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"fmt"
"net"
@ -46,22 +43,17 @@ const (
// testTLSCertificates Generates a TLS CA and server key/cert and returns them
// in PEM encoded form.
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
signer, _, err := tlsutil.GeneratePrivateKey()
if err != nil {
return "", "", "", err
}
// generate leaf
serial, err = tlsutil.GenerateSerialNumber()
ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
if err != nil {
return "", "", "", err
}
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}

View File

@ -1,9 +1,6 @@
package pool
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
@ -194,22 +191,18 @@ func deadlineNetPipe(deadline time.Time) (net.Conn, net.Conn, error) {
}
func generateTestCert(serverName string) (cert tls.Certificate, caPEM []byte, err error) {
// generate CA
serial, err := tlsutil.GenerateSerialNumber()
signer, _, err := tlsutil.GeneratePrivateKey()
if err != nil {
return tls.Certificate{}, nil, err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, nil, err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
if err != nil {
return tls.Certificate{}, nil, err
}
// generate leaf
serial, err = tlsutil.GenerateSerialNumber()
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return tls.Certificate{}, nil, err
}

View File

@ -1,9 +1,6 @@
package leakcheck
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"io/ioutil"
"path/filepath"
@ -18,22 +15,18 @@ import (
)
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{})
if err != nil {
return "", "", "", err
}
// generate leaf
serial, err = tlsutil.GenerateSerialNumber()
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, _, err := tlsutil.GeneratePrivateKey()
if err != nil {
return "", "", "", err
}

View File

@ -3,8 +3,6 @@ package agent
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"fmt"
"io"
@ -569,22 +567,17 @@ func TestACLConfigWithParams(params *TestACLConfigParams) string {
// testTLSCertificates Generates a TLS CA and server key/cert and returns them
// in PEM encoded form.
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.New(rand.NewSource(99)))
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
signer, _, err := tlsutil.GeneratePrivateKey()
if err != nil {
return "", "", "", err
}
// generate leaf
serial, err = tlsutil.GenerateSerialNumber()
ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
if err != nil {
return "", "", "", err
}
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}

View File

@ -23,7 +23,9 @@ type cmd struct {
help string
days int
domain string
clusterID string
constraint bool
commonName string
additionalConstraints flags.AppendSliceValue
}
@ -36,6 +38,8 @@ func (c *cmd) init() {
"DNS. If the UI is going to be served over HTTPS its DNS has to be added with -additional-constraint. It is not "+
"possible to add that after the fact! Defaults to false.")
c.flags.StringVar(&c.domain, "domain", "consul", "Domain of consul cluster. Only used in combination with -name-constraint. Defaults to consul.")
c.flags.StringVar(&c.clusterID, "cluster-id", "", "ClusterID of the consul cluster, requires -domain to be set as well. When used will set URIs with spiffeid.")
c.flags.StringVar(&c.commonName, "common-name", "", "Common Name of CA. Defaults to Consul Agent CA.")
c.flags.Var(&c.additionalConstraints, "additional-name-constraint", "Add name constraints for the CA. Results in rejecting certificates "+
"for other DNS than specified. Can be used multiple times. Only used in combination with -name-constraint.")
c.help = flags.Usage(help, c.flags)
@ -62,23 +66,12 @@ func (c *cmd) Run(args []string) int {
return 1
}
sn, err := tlsutil.GenerateSerialNumber()
if err != nil {
c.UI.Error(err.Error())
return 1
}
s, pk, err := tlsutil.GeneratePrivateKey()
if err != nil {
c.UI.Error(err.Error())
return 1
}
constraints := []string{}
if c.constraint {
constraints = append(c.additionalConstraints, []string{c.domain, "localhost"}...)
}
ca, err := tlsutil.GenerateCA(s, sn, c.days, constraints)
ca, pk, err := tlsutil.GenerateCA(tlsutil.CAOpts{Name: c.commonName, Days: c.days, Domain: c.domain, PermittedDNSDomains: constraints, ClusterID: c.clusterID})
if err != nil {
c.UI.Error(err.Error())
return 1

View File

@ -62,22 +62,53 @@ func TestCACreateCommand(t *testing.T) {
require.ElementsMatch(t, cert.PermittedDNSDomains, []string{"foo", "localhost", "bar"})
},
},
{"with cluster-id",
[]string{
"-domain=foo",
"-cluster-id=uuid",
},
"foo-agent-ca.pem",
"foo-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.Len(t, cert.URIs, 1)
require.Equal(t, cert.URIs[0].String(), "spiffe://uuid.foo")
},
},
{"with common-name",
[]string{
"-common-name=foo",
},
"consul-agent-ca.pem",
"consul-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.Equal(t, cert.Subject.CommonName, "foo")
},
},
{"without common-name",
[]string{},
"consul-agent-ca.pem",
"consul-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA"))
},
},
}
for _, tc := range cases {
tc := tc
require.True(t, t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
require.Equal(t, 0, cmd.Run(tc.args))
require.Equal(t, 0, cmd.Run(tc.args), ui.ErrorWriter.String())
require.Equal(t, "", ui.ErrorWriter.String())
cert, _ := expectFiles(t, tc.caPath, tc.keyPath)
require.Contains(t, cert.Subject.CommonName, "Consul Agent CA")
require.True(t, cert.BasicConstraintsValid)
require.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageDigitalSignature, cert.KeyUsage)
require.True(t, cert.IsCA)
require.Equal(t, cert.AuthorityKeyId, cert.SubjectKeyId)
tc.extraCheck(t, cert)
require.NoError(t, os.Remove(tc.caPath))
require.NoError(t, os.Remove(tc.keyPath))
}))
}

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/hashicorp/consul/agent/connect"
"net/url"
)
// GenerateSerialNumber returns random bigint generated with crypto/rand
@ -32,18 +33,61 @@ func GeneratePrivateKey() (crypto.Signer, string, error) {
return connect.GeneratePrivateKey()
}
type CAOpts struct {
Signer crypto.Signer
Serial *big.Int
ClusterID string
Days int
PermittedDNSDomains []string
Domain string
Name string
}
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
func GenerateCA(signer crypto.Signer, sn *big.Int, days int, constraints []string) (string, error) {
id, err := keyID(signer.Public())
if err != nil {
return "", err
func GenerateCA(opts CAOpts) (string, string, error) {
signer := opts.Signer
var pk string
if signer == nil {
var err error
signer, pk, err = GeneratePrivateKey()
if err != nil {
return "", "", err
}
}
name := fmt.Sprintf("Consul Agent CA %d", sn)
id, err := keyID(signer.Public())
if err != nil {
return "", "", err
}
sn := opts.Serial
if sn == nil {
var err error
sn, err = GenerateSerialNumber()
if err != nil {
return "", "", err
}
}
name := opts.Name
if name == "" {
name = fmt.Sprintf("Consul Agent CA %d", sn)
}
days := opts.Days
if opts.Days == 0 {
days = 365
}
var uris []*url.URL
if opts.ClusterID != "" {
spiffeID := connect.SpiffeIDSigning{ClusterID: opts.ClusterID, Domain: opts.Domain}
uris = []*url.URL{spiffeID.URI()}
}
// Create the CA cert
template := x509.Certificate{
SerialNumber: sn,
URIs: uris,
Subject: pkix.Name{
Country: []string{"US"},
PostalCode: []string{"94105"},
@ -62,23 +106,23 @@ func GenerateCA(signer crypto.Signer, sn *big.Int, days int, constraints []strin
SubjectKeyId: id,
}
if len(constraints) > 0 {
if len(opts.PermittedDNSDomains) > 0 {
template.PermittedDNSDomainsCritical = true
template.PermittedDNSDomains = constraints
template.PermittedDNSDomains = opts.PermittedDNSDomains
}
bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, signer.Public(), signer)
if err != nil {
return "", fmt.Errorf("error generating CA certificate: %s", err)
return "", "", fmt.Errorf("error generating CA certificate: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
return "", fmt.Errorf("error encoding private key: %s", err)
return "", "", fmt.Errorf("error encoding private key: %s", err)
}
return buf.String(), nil
return buf.String(), pk, nil
}
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)

View File

@ -8,13 +8,13 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
"strings"
)
func TestSerialNumber(t *testing.T) {
@ -62,32 +62,26 @@ func (s *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts)
func TestGenerateCA(t *testing.T) {
t.Parallel()
sn, err := GenerateSerialNumber()
require.Nil(t, err)
var s crypto.Signer
// test what happens without key
s = &TestSigner{}
ca, err := GenerateCA(s, sn, 0, nil)
ca, pk, err := GenerateCA(CAOpts{Signer: &TestSigner{}})
require.Error(t, err)
require.Empty(t, ca)
require.Empty(t, pk)
// test what happens with wrong key
s = &TestSigner{public: &rsa.PublicKey{}}
ca, err = GenerateCA(s, sn, 0, nil)
ca, pk, err = GenerateCA(CAOpts{Signer: &TestSigner{public: &rsa.PublicKey{}}})
require.Error(t, err)
require.Empty(t, ca)
require.Empty(t, pk)
// test what happens with correct key
s, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.Nil(t, err)
ca, err = GenerateCA(s, sn, 365, nil)
ca, pk, err = GenerateCA(CAOpts{})
require.Nil(t, err)
require.NotEmpty(t, ca)
require.NotEmpty(t, pk)
cert, err := parseCert(ca)
require.Nil(t, err)
require.Equal(t, fmt.Sprintf("Consul Agent CA %d", sn), cert.Subject.CommonName)
require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA"))
require.Equal(t, true, cert.IsCA)
require.Equal(t, true, cert.BasicConstraintsValid)
@ -99,14 +93,12 @@ func TestGenerateCA(t *testing.T) {
func TestGenerateCert(t *testing.T) {
t.Parallel()
sn, err := GenerateSerialNumber()
require.Nil(t, err)
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.Nil(t, err)
ca, err := GenerateCA(signer, sn, 365, nil)
ca, _, err := GenerateCA(CAOpts{Signer: signer})
require.Nil(t, err)
sn, err = GenerateSerialNumber()
sn, err := GenerateSerialNumber()
require.Nil(t, err)
DNSNames := []string{"server.dc1.consul"}
IPAddresses := []net.IP{net.ParseIP("123.234.243.213")}