Add certgen.TestCA for in-memory test certificate infrastructure
Provides a P-256 CA that issues leaf certificates for TLS testing with full verification enabled. No files written to disk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
213
certlib/certgen/testca.go
Normal file
213
certlib/certgen/testca.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCA is an in-memory certificate authority for use in tests. It
|
||||
// provides a root CA certificate and the ability to issue leaf
|
||||
// certificates for TLS testing with full verification enabled.
|
||||
type TestCA struct {
|
||||
cert *x509.Certificate
|
||||
key *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// NewTestCA creates a new TestCA with a self-signed P-256 root
|
||||
// certificate. The CA is valid for 1 hour.
|
||||
func NewTestCA() (*TestCA, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: generating CA key: %w", err)
|
||||
}
|
||||
|
||||
serial, err := SerialNumber()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: generating serial: %w", err)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Test CA",
|
||||
Organization: []string{"Test"},
|
||||
},
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(1 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 1,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: creating CA certificate: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: parsing CA certificate: %w", err)
|
||||
}
|
||||
|
||||
return &TestCA{cert: cert, key: key}, nil
|
||||
}
|
||||
|
||||
// Certificate returns the root CA certificate.
|
||||
func (ca *TestCA) Certificate() *x509.Certificate {
|
||||
return ca.cert
|
||||
}
|
||||
|
||||
// CertificatePEM returns the root CA certificate as a PEM-encoded
|
||||
// byte slice.
|
||||
func (ca *TestCA) CertificatePEM() []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: ca.cert.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
// CertPool returns a certificate pool containing the root CA
|
||||
// certificate, suitable for use as a TLS root CA pool.
|
||||
func (ca *TestCA) CertPool() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(ca.cert)
|
||||
return pool
|
||||
}
|
||||
|
||||
// Issue creates a new leaf certificate signed by the CA for the given
|
||||
// DNS names and IP addresses. It returns the leaf private key and
|
||||
// certificate. The leaf certificate is valid for 1 hour with key
|
||||
// usage appropriate for a TLS server.
|
||||
func (ca *TestCA) Issue(dnsNames []string, ips []net.IP) (crypto.Signer, *x509.Certificate, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: generating leaf key: %w", err)
|
||||
}
|
||||
|
||||
serial, err := SerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: generating serial: %w", err)
|
||||
}
|
||||
|
||||
cn := "localhost"
|
||||
if len(dnsNames) > 0 {
|
||||
cn = dnsNames[0]
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: []string{"Test"},
|
||||
},
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(1 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ips,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: creating leaf certificate: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: parsing leaf certificate: %w", err)
|
||||
}
|
||||
|
||||
return key, cert, nil
|
||||
}
|
||||
|
||||
// IssueServer is a convenience wrapper around Issue for the common
|
||||
// case of a server certificate for localhost (both DNS and IP).
|
||||
func (ca *TestCA) IssueServer() (crypto.Signer, *x509.Certificate, error) {
|
||||
return ca.Issue(
|
||||
[]string{"localhost"},
|
||||
[]net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
)
|
||||
}
|
||||
|
||||
// TLSConfig returns a tls.Config (by value) configured with the CA's
|
||||
// root pool for verification. The caller can set additional fields
|
||||
// (e.g., Certificates) or modify the returned config safely.
|
||||
func (ca *TestCA) TLSConfig() tls.Config {
|
||||
return tls.Config{
|
||||
RootCAs: ca.CertPool(),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
|
||||
// ServerTLSConfig returns a tls.Config (by value) for a TLS server
|
||||
// using the given leaf key and certificate, with client verification
|
||||
// against the CA root pool. Pass key and cert from Issue or
|
||||
// IssueServer.
|
||||
func (ca *TestCA) ServerTLSConfig(key crypto.Signer, cert *x509.Certificate) tls.Config {
|
||||
return tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
},
|
||||
},
|
||||
ClientCAs: ca.CertPool(),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
|
||||
// TLSKeyPair returns a tls.Certificate from the given key and
|
||||
// certificate, suitable for use in a tls.Config.Certificates slice.
|
||||
func TLSKeyPair(key crypto.Signer, cert *x509.Certificate) tls.Certificate {
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
}
|
||||
}
|
||||
|
||||
// MustTestCA calls NewTestCA and panics on error. Intended for use
|
||||
// in TestMain or test helpers where error handling is impractical.
|
||||
func MustTestCA() *TestCA {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
panic("certgen: " + err.Error())
|
||||
}
|
||||
return ca
|
||||
}
|
||||
|
||||
// CertificatePEM returns a PEM-encoded byte slice for the given
|
||||
// certificate.
|
||||
func CertificatePEM(cert *x509.Certificate) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
// PrivateKeyPEM returns a PEM-encoded PKCS#8 byte slice for the
|
||||
// given private key.
|
||||
func PrivateKeyPEM(key crypto.Signer) ([]byte, error) {
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: marshaling private key: %w", err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: der,
|
||||
}), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user