diff --git a/testing/certutil/certutil.go b/testing/certutil/certutil.go index 5c8d695..ef1b40b 100644 --- a/testing/certutil/certutil.go +++ b/testing/certutil/certutil.go @@ -23,6 +23,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" @@ -39,81 +40,35 @@ type Pair struct { Key []byte } -// NewRootCA generates a new x509 Certificate and returns: +// NewRootCA generates a new x509 Certificate using ECDSA P-384 and returns: // - the private key // - the certificate -// - the certificate in PEM format as a byte slice. +// - 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() (*ecdsa.PrivateKey, *x509.Certificate, Pair, error) { +func NewRootCA() (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) } - notBefore, notAfter := makeNotBeforeAndAfter() - - rootTemplate := x509.Certificate{ - SerialNumber: big.NewInt(1653), - Subject: pkix.Name{ - Country: []string{"Gallifrey"}, - Locality: []string{"The Capitol"}, - OrganizationalUnit: []string{"Time Lords"}, - Organization: []string{"High Council of the Time Lords"}, - CommonName: "High Council", - }, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - IsCA: true, - } - - rootCertRawBytes, err := x509.CreateCertificate( - rand.Reader, &rootTemplate, &rootTemplate, &rootKey.PublicKey, rootKey) - if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not create CA: %w", err) - } - - rootPrivKeyDER, err := x509.MarshalECPrivateKey(rootKey) - if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err) - } - - // PEM private key - var rootPrivBytesOut []byte - rootPrivateKeyBuff := bytes.NewBuffer(rootPrivBytesOut) - err = pem.Encode(rootPrivateKeyBuff, &pem.Block{ - Type: "EC PRIVATE KEY", Bytes: rootPrivKeyDER}) - if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err) - } - - // PEM certificate - var rootCertBytesOut []byte - rootCertPemBuff := bytes.NewBuffer(rootCertBytesOut) - err = pem.Encode(rootCertPemBuff, &pem.Block{ - Type: "CERTIFICATE", Bytes: rootCertRawBytes}) - if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err) - } - - // tls.Certificate - rootTLSCert, err := tls.X509KeyPair( - rootCertPemBuff.Bytes(), rootPrivateKeyBuff.Bytes()) - if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not create key pair: %w", err) - } + _, cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey) + return rootKey, cert, pair, err +} - rootCACert, err := x509.ParseCertificate(rootTLSCert.Certificate[0]) +// NewRSARootCA generates a new x509 Certificate using RSA with a 2048-bit key 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 NewRSARootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) { + rootKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return nil, nil, Pair{}, fmt.Errorf("could not parse certificate: %w", err) + return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err) } - - return rootKey, rootCACert, Pair{ - Cert: rootCertPemBuff.Bytes(), - Key: rootPrivateKeyBuff.Bytes(), - }, nil + _, cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey) + return rootKey, cert, pair, err } // GenerateChildCert generates a x509 Certificate as a child of caCert and @@ -123,7 +78,13 @@ func NewRootCA() (*ecdsa.PrivateKey, *x509.Certificate, Pair, error) { // - the certificate and private key as a tls.Certificate // // 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, + priv crypto.PrivateKey, + pub crypto.PublicKey, + caPrivKey crypto.PrivateKey, + caCert *x509.Certificate) (*tls.Certificate, Pair, error) { notBefore, notAfter := makeNotBeforeAndAfter() @@ -143,18 +104,13 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, } - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return nil, Pair{}, fmt.Errorf("could not create private key: %w", err) - } - certRawBytes, err := x509.CreateCertificate( - rand.Reader, certTemplate, caCert, &privateKey.PublicKey, caPrivKey) + rand.Reader, certTemplate, caCert, pub, caPrivKey) if err != nil { return nil, Pair{}, fmt.Errorf("could not create CA: %w", err) } - privateKeyDER, err := x509.MarshalECPrivateKey(privateKey) + privateKeyDER, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err) } @@ -162,8 +118,8 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c // PEM private key var privBytesOut []byte privateKeyBuff := bytes.NewBuffer(privBytesOut) - err = pem.Encode(privateKeyBuff, &pem.Block{ - Type: "EC PRIVATE KEY", Bytes: privateKeyDER}) + err = pem.Encode(privateKeyBuff, + &pem.Block{Type: keyBlockType(priv), Bytes: privateKeyDER}) if err != nil { return nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err) } @@ -191,28 +147,151 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c }, nil } -// NewRootAndChildCerts returns a root CA and a child certificate and their keys -// for "localhost" and "127.0.0.1". +// NewRootAndChildCerts returns an ECDSA (P-384) root CA and a child certificate +// and their keys for "localhost" and "127.0.0.1". func NewRootAndChildCerts() (Pair, Pair, error) { rootKey, rootCACert, rootPair, err := NewRootCA() if err != nil { return Pair{}, Pair{}, fmt.Errorf("could not generate root CA: %w", err) } + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return Pair{}, Pair{}, fmt.Errorf("could not create private key: %w", err) + } + + childPair, err := defaultChildCert(rootKey, priv, &priv.PublicKey, rootCACert) + return rootPair, childPair, err +} + +// NewRSARootAndChildCerts returns an RSA (2048-bit) root CA and a child +// certificate and their keys for "localhost" and "127.0.0.1". +func NewRSARootAndChildCerts() (Pair, Pair, error) { + rootKey, rootCACert, rootPair, err := NewRSARootCA() + if err != nil { + return Pair{}, Pair{}, fmt.Errorf("could not generate RSA root CA: %w", err) + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return Pair{}, Pair{}, fmt.Errorf("could not create RSA private key: %w", err) + } + + childPair, err := defaultChildCert(rootKey, priv, &priv.PublicKey, rootCACert) + return rootPair, childPair, err +} + +// newRootCert creates a new self-signed root certificate using the provided +// private key and public key. +// It returns: +// - the private key, +// - the certificate, +// - 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) (any, *x509.Certificate, Pair, error) { + notBefore, notAfter := makeNotBeforeAndAfter() + + rootTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1653), + Subject: pkix.Name{ + Country: []string{"Gallifrey"}, + Locality: []string{"The Capitol"}, + OrganizationalUnit: []string{"Time Lords"}, + Organization: []string{"High Council of the Time Lords"}, + CommonName: "High Council", + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + rootCertRawBytes, err := x509.CreateCertificate( + rand.Reader, &rootTemplate, &rootTemplate, pub, priv) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not create CA: %w", err) + } + + rootPrivKeyDER, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err) + } + + // PEM private key + var rootPrivBytesOut []byte + rootPrivateKeyBuff := bytes.NewBuffer(rootPrivBytesOut) + err = pem.Encode(rootPrivateKeyBuff, + &pem.Block{Type: keyBlockType(priv), Bytes: rootPrivKeyDER}) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err) + } + + // PEM certificate + var rootCertBytesOut []byte + rootCertPemBuff := bytes.NewBuffer(rootCertBytesOut) + err = pem.Encode(rootCertPemBuff, + &pem.Block{Type: "CERTIFICATE", Bytes: rootCertRawBytes}) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err) + } + + // tls.Certificate + rootTLSCert, err := tls.X509KeyPair( + rootCertPemBuff.Bytes(), rootPrivateKeyBuff.Bytes()) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not create key pair: %w", err) + } + + rootCACert, err := x509.ParseCertificate(rootTLSCert.Certificate[0]) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not parse certificate: %w", err) + } + + return priv, rootCACert, Pair{ + Cert: rootCertPemBuff.Bytes(), + Key: rootPrivateKeyBuff.Bytes(), + }, nil +} + +// 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( + rootPriv, + priv crypto.PrivateKey, + pub crypto.PublicKey, + rootCACert *x509.Certificate) (Pair, error) { _, childPair, err := GenerateChildCert( "localhost", []net.IP{net.ParseIP("127.0.0.1")}, - rootKey, + priv, + pub, + rootPriv, rootCACert) if err != nil { - return Pair{}, Pair{}, fmt.Errorf( + return Pair{}, fmt.Errorf( "could not generate child TLS certificate CA: %w", err) } + return childPair, nil +} - return rootPair, childPair, nil +// keyBlockType returns the correct PEM block type for the given private key. +func keyBlockType(priv crypto.PrivateKey) string { + switch priv.(type) { + case *rsa.PrivateKey: + return "RSA PRIVATE KEY" + case *ecdsa.PrivateKey: + return "EC PRIVATE KEY" + default: + panic(fmt.Errorf("unsupported private key type: %T", priv)) + } } +// makeNotBeforeAndAfter returns: +// - notBefore: 1 minute before now +// - notAfter: 7 days after now func makeNotBeforeAndAfter() (time.Time, time.Time) { now := time.Now() notBefore := now.Add(-1 * time.Minute) diff --git a/testing/certutil/certutil_test.go b/testing/certutil/certutil_test.go new file mode 100644 index 0000000..1c53c73 --- /dev/null +++ b/testing/certutil/certutil_test.go @@ -0,0 +1,69 @@ +package certutil + +import ( + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestECCertificates(t *testing.T) { + ecRootPair, ecChildPair, err := NewRootAndChildCerts() + require.NoError(t, err, "could not create EC certificates") + + rsaRootPair, rsaChildPair, err := NewRSARootAndChildCerts() + require.NoError(t, err, "could not create EC certificates") + + tcs := []struct { + name string + rootPair Pair + childPair Pair + }{ + { + name: "EC keys", + rootPair: ecRootPair, + childPair: ecChildPair, + }, + { + name: "RSA keys", + rootPair: rsaRootPair, + childPair: rsaChildPair, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + rootBlock, _ := pem.Decode(tc.rootPair.Cert) + if rootBlock == nil { + panic("Failed to parse certificate PEM") + + } + root, err := x509.ParseCertificate(rootBlock.Bytes) + if err != nil { + panic("Failed to parse certificate: " + err.Error()) + } + + childBlock, _ := pem.Decode(tc.childPair.Cert) + if rootBlock == nil { + panic("Failed to parse certificate PEM") + + } + child, err := x509.ParseCertificate(childBlock.Bytes) + if err != nil { + panic("Failed to parse certificate: " + err.Error()) + } + + caCertPool := x509.NewCertPool() + caCertPool.AddCert(root) + + opts := x509.VerifyOptions{ + Roots: caCertPool, + } + + _, err = child.Verify(opts) + assert.NoError(t, err, "failed to verify child certificate") + }) + } +} diff --git a/testing/certutil/cmd/main.go b/testing/certutil/cmd/main.go index fb0ae64..7415689 100644 --- a/testing/certutil/cmd/main.go +++ b/testing/certutil/cmd/main.go @@ -19,7 +19,10 @@ package main import ( "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" @@ -36,12 +39,13 @@ import ( func main() { var caPath, caKeyPath, dest, name, ipList, filePrefix, pass string + var rsa 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.StringVar(&caKeyPath, "dest", "", - "Directory to save the generated files") + flag.BoolVar(&rsa, "rsa", false, + "") 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", @@ -76,21 +80,16 @@ func main() { netIPs = append(netIPs, net.ParseIP(ip)) } - var rootCert *x509.Certificate - var rootKey crypto.PrivateKey - if caPath == "" && caKeyPath == "" { - var pair certutil.Pair - rootKey, rootCert, pair, err = certutil.NewRootCA() - if err != nil { - panic(fmt.Errorf("could not create root CA certificate: %w", err)) - } + rootCert, rootKey, err := getCA(rsa, caPath, caKeyPath, dest, filePrefix) + priv, pub := generateKey(rsa) - savePair(dest, filePrefix+"ca", pair) - } else { - rootKey, rootCert = loadCA(caPath, caKeyPath) - } - - childCert, childPair, err := certutil.GenerateChildCert(name, netIPs, rootKey, rootCert) + childCert, childPair, err := certutil.GenerateChildCert( + name, + netIPs, + priv, + pub, + rootKey, + rootCert) if err != nil { panic(fmt.Errorf("error generating child certificate: %w", err)) } @@ -113,9 +112,13 @@ func main() { panic(fmt.Errorf("error getting ecdh.PrivateKey from the child's private key: %w", err)) } + blockType := "EC PRIVATE KEY" + if rsa { + 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, - "EC PRIVATE KEY", + blockType, key, []byte(pass), x509.PEMCipherAES128) @@ -131,6 +134,49 @@ func main() { } } +func generateKey(useRSA bool) (crypto.PrivateKey, crypto.PublicKey) { + if useRSA { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(fmt.Errorf("failed to generate RSA key: %v", err)) + } + + return priv, &priv.PublicKey + } + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + panic(fmt.Errorf("failed to generate EC key: %v", err)) + } + + return priv, &priv.PublicKey +} + +func getCA(rsa bool, caPath, caKeyPath, dest, filePrefix string) (*x509.Certificate, crypto.PrivateKey, error) { + var rootCert *x509.Certificate + var rootKey crypto.PrivateKey + var err error + + if caPath == "" && caKeyPath == "" { + caFn := certutil.NewRootCA + if rsa { + caFn = certutil.NewRSARootCA + } + + var pair certutil.Pair + rootKey, rootCert, pair, err = caFn() + if err != nil { + panic(fmt.Errorf("could not create root CA certificate: %w", err)) + } + + savePair(dest, filePrefix+"ca", pair) + } else { + rootKey, rootCert = loadCA(caPath, caKeyPath) + } + + return rootCert, rootKey, err +} + func loadCA(caPath string, keyPath string) (crypto.PrivateKey, *x509.Certificate) { caBytes, err := os.ReadFile(caPath) if err != nil {