testing/certutil/certutil.go (332 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package certutil
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
)
// Pair is a certificate and its private key in PEM format.
type Pair struct {
Cert []byte
Key []byte
}
type configs struct {
cnPrefix string
dnsNames []string
clientCert bool
}
type Option func(opt *configs)
// WithClientCert generates a client certificate, without any IP or SAN/DNS.
// It overrides any other IP or name set by other means.
func WithClientCert(clientCert bool) Option {
return func(opt *configs) {
opt.clientCert = clientCert
}
}
// WithCNPrefix adds cnPrefix as prefix for the CN.
func WithCNPrefix(cnPrefix string) Option {
return func(opt *configs) {
opt.cnPrefix = cnPrefix
}
}
// WithDNSNames adds dnsNames to the DNSNames.
func WithDNSNames(dnsNames ...string) Option {
return func(opt *configs) {
opt.dnsNames = dnsNames
}
}
// NewRootCA generates a new x509 Certificate using ECDSA P-384 and returns:
// - the private key
// - the certificate
// - the certificate and its key in PEM format as a byte slice.
//
// If any error occurs during the generation process, a non-nil error is returned.
func NewRootCA(opts ...Option) (crypto.PrivateKey, *x509.Certificate, Pair, error) {
rootKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err)
}
cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...)
return rootKey, cert, pair, err
}
// NewRSARootCA generates a new x509 Certificate using RSA with a 2048-bit key and returns:
// - the private key
// - the certificate
// - the certificate and its key in PEM format as a byte slice.
//
// If any error occurs during the generation process, a non-nil error is returned.
func NewRSARootCA(opts ...Option) (crypto.PrivateKey, *x509.Certificate, Pair, error) {
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err)
}
cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...)
return rootKey, cert, pair, err
}
// GenerateChildCert generates a ECDSA (P-384) x509 Certificate as a child of
// caCert and returns the following:
// - the certificate and private key as a tls.Certificate
// - a Pair with the certificate and its key im PEM format
//
// If any error occurs during the generation process, a non-nil error is returned.
func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate, opts ...Option) (*tls.Certificate, Pair, error) {
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create ECDSA private key: %w", err)
}
cert, childPair, err :=
GenerateGenericChildCert(
name,
ips,
priv,
&priv.PublicKey,
caPrivKey,
caCert,
opts...)
if err != nil {
return nil, Pair{}, fmt.Errorf(
"could not generate child TLS certificate CA: %w", err)
}
return cert, childPair, nil
}
// GenerateRSAChildCert generates a RSA with a 2048-bit key x509 Certificate as a
// child of caCert and returns the following:
// - the certificate and private key as a tls.Certificate
// - a Pair with the certificate and its key im PEM format
//
// If any error occurs during the generation process, a non-nil error is returned.
func GenerateRSAChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate, opts ...Option) (*tls.Certificate, Pair, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create RSA private key: %w", err)
}
cert, childPair, err :=
GenerateGenericChildCert(
name,
ips,
priv,
&priv.PublicKey,
caPrivKey,
caCert,
opts...)
if err != nil {
return nil, Pair{}, fmt.Errorf(
"could not generate child TLS certificate: %w", err)
}
return cert, childPair, nil
}
// GenerateGenericChildCert generates a x509 Certificate using priv and pub
// as the certificate's private and public keys and as a child of caCert.
// Use this function if you need fine control over keys or ips and certificate name,
// otherwise prefer GenerateChildCert or NewRootAndChildCerts/NewRSARootAndChildCerts
//
// It returns the following:
// - the certificate and private key as a tls.Certificate
// - a Pair with the certificate and its key im PEM format
//
// If any error occurs during the generation process, a non-nil error is returned.
func GenerateGenericChildCert(
name string,
ips []net.IP,
priv crypto.PrivateKey,
pub crypto.PublicKey,
caPrivKey crypto.PrivateKey,
caCert *x509.Certificate,
opts ...Option) (*tls.Certificate, Pair, error) {
cfg := getCgf(opts)
cn := "Police Public Call Box"
if cfg.cnPrefix != "" {
cn = fmt.Sprintf("[%s] %s", cfg.cnPrefix, cn)
}
dnsNames := append(cfg.dnsNames, name)
notBefore, notAfter := makeNotBeforeAndAfter()
certTemplate := &x509.Certificate{
DNSNames: dnsNames,
IPAddresses: ips,
SerialNumber: big.NewInt(1658),
Subject: pkix.Name{
Locality: []string{"anywhere in time and space"},
Organization: []string{"TARDIS"},
CommonName: cn,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature |
x509.KeyUsageKeyEncipherment |
x509.KeyUsageKeyAgreement,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
}
if cfg.clientCert {
certTemplate.IPAddresses = nil
certTemplate.DNSNames = nil
}
certRawBytes, err := x509.CreateCertificate(
rand.Reader, certTemplate, caCert, pub, caPrivKey)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create CA: %w", err)
}
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err)
}
// PEM private key
var privBytesOut []byte
privateKeyBuff := bytes.NewBuffer(privBytesOut)
err = pem.Encode(privateKeyBuff,
&pem.Block{Type: keyBlockType(priv), Bytes: privateKeyDER})
if err != nil {
return nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err)
}
privateKeyPemBytes := privateKeyBuff.Bytes()
// PEM certificate
var certBytesOut []byte
certBuff := bytes.NewBuffer(certBytesOut)
err = pem.Encode(certBuff, &pem.Block{
Type: "CERTIFICATE", Bytes: certRawBytes})
if err != nil {
return nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err)
}
certPemBytes := certBuff.Bytes()
// TLS Certificate
tlsCert, err := tls.X509KeyPair(certPemBytes, privateKeyPemBytes)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create key pair: %w", err)
}
return &tlsCert, Pair{
Cert: certPemBytes,
Key: privateKeyPemBytes,
}, nil
}
// NewRootAndChildCerts returns an ECDSA (P-384) root CA and a child certificate
// and their keys for "localhost" and "127.0.0.1".
func NewRootAndChildCerts() (Pair, Pair, error) {
rootKey, rootCACert, rootPair, err := NewRootCA()
if err != nil {
return Pair{}, Pair{}, fmt.Errorf("could not generate root CA: %w", err)
}
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return Pair{}, Pair{}, fmt.Errorf("could not create private key: %w", err)
}
childPair, err := defaultChildCert(rootKey, priv, &priv.PublicKey, rootCACert)
return rootPair, childPair, err
}
// NewRSARootAndChildCerts returns an RSA (2048-bit) root CA and a child
// certificate and their keys for "localhost" and "127.0.0.1".
func NewRSARootAndChildCerts() (Pair, Pair, error) {
rootKey, rootCACert, rootPair, err := NewRSARootCA()
if err != nil {
return Pair{}, Pair{}, fmt.Errorf("could not generate RSA root CA: %w", err)
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return Pair{}, Pair{}, fmt.Errorf("could not create RSA private key: %w", err)
}
childPair, err := defaultChildCert(rootKey, priv, &priv.PublicKey, rootCACert)
return rootPair, childPair, err
}
// EncryptKey accepts a *ecdsa.PrivateKey or *rsa.PrivateKey, it encrypts it
// and returns the encrypted key in PEM format.
func EncryptKey(key crypto.PrivateKey, passphrase string) ([]byte, error) {
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, fmt.Errorf("error converting private key to DER: %w", err)
}
var blockType string
switch key.(type) {
case *rsa.PrivateKey:
blockType = "RSA PRIVATE KEY"
case *ecdsa.PrivateKey:
blockType = "EC PRIVATE KEY"
default:
return nil, fmt.Errorf("unsupported private key type: %T", key)
}
encPem, err := x509.EncryptPEMBlock( //nolint:staticcheck // we need to drop support for this, but while we don't, it needs to be tested.
rand.Reader,
blockType,
keyDER,
[]byte(passphrase),
x509.PEMCipherAES128)
if err != nil {
return nil, fmt.Errorf("failed encrypting certificate key: %w", err)
}
certKeyEnc := pem.EncodeToMemory(encPem)
return certKeyEnc, nil
}
// newRootCert creates a new self-signed root certificate using the provided
// private key and public key.
// It returns:
// - the private key,
// - the certificate,
// - a Pair containing the certificate and private key in PEM format.
//
// If an error occurs during certificate creation, it returns a non-nil error.
func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey, opts ...Option) (*x509.Certificate, Pair, error) {
cn := "High Council"
cfg := getCgf(opts)
if cfg.cnPrefix != "" {
cn = fmt.Sprintf("[%s] %s", cfg.cnPrefix, cn)
}
notBefore, notAfter := makeNotBeforeAndAfter()
rootTemplate := x509.Certificate{
SerialNumber: big.NewInt(1653),
Subject: pkix.Name{
Country: []string{"Gallifrey"},
Locality: []string{"The Capitol"},
OrganizationalUnit: []string{"Time Lords"},
Organization: []string{"High Council of the Time Lords"},
CommonName: cn,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
SubjectKeyId: generateSubjectKeyID(pub),
}
rootCertRawBytes, err := x509.CreateCertificate(
rand.Reader, &rootTemplate, &rootTemplate, pub, priv)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create CA: %w", err)
}
rootPrivKeyDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err)
}
// PEM private key
var rootPrivBytesOut []byte
rootPrivateKeyBuff := bytes.NewBuffer(rootPrivBytesOut)
err = pem.Encode(rootPrivateKeyBuff,
&pem.Block{Type: keyBlockType(priv), Bytes: rootPrivKeyDER})
if err != nil {
return nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err)
}
// PEM certificate
var rootCertBytesOut []byte
rootCertPemBuff := bytes.NewBuffer(rootCertBytesOut)
err = pem.Encode(rootCertPemBuff,
&pem.Block{Type: "CERTIFICATE", Bytes: rootCertRawBytes})
if err != nil {
return nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err)
}
// tls.Certificate
rootTLSCert, err := tls.X509KeyPair(
rootCertPemBuff.Bytes(), rootPrivateKeyBuff.Bytes())
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create key pair: %w", err)
}
rootCACert, err := x509.ParseCertificate(rootTLSCert.Certificate[0])
if err != nil {
return nil, Pair{}, fmt.Errorf("could not parse certificate: %w", err)
}
return rootCACert, Pair{
Cert: rootCertPemBuff.Bytes(),
Key: rootPrivateKeyBuff.Bytes(),
}, nil
}
func getCgf(opts []Option) configs {
cfg := configs{dnsNames: []string{}}
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
func generateSubjectKeyID(pub crypto.PublicKey) []byte {
// SubjectKeyId generated using method 1 in RFC 7093, Section 2:
// 1) The keyIdentifier is composed of the leftmost 160-bits of the
// SHA-256 hash of the value of the BIT STRING subjectPublicKey
// (excluding the tag, length, and number of unused bits).
var publicKeyBytes []byte
switch publicKey := pub.(type) {
case *rsa.PublicKey:
publicKeyBytes = x509.MarshalPKCS1PublicKey(publicKey)
case *ecdsa.PublicKey:
//nolint:staticcheck // no alternative
publicKeyBytes = elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y)
}
h := sha256.Sum256(publicKeyBytes)
return h[:20]
}
// defaultChildCert generates a child certificate for localhost and 127.0.0.1.
// It returns the certificate and its key as a Pair and an error if any happens.
func defaultChildCert(
rootPriv,
priv crypto.PrivateKey,
pub crypto.PublicKey,
rootCACert *x509.Certificate) (Pair, error) {
_, childPair, err :=
GenerateGenericChildCert(
"localhost",
[]net.IP{net.ParseIP("127.0.0.1")},
priv,
pub,
rootPriv,
rootCACert)
if err != nil {
return Pair{}, fmt.Errorf(
"could not generate child TLS certificate CA: %w", err)
}
return childPair, nil
}
// keyBlockType returns the correct PEM block type for the given private key.
func keyBlockType(priv crypto.PrivateKey) string {
switch priv.(type) {
case *rsa.PrivateKey:
return "RSA PRIVATE KEY"
case *ecdsa.PrivateKey:
return "EC PRIVATE KEY"
default:
panic(fmt.Errorf("unsupported private key type: %T", priv))
}
}
// makeNotBeforeAndAfter returns:
// - notBefore: 1 minute before now
// - notAfter: 7 days after now
func makeNotBeforeAndAfter() (time.Time, time.Time) {
now := time.Now()
notBefore := now.Add(-1 * time.Minute)
notAfter := now.Add(30 * 24 * time.Hour)
return notBefore, notAfter
}