| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package certhelpers |
| |
| import ( |
| "bytes" |
| "crypto" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/sha1" |
| "crypto/tls" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/pem" |
| "math/big" |
| "net" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| type CertBuilder struct { |
| tmpl *x509.Certificate |
| parentTmpl *x509.Certificate |
| |
| selfSign bool |
| parentKey *rsa.PrivateKey |
| |
| isCA bool |
| } |
| |
| type CertOpt func(*CertBuilder) error |
| |
| func CommonName(cn string) CertOpt { |
| return func(builder *CertBuilder) error { |
| builder.tmpl.Subject.CommonName = cn |
| return nil |
| } |
| } |
| |
| func Parent(parent Certificate) CertOpt { |
| return func(builder *CertBuilder) error { |
| builder.parentKey = parent.PrivKey.PrivKey |
| builder.parentTmpl = parent.Template |
| return nil |
| } |
| } |
| |
| func IsCA(isCA bool) CertOpt { |
| return func(builder *CertBuilder) error { |
| builder.isCA = isCA |
| return nil |
| } |
| } |
| |
| func SelfSign() CertOpt { |
| return func(builder *CertBuilder) error { |
| builder.selfSign = true |
| return nil |
| } |
| } |
| |
| func IP(ip ...string) CertOpt { |
| return func(builder *CertBuilder) error { |
| for _, addr := range ip { |
| if ipAddr := net.ParseIP(addr); ipAddr != nil { |
| builder.tmpl.IPAddresses = append(builder.tmpl.IPAddresses, ipAddr) |
| } |
| } |
| return nil |
| } |
| } |
| |
| func DNS(dns ...string) CertOpt { |
| return func(builder *CertBuilder) error { |
| builder.tmpl.DNSNames = dns |
| return nil |
| } |
| } |
| |
| func NewCert(t *testing.T, opts ...CertOpt) (cert Certificate) { |
| t.Helper() |
| |
| builder := CertBuilder{ |
| tmpl: &x509.Certificate{ |
| SerialNumber: makeSerial(t), |
| Subject: pkix.Name{ |
| CommonName: makeCommonName(), |
| }, |
| NotBefore: time.Now().Add(-1 * time.Hour), |
| NotAfter: time.Now().Add(1 * time.Hour), |
| IsCA: false, |
| KeyUsage: x509.KeyUsageDigitalSignature | |
| x509.KeyUsageKeyEncipherment | |
| x509.KeyUsageKeyAgreement, |
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, |
| BasicConstraintsValid: true, |
| }, |
| } |
| |
| for _, opt := range opts { |
| err := opt(&builder) |
| if err != nil { |
| t.Fatalf("Failed to set up certificate builder: %s", err) |
| } |
| } |
| |
| key := NewPrivateKey(t) |
| |
| builder.tmpl.SubjectKeyId = getSubjKeyID(t, key.PrivKey) |
| |
| tmpl := builder.tmpl |
| parent := builder.parentTmpl |
| publicKey := key.PrivKey.Public() |
| signingKey := builder.parentKey |
| |
| if builder.selfSign { |
| parent = tmpl |
| signingKey = key.PrivKey |
| } |
| |
| if builder.isCA { |
| tmpl.IsCA = true |
| tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign |
| tmpl.ExtKeyUsage = nil |
| } else { |
| tmpl.KeyUsage = x509.KeyUsageDigitalSignature | |
| x509.KeyUsageKeyEncipherment | |
| x509.KeyUsageKeyAgreement |
| tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} |
| } |
| |
| certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, parent, publicKey, signingKey) |
| if err != nil { |
| t.Fatalf("Unable to generate certificate: %s", err) |
| } |
| certPem := pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: certBytes, |
| }) |
| |
| tlsCert, err := tls.X509KeyPair(certPem, key.Pem) |
| if err != nil { |
| t.Fatalf("Unable to parse X509 key pair: %s", err) |
| } |
| |
| return Certificate{ |
| Template: tmpl, |
| PrivKey: key, |
| TLSCert: tlsCert, |
| RawCert: certBytes, |
| Pem: certPem, |
| IsCA: builder.isCA, |
| } |
| } |
| |
| // //////////////////////////////////////////////////////////////////////////// |
| // Private Key |
| // //////////////////////////////////////////////////////////////////////////// |
| type KeyWrapper struct { |
| PrivKey *rsa.PrivateKey |
| Pem []byte |
| } |
| |
| func NewPrivateKey(t *testing.T) (key KeyWrapper) { |
| t.Helper() |
| |
| privKey, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| t.Fatalf("Unable to generate key for cert: %s", err) |
| } |
| |
| privKeyPem := pem.EncodeToMemory( |
| &pem.Block{ |
| Type: "RSA PRIVATE KEY", |
| Bytes: x509.MarshalPKCS1PrivateKey(privKey), |
| }, |
| ) |
| |
| key = KeyWrapper{ |
| PrivKey: privKey, |
| Pem: privKeyPem, |
| } |
| |
| return key |
| } |
| |
| // //////////////////////////////////////////////////////////////////////////// |
| // Certificate |
| // //////////////////////////////////////////////////////////////////////////// |
| type Certificate struct { |
| PrivKey KeyWrapper |
| Template *x509.Certificate |
| TLSCert tls.Certificate |
| RawCert []byte |
| Pem []byte |
| IsCA bool |
| } |
| |
| func (cert Certificate) CombinedPEM() []byte { |
| if cert.IsCA { |
| return cert.Pem |
| } |
| return bytes.Join([][]byte{cert.PrivKey.Pem, cert.Pem}, []byte{'\n'}) |
| } |
| |
| func (cert Certificate) PrivateKeyPEM() []byte { |
| return cert.PrivKey.Pem |
| } |
| |
| // //////////////////////////////////////////////////////////////////////////// |
| // Helpers |
| // //////////////////////////////////////////////////////////////////////////// |
| func makeSerial(t *testing.T) *big.Int { |
| t.Helper() |
| |
| v := &big.Int{} |
| serialNumberLimit := v.Lsh(big.NewInt(1), 128) |
| serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
| if err != nil { |
| t.Fatalf("Unable to generate serial number: %s", err) |
| } |
| return serialNumber |
| } |
| |
| // Pulled from sdk/helper/certutil & slightly modified for test usage |
| func getSubjKeyID(t *testing.T, privateKey crypto.Signer) []byte { |
| t.Helper() |
| |
| if privateKey == nil { |
| t.Fatalf("passed-in private key is nil") |
| } |
| |
| marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) |
| if err != nil { |
| t.Fatalf("error marshalling public key: %s", err) |
| } |
| |
| subjKeyID := sha1.Sum(marshaledKey) |
| |
| return subjKeyID[:] |
| } |
| |
| func makeCommonName() (cn string) { |
| return strings.ReplaceAll(time.Now().Format("20060102T150405.000"), ".", "") |
| } |