connect: create connect package for helpers

pull/4275/head
Mitchell Hashimoto 2018-03-19 10:48:38 -07:00
parent 6d294b6bb4
commit 7349c94c23
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
4 changed files with 390 additions and 0 deletions

48
connect/ca.go Normal file
View File

@ -0,0 +1,48 @@
package connect
import (
"crypto"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
)
// ParseCert parses the x509 certificate from a PEM-encoded value.
func ParseCert(pemValue string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return nil, fmt.Errorf("no PEM-encoded data found")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
}
return x509.ParseCertificate(block.Bytes)
}
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key
// is expected to be the first block in the PEM value.
func ParseSigner(pemValue string) (crypto.Signer, error) {
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return nil, fmt.Errorf("no PEM-encoded data found")
}
switch block.Type {
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type)
}
}
// SerialNumber generates a serial number suitable for a certificate.
//
// This function is taken directly from the Vault implementation.
func SerialNumber() (*big.Int, error) {
return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
}

3
connect/connect.go Normal file
View File

@ -0,0 +1,3 @@
// Package connect contains utilities and helpers for working with the
// Connect feature of Consul.
package connect

230
connect/testing.go Normal file
View File

@ -0,0 +1,230 @@
package connect
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net/url"
"sync/atomic"
"time"
"github.com/hashicorp/consul/agent/structs"
"github.com/mitchellh/go-testing-interface"
)
// testClusterID is the Consul cluster ID for testing.
//
// NOTE(mitchellh): This might have to change some other constant for
// real testing once we integrate the Cluster ID into the core. For now it
// is unchecked.
const testClusterID = "11111111-2222-3333-4444-555555555555"
// testCACounter is just an atomically incremented counter for creating
// unique names for the CA certs.
var testCACounter uint64 = 0
// TestCA creates a test CA certificate and signing key and returns it
// in the CARoot structure format. The CARoot returned will NOT have an ID
// set.
//
// If xc is non-nil, then the returned certificate will have a signing cert
// that is cross-signed with the previous cert, and this will be set as
// SigningCert.
func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot {
var result structs.CARoot
result.Name = fmt.Sprintf("Test CA %d", atomic.AddUint64(&testCACounter, 1))
// Create the private key we'll use for this CA cert.
signer := testPrivateKey(t, &result)
// The serial number for the cert
sn, err := SerialNumber()
if err != nil {
t.Fatalf("error generating serial number: %s", err)
}
// The URI (SPIFFE compatible) for the cert
uri, err := url.Parse(fmt.Sprintf("spiffe://%s.consul", testClusterID))
if err != nil {
t.Fatalf("error parsing CA URI: %s", err)
}
// Create the CA cert
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{CommonName: result.Name},
URIs: []*url.URL{uri},
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{uri.Hostname()},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
IsCA: true,
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now(),
AuthorityKeyId: testKeyID(t, signer.Public()),
SubjectKeyId: testKeyID(t, signer.Public()),
}
bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, signer.Public(), signer)
if err != nil {
t.Fatalf("error generating CA certificate: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
t.Fatalf("error encoding private key: %s", err)
}
result.RootCert = buf.String()
// If there is a prior CA to cross-sign with, then we need to create that
// and set it as the signing cert.
if xc != nil {
xccert, err := ParseCert(xc.RootCert)
if err != nil {
t.Fatalf("error parsing CA cert: %s", err)
}
xcsigner, err := ParseSigner(xc.SigningKey)
if err != nil {
t.Fatalf("error parsing signing key: %s", err)
}
// Set the authority key to be the previous one
template.AuthorityKeyId = testKeyID(t, xcsigner.Public())
// Create the new certificate where the parent is the previous
// CA, the public key is the new public key, and the signing private
// key is the old private key.
bs, err := x509.CreateCertificate(
rand.Reader, &template, xccert, signer.Public(), xcsigner)
if err != nil {
t.Fatalf("error generating CA certificate: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
t.Fatalf("error encoding private key: %s", err)
}
result.SigningCert = buf.String()
}
return &result
}
// TestLeaf returns a valid leaf certificate for the named service with
// the given CA Root.
func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
// Parse the CA cert and signing key from the root
caCert, err := ParseCert(root.RootCert)
if err != nil {
t.Fatalf("error parsing CA cert: %s", err)
}
signer, err := ParseSigner(root.SigningKey)
if err != nil {
t.Fatalf("error parsing signing key: %s", err)
}
// The serial number for the cert
sn, err := SerialNumber()
if err != nil {
t.Fatalf("error generating serial number: %s", err)
}
// Cert template for generation
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{CommonName: service},
SignatureAlgorithm: x509.ECDSAWithSHA256,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now(),
AuthorityKeyId: testKeyID(t, signer.Public()),
SubjectKeyId: testKeyID(t, signer.Public()),
}
// Create the certificate, PEM encode it and return that value.
var buf bytes.Buffer
bs, err := x509.CreateCertificate(
rand.Reader, &template, caCert, signer.Public(), signer)
if err != nil {
t.Fatalf("error generating certificate: %s", err)
}
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
t.Fatalf("error encoding private key: %s", err)
}
return buf.String()
}
// testKeyID returns a KeyID from the given public key. The "raw" must be
// an *ecdsa.PublicKey, but is an interface type to suppot crypto.Signer.Public
// values.
func testKeyID(t testing.T, raw interface{}) []byte {
pub, ok := raw.(*ecdsa.PublicKey)
if !ok {
t.Fatalf("raw is type %T, expected *ecdsa.PublicKey", raw)
}
// This is not standard; RFC allows any unique identifier as long as they
// match in subject/authority chains but suggests specific hashing of DER
// bytes of public key including DER tags. I can't be bothered to do esp.
// since ECDSA keys don't have a handy way to marshal the publick key alone.
h := sha256.New()
h.Write(pub.X.Bytes())
h.Write(pub.Y.Bytes())
return h.Sum([]byte{})
}
// testMemoizePK is the private key that we memoize once we generate it
// once so that our tests don't rely on too much system entropy.
var testMemoizePK atomic.Value
// testPrivateKey creates an ECDSA based private key.
func testPrivateKey(t testing.T, ca *structs.CARoot) crypto.Signer {
// If we already generated a private key, use that
var pk *ecdsa.PrivateKey
if v := testMemoizePK.Load(); v != nil {
pk = v.(*ecdsa.PrivateKey)
}
// If we have no key, then create a new one.
if pk == nil {
var err error
pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("error generating private key: %s", err)
}
}
bs, err := x509.MarshalECPrivateKey(pk)
if err != nil {
t.Fatalf("error generating private key: %s", err)
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs})
if err != nil {
t.Fatalf("error encoding private key: %s", err)
}
ca.SigningKey = buf.String()
// Memoize the key
testMemoizePK.Store(pk)
return pk
}

