Skip to content

Commit

Permalink
certutil: add functional options (elastic#240)
Browse files Browse the repository at this point in the history
add functional options to New*RootCA and Generate*ChildCert to allow setting a prefix for the CN and multiple DNS names
  • Loading branch information
AndersonQ committed Oct 23, 2024
1 parent 6634efe commit 2158076
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 56 deletions.
96 changes: 84 additions & 12 deletions testing/certutil/certutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,40 @@ type Pair struct {
Key []byte
}

type configs struct {
cnPrefix string
dnsNames []string
}

type Option func(opt *configs)

// 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() (crypto.PrivateKey, *x509.Certificate, Pair, error) {
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)
cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...)
return rootKey, cert, pair, err
}

Expand All @@ -62,12 +83,12 @@ func NewRootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) {
// - 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() (crypto.PrivateKey, *x509.Certificate, Pair, error) {
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)
cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...)
return rootKey, cert, pair, err
}

Expand All @@ -77,7 +98,36 @@ func NewRSARootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) {
// - 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) (*tls.Certificate, Pair, error) {
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)
Expand All @@ -90,10 +140,11 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c
priv,
&priv.PublicKey,
caPrivKey,
caCert)
caCert,
opts...)
if err != nil {
return nil, Pair{}, fmt.Errorf(
"could not generate child TLS certificate CA: %w", err)
"could not generate child TLS certificate: %w", err)
}

return cert, childPair, nil
Expand All @@ -115,18 +166,26 @@ func GenerateGenericChildCert(
priv crypto.PrivateKey,
pub crypto.PublicKey,
caPrivKey crypto.PrivateKey,
caCert *x509.Certificate) (*tls.Certificate, Pair, error) {
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([]string{name}, cfg.dnsNames...)
notBefore, notAfter := makeNotBeforeAndAfter()

certTemplate := &x509.Certificate{
DNSNames: []string{name},
DNSNames: dnsNames,
IPAddresses: ips,
SerialNumber: big.NewInt(1658),
Subject: pkix.Name{
Locality: []string{"anywhere in time and space"},
Organization: []string{"TARDIS"},
CommonName: "Police Public Call Box",
CommonName: cn,
},
NotBefore: notBefore,
NotAfter: notAfter,
Expand Down Expand Up @@ -220,7 +279,12 @@ func NewRSARootAndChildCerts() (Pair, Pair, error) {
// - 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) (*x509.Certificate, Pair, 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{
Expand All @@ -230,7 +294,7 @@ func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificat
Locality: []string{"The Capitol"},
OrganizationalUnit: []string{"Time Lords"},
Organization: []string{"High Council of the Time Lords"},
CommonName: "High Council",
CommonName: cn,
},
NotBefore: notBefore,
NotAfter: notAfter,
Expand Down Expand Up @@ -286,6 +350,14 @@ func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificat
}, nil
}

func getCgf(opts []Option) configs {
cfg := configs{dnsNames: []string{}}
for _, opt := range opts {
opt(&cfg)
}
return cfg
}

// 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(
Expand Down
30 changes: 16 additions & 14 deletions testing/certutil/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,20 @@ import (
)

