From 91aa43d9f8bc1b2eed87bc0d1522791d2d93fc25 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 24 Oct 2024 17:01:35 +0200 Subject: [PATCH] certutil: certificates are valid for 30 days and cli improvments - the generated certificates are valid for 30 days instead of 7 - add `-noip` flag to allow generating certificates without ips - restore the `-rsa` flag - add `-names` flag to allow setting multiple dns names - add `-client` flag to generate a certificate without any SAN/DNS or IP --- testing/certutil/certutil.go | 60 ++++++++++++++++++++++--- testing/certutil/cmd/main.go | 86 ++++++++++++++++++------------------ 2 files changed, 96 insertions(+), 50 deletions(-) diff --git a/testing/certutil/certutil.go b/testing/certutil/certutil.go index e45182d..030a5be 100644 --- a/testing/certutil/certutil.go +++ b/testing/certutil/certutil.go @@ -41,12 +41,21 @@ type Pair struct { } type configs struct { - cnPrefix string - dnsNames []string + 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) { @@ -175,9 +184,9 @@ func GenerateGenericChildCert( if cfg.cnPrefix != "" { cn = fmt.Sprintf("[%s] %s", cfg.cnPrefix, cn) } - dnsNames := append([]string{name}, cfg.dnsNames...) - notBefore, notAfter := makeNotBeforeAndAfter() + dnsNames := append(cfg.dnsNames, name) + notBefore, notAfter := makeNotBeforeAndAfter() certTemplate := &x509.Certificate{ DNSNames: dnsNames, IPAddresses: ips, @@ -189,11 +198,18 @@ func GenerateGenericChildCert( }, NotBefore: notBefore, NotAfter: notAfter, - KeyUsage: x509.KeyUsageDigitalSignature, + 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 { @@ -271,6 +287,38 @@ func NewRSARootAndChildCerts() (Pair, Pair, error) { 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: @@ -398,6 +446,6 @@ func keyBlockType(priv crypto.PrivateKey) string { func makeNotBeforeAndAfter() (time.Time, time.Time) { now := time.Now() notBefore := now.Add(-1 * time.Minute) - notAfter := now.Add(7 * 24 * time.Hour) + notAfter := now.Add(30 * 24 * time.Hour) return notBefore, notAfter } diff --git a/testing/certutil/cmd/main.go b/testing/certutil/cmd/main.go index c74d906..afef972 100644 --- a/testing/certutil/cmd/main.go +++ b/testing/certutil/cmd/main.go @@ -26,7 +26,6 @@ import ( "crypto/rsa" "crypto/tls" "crypto/x509" - "encoding/pem" "flag" "fmt" "net" @@ -39,17 +38,22 @@ import ( ) func main() { - var caPath, caKeyPath, dest, name, ipList, prefix, pass string - var rsaflag bool + var caPath, caKeyPath, dest, name, names, ipList, prefix, pass string + var client, rsaflag, noip 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(&rsaflag, "rsaflag", false, - "") - // TODO: accept multiple DNS names + flag.BoolVar(&rsaflag, "rsa", false, + "generate a RSA with a 2048-bit key certificate") + flag.BoolVar(&client, "client", false, + "generates a client certificate without any IP or SAN/DNS") flag.StringVar(&name, "name", "localhost", - "used as \"distinguished name\" and \"Subject Alternate Name values\" for the child certificate") + "a single \"Subject Alternate Name values\" for the child certificate. It's added to 'names' if set") + flag.StringVar(&names, "names", "", + "a comma separated list of \"Subject Alternate Name values\" for the child certificate") + flag.BoolVar(&noip, "noip", false, + "generate a certificate with no IP. It overrides -ips.") flag.StringVar(&ipList, "ips", "127.0.0.1", "a comma separated list of IP addresses for the child certificate") flag.StringVar(&prefix, "prefix", "current timestamp", @@ -76,10 +80,17 @@ func main() { } fmt.Println("files will be witten to:", wd) - ips := strings.Split(ipList, ",") var netIPs []net.IP - for _, ip := range ips { - netIPs = append(netIPs, net.ParseIP(ip)) + if !noip { + ips := strings.Split(ipList, ",") + for _, ip := range ips { + netIPs = append(netIPs, net.ParseIP(ip)) + } + } + + var dnsNames []string + if names != "" { + dnsNames = strings.Split(names, ",") } rootCert, rootKey := getCA(rsaflag, caPath, caKeyPath, dest, prefix) @@ -92,48 +103,35 @@ func main() { pub, rootKey, rootCert, - certutil.WithCNPrefix(prefix)) + certutil.WithCNPrefix(prefix), + certutil.WithDNSNames(dnsNames...), + certutil.WithClientCert(client)) if err != nil { panic(fmt.Errorf("error generating child certificate: %w", err)) } - savePair(dest, filePrefix+name, childPair) - - if pass == "" { - return - } - - fmt.Printf("passphrase present, encrypting \"%s\" certificate key\n", - name) - err = os.WriteFile(filePrefix+name+"-passphrase", []byte(pass), 0o600) - if err != nil { - panic(fmt.Errorf("error writing passphrase file: %w", err)) - } - - key, err := x509.MarshalPKCS8PrivateKey(childCert.PrivateKey) - if err != nil { - panic(fmt.Errorf("error getting ecdh.PrivateKey from the child's private key: %w", err)) + if client { + name = "client" } + savePair(dest, filePrefix+name, childPair) - blockType := "EC PRIVATE KEY" - 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. - rand.Reader, - blockType, - key, - []byte(pass), - x509.PEMCipherAES128) - if err != nil { - panic(fmt.Errorf("failed encrypting agent child certificate key block: %v", err)) - } + if pass != "" { + fmt.Printf("passphrase present, encrypting \"%s\" certificate key\n", + name) + err = os.WriteFile(filePrefix+name+"-passphrase", []byte(pass), 0o600) + if err != nil { + panic(fmt.Errorf("error writing passphrase file: %w", err)) + } - certKeyEnc := pem.EncodeToMemory(encPem) + certKeyEnc, err := certutil.EncryptKey(childCert.PrivateKey, pass) + if err != nil { + panic(err) + } - err = os.WriteFile(filepath.Join(dest, filePrefix+name+"_enc-key.pem"), certKeyEnc, 0o600) - if err != nil { - panic(fmt.Errorf("could not save %s certificate encrypted key: %w", filePrefix+name+"_enc-key.pem", err)) + err = os.WriteFile(filepath.Join(dest, filePrefix+name+"_enc-key.pem"), certKeyEnc, 0o600) + if err != nil { + panic(fmt.Errorf("could not save %s certificate encrypted key: %w", filePrefix+name+"_enc-key.pem", err)) + } } }