Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

certutil: add functional options #240

Merged
merged 2 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
belimawr marked this conversation as resolved.
Show resolved Hide resolved
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, Pair{}, fmt.Errorf("could not create RSA private key: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're calling ecdsa.GenerateKey, it looks like a typo in the error message:

Suggested change
return nil, Pair{}, fmt.Errorf("could not create RSA private key: %w", err)
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the 'CA' at the end of this error message? I believe "could not generate child TLS certificate: %w" would be enough.

Suggested change
"could not generate child TLS certificate CA: %w", err)
"could not generate child TLS certificate: %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
Loading