Skip to content

Commit

Permalink
testing/certutil: add support to RSA
Browse files Browse the repository at this point in the history
* add support to generate RSA certificates
* add `-rsa` cli to generate RSA certificates
  • Loading branch information
AndersonQ committed Sep 27, 2024
1 parent 6c381fb commit 95c13e9
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 39 deletions.
99 changes: 77 additions & 22 deletions testing/certutil/certutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
Expand All @@ -45,12 +46,26 @@ type Pair struct {
// - the certificate 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)
}

_, cert, pair, err := newRootCert(err, rootKey, &rootKey.PublicKey)
return rootKey, cert, pair, err
}

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 create private key: %w", err)
}
_, cert, pair, err := newRootCert(err, rootKey, &rootKey.PublicKey)
return rootKey, cert, pair, err
}

func newRootCert(err error, priv crypto.PrivateKey, pub crypto.PublicKey) (any, *x509.Certificate, Pair, error) {

Check failure on line 68 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (windows)

SA4009: argument err is overwritten before first use (staticcheck)

Check failure on line 68 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (linux)

SA4009: argument err is overwritten before first use (staticcheck)

Check failure on line 68 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

SA4009: argument err is overwritten before first use (staticcheck)
notBefore, notAfter := makeNotBeforeAndAfter()

rootTemplate := x509.Certificate{
Expand All @@ -70,30 +85,30 @@ func NewRootCA() (*ecdsa.PrivateKey, *x509.Certificate, Pair, error) {
}

rootCertRawBytes, err := x509.CreateCertificate(

Check failure on line 87 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (windows)

SA4009(related information): assignment to err (staticcheck)

Check failure on line 87 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (linux)

SA4009(related information): assignment to err (staticcheck)

Check failure on line 87 in testing/certutil/certutil.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

SA4009(related information): assignment to err (staticcheck)
rand.Reader, &rootTemplate, &rootTemplate, &rootKey.PublicKey, rootKey)
rand.Reader, &rootTemplate, &rootTemplate, pub, priv)
if err != nil {
return nil, nil, Pair{}, fmt.Errorf("could not create CA: %w", err)
}

rootPrivKeyDER, err := x509.MarshalECPrivateKey(rootKey)
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: "EC PRIVATE KEY", Bytes: rootPrivKeyDER})
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})
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)
}
Expand All @@ -110,7 +125,7 @@ func NewRootCA() (*ecdsa.PrivateKey, *x509.Certificate, Pair, error) {
return nil, nil, Pair{}, fmt.Errorf("could not parse certificate: %w", err)
}

return rootKey, rootCACert, Pair{
return priv, rootCACert, Pair{
Cert: rootCertPemBuff.Bytes(),
Key: rootPrivateKeyBuff.Bytes(),
}, nil
Expand All @@ -123,7 +138,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()

Expand All @@ -143,27 +164,22 @@ 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)
}

// 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)
}
Expand Down Expand Up @@ -191,6 +207,17 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c
}, nil
}

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))
}
}

// NewRootAndChildCerts returns a root CA and a child certificate and their keys
// for "localhost" and "127.0.0.1".
func NewRootAndChildCerts() (Pair, Pair, error) {
Expand All @@ -199,18 +226,46 @@ func NewRootAndChildCerts() (Pair, Pair, error) {
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 a RSA 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
}

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 rootPair, childPair, nil
return childPair, nil
}

func makeNotBeforeAndAfter() (time.Time, time.Time) {
Expand Down
69 changes: 69 additions & 0 deletions testing/certutil/certutil_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
}
80 changes: 63 additions & 17 deletions testing/certutil/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package main

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
Expand All @@ -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",
Expand Down Expand Up @@ -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)

Check failure on line 83 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (windows)

ineffectual assignment to err (ineffassign)

Check failure on line 83 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

ineffectual assignment to err (ineffassign)

Check failure on line 83 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

ineffectual assignment to err (ineffassign)
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))
}
Expand All @@ -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)
Expand All @@ -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))

Check failure on line 141 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (windows)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 141 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 141 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}

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))

Check failure on line 149 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (windows)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 149 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 149 in testing/certutil/cmd/main.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}

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 {
Expand Down

0 comments on commit 95c13e9

Please sign in to comment.