func main() {
var caPath, caKeyPath, dest, name, ipList, filePrefix, pass string
var rsa bool
var caPath, caKeyPath, dest, name, ipList, prefix, pass string
var rsaflag bool
flag.StringVar(&caPath, "ca", "",
"File path for CA in PEM format")
flag.StringVar(&caKeyPath, "ca-key", "",
"File path for the CA key in PEM format")
flag.BoolVar(&rsa, "rsa", false,
flag.BoolVar(&rsaflag, "rsaflag", false,
"")
// TODO: accept multiple DNS names
flag.StringVar(&name, "name", "localhost",
"used as \"distinguished name\" and \"Subject Alternate Name values\" for the child certificate")
flag.StringVar(&ipList, "ips", "127.0.0.1",
"a comma separated list of IP addresses for the child certificate")
flag.StringVar(&filePrefix, "prefix", "current timestamp",
flag.StringVar(&prefix, "prefix", "current timestamp",
"a prefix to be added to the file name. If not provided a timestamp will be used")
flag.StringVar(&pass, "pass", "",
"a passphrase to encrypt the certificate key")
Expand All @@ -64,10 +65,10 @@ func main() {
caPath, caKeyPath)

}
if filePrefix == "" {
filePrefix = fmt.Sprintf("%d", time.Now().Unix())
if prefix == "current timestamp" {
prefix = fmt.Sprintf("%d", time.Now().Unix())
}
filePrefix += "-"
filePrefix := prefix + "-"

wd, err := os.Getwd()
if err != nil {
Expand All @@ -81,16 +82,17 @@ func main() {
netIPs = append(netIPs, net.ParseIP(ip))
}

rootCert, rootKey := getCA(rsa, caPath, caKeyPath, dest, filePrefix)
priv, pub := generateKey(rsa)
rootCert, rootKey := getCA(rsaflag, caPath, caKeyPath, dest, prefix)
priv, pub := generateKey(rsaflag)

childCert, childPair, err := certutil.GenerateGenericChildCert(
name,
netIPs,
priv,
pub,
rootKey,
rootCert)
rootCert,
certutil.WithCNPrefix(prefix))
if err != nil {
panic(fmt.Errorf("error generating child certificate: %w", err))
}
Expand All @@ -114,7 +116,7 @@ func main() {
}

blockType := "EC PRIVATE KEY"
if rsa {
if rsaflag {
blockType = "RSA PRIVATE KEY"
}
encPem, err := x509.EncryptPEMBlock( //nolint:staticcheck // we need to drop support for this, but while we don't, it needs to be tested.
Expand Down Expand Up @@ -153,7 +155,7 @@ func generateKey(useRSA bool) (crypto.PrivateKey, crypto.PublicKey) {
return priv, &priv.PublicKey
}

func getCA(rsa bool, caPath, caKeyPath, dest, filePrefix string) (*x509.Certificate, crypto.PrivateKey) {
func getCA(rsa bool, caPath, caKeyPath, dest, prefix string) (*x509.Certificate, crypto.PrivateKey) {
var rootCert *x509.Certificate
var rootKey crypto.PrivateKey
var err error
Expand All @@ -165,12 +167,12 @@ func getCA(rsa bool, caPath, caKeyPath, dest, filePrefix string) (*x509.Certific
}

var pair certutil.Pair
rootKey, rootCert, pair, err = caFn()
rootKey, rootCert, pair, err = caFn(certutil.WithCNPrefix(prefix))
if err != nil {
panic(fmt.Errorf("could not create root CA certificate: %w", err))
}

savePair(dest, filePrefix+"ca", pair)
savePair(dest, prefix+"-ca", pair)
} else {
rootKey, rootCert = loadCA(caPath, caKeyPath)
}
Expand Down
40 changes: 13 additions & 27 deletions transport/tlscommon/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (c *TLSConfig) ToConfig() *tls.Config {
Certificates: c.Certificates,
RootCAs: c.RootCAs,
ClientCAs: c.ClientCAs,
InsecureSkipVerify: insecure, //nolint: gosec // we are using our own verification for now
InsecureSkipVerify: insecure, //nolint:gosec // we are using our own verification for now
CipherSuites: convCipherSuites(c.CipherSuites),
CurvePreferences: c.CurvePreferences,
Renegotiation: c.Renegotiation,
Expand All @@ -123,13 +123,13 @@ func (c *TLSConfig) ToConfig() *tls.Config {
}
}

// BuildModuleConfig takes the TLSConfig and transform it into a `tls.Config`.
// BuildModuleClientConfig takes the TLSConfig and transform it into a `tls.Config`.
func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config {
if c == nil {
// use default TLS settings, if config is empty.
return &tls.Config{
ServerName: host,
InsecureSkipVerify: true, //nolint: gosec // we are using our own verification for now
InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now
VerifyConnection: makeVerifyConnection(&TLSConfig{
Verification: VerifyFull,
ServerName: host,
Expand All @@ -154,15 +154,16 @@ func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config {
return config
}

// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config` for server side objects.
// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config`
// for server side connections.
func (c *TLSConfig) BuildServerConfig(host string) *tls.Config {
if c == nil {
// use default TLS settings, if config is empty.
return &tls.Config{
ServerName: host,
InsecureSkipVerify: true, //nolint: gosec // we are using our own verification for now
InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now
VerifyConnection: makeVerifyServerConnection(&TLSConfig{
Verification: VerifyFull,
Verification: VerifyCertificate,
ServerName: host,
}),
}
Expand Down Expand Up @@ -285,27 +286,13 @@ func makeVerifyConnection(cfg *TLSConfig) func(tls.ConnectionState) error {

func makeVerifyServerConnection(cfg *TLSConfig) func(tls.ConnectionState) error {
switch cfg.Verification {
case VerifyFull:
return func(cs tls.ConnectionState) error {
if len(cs.PeerCertificates) == 0 {
if cfg.ClientAuth == tls.RequireAndVerifyClientCert {
return ErrMissingPeerCertificate
}
return nil
}

opts := x509.VerifyOptions{
Roots: cfg.ClientCAs,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
err := verifyCertsWithOpts(cs.PeerCertificates, cfg.CASha256, opts)
if err != nil {
return err
}
return verifyHostname(cs.PeerCertificates[0], cs.ServerName)
}
case VerifyCertificate:
// VerifyFull would attempt to match 'host' (c.ServerName) that is the host
// the client is trying to connect to with a DNS, IP or the CN from the
// client's certificate. Such validation, besides making no sense on the
// server side also causes errors as the client certificate usually does not
// contain a DNS, IP or CN matching the server's hostname.
case VerifyFull, VerifyCertificate:
return func(cs tls.ConnectionState) error {
if len(cs.PeerCertificates) == 0 {
if cfg.ClientAuth == tls.RequireAndVerifyClientCert {
Expand All @@ -331,7 +318,6 @@ func makeVerifyServerConnection(cfg *TLSConfig) func(tls.ConnectionState) error
}

return nil

}

func verifyCertsWithOpts(certs []*x509.Certificate, casha256 []string, opts x509.VerifyOptions) error {
Expand Down
6 changes: 3 additions & 3 deletions transport/tlscommon/tls_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ func TestMakeVerifyServerConnection(t *testing.T) {
expectedCallback: true,
expectedError: x509.CertificateInvalidError{Cert: testCerts["expired"], Reason: x509.Expired},
},
"default verification with certificates when required with incorrect server name in cert": {
"default verification with certificates when required do not verify hostname": {
verificationMode: VerifyFull,
clientAuth: tls.RequireAndVerifyClientCert,
certAuthorities: certPool,
peerCerts: []*x509.Certificate{testCerts["correct"]},
serverName: "bad.example.com",
serverName: "some.example.com",
expectedCallback: true,
expectedError: x509.HostnameError{Certificate: testCerts["correct"], Host: "bad.example.com"},
expectedError: nil,
},
"default verification with certificates when required with correct cert": {
verificationMode: VerifyFull,
Expand Down

0 comments on commit 2158076

Please sign in to comment.