From 16569381887f8940007f8c3794b440e3d35a492c Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Wed, 16 Oct 2024 10:53:13 +0200 Subject: [PATCH] certutil: add functional options add functional options to New*RootCA and Generate*ChildCert to allow setting a prefix for the CN and multiple DNS names --- testing/certutil/certutil.go | 94 +++++++++++++++++++++++++++++++----- testing/certutil/cmd/main.go | 32 ++++++------ 2 files changed, 100 insertions(+), 26 deletions(-) diff --git a/testing/certutil/certutil.go b/testing/certutil/certutil.go index d8caec8..5377b4d 100644 --- a/testing/certutil/certutil.go +++ b/testing/certutil/certutil.go @@ -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 } @@ -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 } @@ -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 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 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) @@ -90,7 +140,8 @@ 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) @@ -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, @@ -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{ @@ -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, @@ -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( diff --git a/testing/certutil/cmd/main.go b/testing/certutil/cmd/main.go index bd34829..265e56f 100644 --- a/testing/certutil/cmd/main.go +++ b/testing/certutil/cmd/main.go @@ -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") @@ -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 { @@ -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, + nil, // netIPs, priv, pub, rootKey, - rootCert) + rootCert, + certutil.WithCNPrefix(prefix)) if err != nil { panic(fmt.Errorf("error generating child certificate: %w", err)) } @@ -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. @@ -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 @@ -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) }