open-consul/tlsutil/generate.go
R.B. Boyer c3d5a2a5ab
connect/ca: cease including the common name field in generated certs (#10424)
As part of this change, we ensure that the SAN extensions are marked as
critical when the subject is empty so that AWS PCA tolerates the loss of
common names well and continues to function as a Connect CA provider.

Parts of this currently hack around a bug in crypto/x509 and can be
removed after https://go-review.googlesource.com/c/go/+/329129 lands in
a Go release.

Note: the AWS PCA tests do not run automatically, but the following
passed locally for me:

    ENABLE_AWS_PCA_TESTS=1 go test ./agent/connect/ca -run TestAWS
2021-06-25 13:00:00 -05:00

257 lines
6.1 KiB
Go

package tlsutil
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
"net/url"
"github.com/hashicorp/consul/agent/connect"
)
// GenerateSerialNumber returns random bigint generated with crypto/rand
func GenerateSerialNumber() (*big.Int, error) {
l := new(big.Int).Lsh(big.NewInt(1), 128)
s, err := rand.Int(rand.Reader, l)
if err != nil {
return nil, err
}
return s, nil
}
// GeneratePrivateKey generates a new ecdsa private key
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
}
type CertOpts struct {
Signer crypto.Signer
CA string
Serial *big.Int
Name string
Days int
DNSNames []string
IPAddresses []net.IP
ExtKeyUsage []x509.ExtKeyUsage
}
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
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
}
}
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"},
Province: []string{"CA"},
Locality: []string{"San Francisco"},
StreetAddress: []string{"101 Second Street"},
Organization: []string{"HashiCorp Inc."},
CommonName: name,
},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().AddDate(0, 0, days),
NotBefore: time.Now(),
AuthorityKeyId: id,
SubjectKeyId: id,
}
if len(opts.PermittedDNSDomains) > 0 {
template.PermittedDNSDomainsCritical = true
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)
}
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 buf.String(), pk, nil
}
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)
func GenerateCert(opts CertOpts) (string, string, error) {
parent, err := parseCert(opts.CA)
if err != nil {
return "", "", err
}
signee, pk, err := GeneratePrivateKey()
if err != nil {
return "", "", err
}
id, err := keyID(signee.Public())
if err != nil {
return "", "", err
}
sn := opts.Serial
if sn == nil {
var err error
sn, err = GenerateSerialNumber()
if err != nil {
return "", "", err
}
}
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{CommonName: opts.Name},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: opts.ExtKeyUsage,
IsCA: false,
NotAfter: time.Now().AddDate(0, 0, opts.Days),
NotBefore: time.Now(),
SubjectKeyId: id,
DNSNames: opts.DNSNames,
IPAddresses: opts.IPAddresses,
}
bs, err := x509.CreateCertificate(rand.Reader, &template, parent, signee.Public(), opts.Signer)
if err != nil {
return "", "", 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 buf.String(), pk, nil
}
// KeyId returns a x509 KeyId from the given signing key.
func keyID(raw interface{}) ([]byte, error) {
switch raw.(type) {
case *ecdsa.PublicKey:
case *rsa.PublicKey:
default:
return nil, fmt.Errorf("invalid key type: %T", raw)
}
// This is not standard; RFC allows any unique identifier as long as they
// match in subject/authority chains but suggests specific hashing of DER
// bytes of public key including DER tags.
bs, err := x509.MarshalPKIXPublicKey(raw)
if err != nil {
return nil, err
}
// String formatted
kID := sha256.Sum256(bs)
return kID[:], nil
}
func parseCert(pemValue string) (*x509.Certificate, error) {
// The _ result below is not an error but the remaining PEM bytes.
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return nil, fmt.Errorf("no PEM-encoded data found")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
}
return x509.ParseCertificate(block.Bytes)
}
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key
// is expected to be the first block in the PEM value.
func ParseSigner(pemValue string) (crypto.Signer, error) {
return connect.ParseSigner(pemValue)
}
func Verify(caString, certString, dns string) error {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(caString))
if !ok {
return fmt.Errorf("failed to parse root certificate")
}
cert, err := parseCert(certString)
if err != nil {
return fmt.Errorf("failed to parse certificate")
}
opts := x509.VerifyOptions{
DNSName: fmt.Sprint(dns),
Roots: roots,
}
_, err = cert.Verify(opts)
return err
}