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 30, 2024
1 parent 6c381fb commit fcae5b8
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 95 deletions.
235 changes: 157 additions & 78 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 @@ -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
Expand All @@ -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()

Expand All @@ -143,27 +104,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,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)
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")
})
}
}
Loading

0 comments on commit fcae5b8

Please sign in to comment.