Skip to content

Commit

Permalink
certutil: certificates are valid for 30 days and cli improvments (#243)
Browse files Browse the repository at this point in the history
- the generated certificates are valid for 30 days instead of 7
 - add `-noip` flag to allow generating certificates without ips
 - restore the `-rsa` flag. It had been changed to `rsaflag` by mistake
 - add `-names` flag to allow setting multiple dns names
 - add `-client` flag to generate a certificate without any SAN/DNS or IP
  • Loading branch information
AndersonQ authored Oct 28, 2024
1 parent 4babafd commit 26a9ae3
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 50 deletions.
60 changes: 54 additions & 6 deletions testing/certutil/certutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
86 changes: 42 additions & 44 deletions testing/certutil/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"net"
Expand All @@ -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",
Expand All @@ -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)
Expand All @@ -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))
}
}
}

Expand Down

0 comments on commit 26a9ae3

Please sign in to comment.