Skip to content

Commit

Permalink
certutil: add functional options
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 16, 2024
1 parent 6634efe commit cdacf83
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 25 deletions.
94 changes: 83 additions & 11 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 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)
Expand All @@ -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)
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

0 comments on commit cdacf83

Please sign in to comment.