Skip to content

Commit

Permalink
Allow mTLS for mysql secrets engine (#9181)
Browse files Browse the repository at this point in the history
* Extract certificate helpers for use in non-mongodb packages
* Created mTLS/X509 test for MySQL secrets engine.
* Ensure mysql username and passwords aren't url encoded
* Skip mTLS test for circleCI
  • Loading branch information
Lauren Voswinkel authored and andaley committed Jul 17, 2020
1 parent 18ffd9e commit fae19a8
Show file tree
Hide file tree
Showing 7 changed files with 826 additions and 46 deletions.
244 changes: 244 additions & 0 deletions helper/testhelpers/certhelpers/cert_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
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"), ".", "")
}
14 changes: 0 additions & 14 deletions plugins/database/mongodb/cert_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io/ioutil"
"math/big"
"os"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -192,18 +190,6 @@ func (cert certificate) CombinedPEM() []byte {
return bytes.Join([][]byte{cert.privKey.pem, cert.pem}, []byte{'\n'})
}

// ////////////////////////////////////////////////////////////////////////////
// Writing to file
// ////////////////////////////////////////////////////////////////////////////
func writeFile(t *testing.T, filename string, data []byte, perms os.FileMode) {
t.Helper()

err := ioutil.WriteFile(filename, data, perms)
if err != nil {
t.Fatalf("Unable to write to file [%s]: %s", filename, err)
}
}

// ////////////////////////////////////////////////////////////////////////////
// Helpers
// ////////////////////////////////////////////////////////////////////////////
Expand Down
46 changes: 30 additions & 16 deletions plugins/database/mongodb/connection_producer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"
"time"

"github.com/hashicorp/vault/helper/testhelpers/certhelpers"
"github.com/hashicorp/vault/helper/testhelpers/mongodb"
"github.com/ory/dockertest"
"go.mongodb.org/mongo-driver/mongo"
Expand All @@ -30,20 +31,20 @@ func TestInit_clientTLS(t *testing.T) {
defer os.RemoveAll(confDir)

// Create certificates for Mongo authentication
caCert := newCert(t,
commonName("test certificate authority"),
isCA(true),
selfSign(),
caCert := certhelpers.NewCert(t,
certhelpers.CommonName("test certificate authority"),
certhelpers.IsCA(true),
certhelpers.SelfSign(),
)
serverCert := newCert(t,
commonName("server"),
dns("localhost"),
parent(caCert),
serverCert := certhelpers.NewCert(t,
certhelpers.CommonName("server"),
certhelpers.DNS("localhost"),
certhelpers.Parent(caCert),
)
clientCert := newCert(t,
commonName("client"),
dns("client"),
parent(caCert),
clientCert := certhelpers.NewCert(t,
certhelpers.CommonName("client"),
certhelpers.DNS("client"),
certhelpers.Parent(caCert),
)

writeFile(t, paths.Join(confDir, "ca.pem"), caCert.CombinedPEM(), 0644)
Expand Down Expand Up @@ -81,7 +82,7 @@ net:
"connection_url": retURL,
"allowed_roles": "*",
"tls_certificate_key": clientCert.CombinedPEM(),
"tls_ca": caCert.pem,
"tls_ca": caCert.Pem,
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
Expand Down Expand Up @@ -111,7 +112,7 @@ net:
AuthInfo: authInfo{
AuthenticatedUsers: []user{
{
User: fmt.Sprintf("CN=%s", clientCert.template.Subject.CommonName),
User: fmt.Sprintf("CN=%s", clientCert.Template.Subject.CommonName),
DB: "$external",
},
},
Expand Down Expand Up @@ -249,11 +250,11 @@ func connect(t *testing.T, uri string) (client *mongo.Client) {
return client
}

func setUpX509User(t *testing.T, client *mongo.Client, cert certificate) {
func setUpX509User(t *testing.T, client *mongo.Client, cert certhelpers.Certificate) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

username := fmt.Sprintf("CN=%s", cert.template.Subject.CommonName)
username := fmt.Sprintf("CN=%s", cert.Template.Subject.CommonName)

cmd := &createUserCommand{
Username: username,
Expand Down Expand Up @@ -301,3 +302,16 @@ type roles []role
func (r roles) Len() int { return len(r) }
func (r roles) Less(i, j int) bool { return r[i].Role < r[j].Role }
func (r roles) Swap(i, j int) { r[i], r[j] = r[j], r[i] }

// ////////////////////////////////////////////////////////////////////////////
// Writing to file
// ////////////////////////////////////////////////////////////////////////////
func writeFile(t *testing.T, filename string, data []byte, perms os.FileMode) {
t.Helper()

err := ioutil.WriteFile(filename, data, perms)
if err != nil {
t.Fatalf("Unable to write to file [%s]: %s", filename, err)
}
}

Loading

0 comments on commit fae19a8

Please sign in to comment.