109
connect/testing_test.go Normal file
View File

@ -0,0 +1,109 @@
package connect
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
// hasOpenSSL is used to determine if the openssl CLI exists for unit tests.
var hasOpenSSL bool
func init() {
_, err := exec.LookPath("openssl")
hasOpenSSL = err == nil
}
// Test that the TestCA and TestLeaf functions generate valid certificates.
func TestTestCAAndLeaf(t *testing.T) {
if !hasOpenSSL {
t.Skip("openssl not found")
return
}
assert := assert.New(t)
// Create the certs
ca := TestCA(t, nil)
leaf := TestLeaf(t, "web", ca)
// Create a temporary directory for storing the certs
td, err := ioutil.TempDir("", "consul")
assert.Nil(err)
defer os.RemoveAll(td)
// Write the cert
assert.Nil(ioutil.WriteFile(filepath.Join(td, "ca.pem"), []byte(ca.RootCert), 0644))
assert.Nil(ioutil.WriteFile(filepath.Join(td, "leaf.pem"), []byte(leaf), 0644))
// Use OpenSSL to verify so we have an external, known-working process
// that can verify this outside of our own implementations.
cmd := exec.Command(
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf.pem")
cmd.Dir = td
output, err := cmd.Output()
t.Log(string(output))
assert.Nil(err)
}
// Test cross-signing.
func TestTestCAAndLeaf_xc(t *testing.T) {
if !hasOpenSSL {
t.Skip("openssl not found")
return
}
assert := assert.New(t)
// Create the certs
ca1 := TestCA(t, nil)
ca2 := TestCA(t, ca1)
leaf1 := TestLeaf(t, "web", ca1)
leaf2 := TestLeaf(t, "web", ca2)
// Create a temporary directory for storing the certs
td, err := ioutil.TempDir("", "consul")
assert.Nil(err)
defer os.RemoveAll(td)
// Write the cert
xcbundle := []byte(ca1.RootCert)
xcbundle = append(xcbundle, '\n')
xcbundle = append(xcbundle, []byte(ca2.SigningCert)...)
assert.Nil(ioutil.WriteFile(filepath.Join(td, "ca.pem"), xcbundle, 0644))
assert.Nil(ioutil.WriteFile(filepath.Join(td, "leaf1.pem"), []byte(leaf1), 0644))
assert.Nil(ioutil.WriteFile(filepath.Join(td, "leaf2.pem"), []byte(leaf2), 0644))
// OpenSSL verify the cross-signed leaf (leaf2)
{
cmd := exec.Command(
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf2.pem")
cmd.Dir = td
output, err := cmd.Output()
t.Log(string(output))
assert.Nil(err)
}
// OpenSSL verify the old leaf (leaf1)
{
cmd := exec.Command(
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf1.pem")
cmd.Dir = td
output, err := cmd.Output()
t.Log(string(output))
assert.Nil(err)
}
}
// Test that the private key is memoized to preseve system entropy.
func TestTestPrivateKey_memoize(t *testing.T) {
ca1 := TestCA(t, nil)
ca2 := TestCA(t, nil)
if ca1.SigningKey != ca2.SigningKey {
t.Fatal("should have the same signing keys for tests")
}
}