Generate CSR using real trust-domain

pull/4275/head
Paul Banks 2018-05-09 17:15:29 +01:00 committed by Mitchell Hashimoto
parent 622a475eb1
commit b4803eca59
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
10 changed files with 136 additions and 31 deletions

View File

@ -2106,13 +2106,14 @@ func TestAgentConnectCARoots_empty(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) assert := assert.New(t)
require := require.New(t)
a := NewTestAgent(t.Name(), "connect { enabled = false }") a := NewTestAgent(t.Name(), "connect { enabled = false }")
defer a.Shutdown() defer a.Shutdown()
req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil) req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.AgentConnectCARoots(resp, req) obj, err := a.srv.AgentConnectCARoots(resp, req)
assert.Nil(err) require.NoError(err)
value := obj.(structs.IndexedCARoots) value := obj.(structs.IndexedCARoots)
assert.Equal(value.ActiveRootID, "") assert.Equal(value.ActiveRootID, "")
@ -2122,6 +2123,7 @@ func TestAgentConnectCARoots_empty(t *testing.T) {
func TestAgentConnectCARoots_list(t *testing.T) { func TestAgentConnectCARoots_list(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t)
require := require.New(t) require := require.New(t)
a := NewTestAgent(t.Name(), "") a := NewTestAgent(t.Name(), "")
defer a.Shutdown() defer a.Shutdown()
@ -2137,30 +2139,34 @@ func TestAgentConnectCARoots_list(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil) req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.AgentConnectCARoots(resp, req) obj, err := a.srv.AgentConnectCARoots(resp, req)
require.Nil(err) require.NoError(err)
value := obj.(structs.IndexedCARoots) value := obj.(structs.IndexedCARoots)
require.Equal(value.ActiveRootID, ca2.ID) assert.Equal(value.ActiveRootID, ca2.ID)
require.Len(value.Roots, 2) // Would like to assert that it's the same as the TestAgent domain but the
// only way to access that state via this package is by RPC to the server
// implementation running in TestAgent which is more or less a tautology.
assert.NotEmpty(value.TrustDomain)
assert.Len(value.Roots, 2)
// We should never have the secret information // We should never have the secret information
for _, r := range value.Roots { for _, r := range value.Roots {
require.Equal("", r.SigningCert) assert.Equal("", r.SigningCert)
require.Equal("", r.SigningKey) assert.Equal("", r.SigningKey)
} }
// That should've been a cache miss, so no hit change // That should've been a cache miss, so no hit change
require.Equal(cacheHits, a.cache.Hits()) assert.Equal(cacheHits, a.cache.Hits())
// Test caching // Test caching
{ {
// List it again // List it again
obj2, err := a.srv.AgentConnectCARoots(httptest.NewRecorder(), req) obj2, err := a.srv.AgentConnectCARoots(httptest.NewRecorder(), req)
require.Nil(err) require.NoError(err)
require.Equal(obj, obj2) assert.Equal(obj, obj2)
// Should cache hit this time and not make request // Should cache hit this time and not make request
require.Equal(cacheHits+1, a.cache.Hits()) assert.Equal(cacheHits+1, a.cache.Hits())
cacheHits++ cacheHits++
} }

View File

@ -1,6 +1,7 @@
package cachetype package cachetype
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -9,9 +10,7 @@ import (
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
// NOTE(mitcehllh): This is temporary while certs are stubbed out. // NOTE(mitcehllh): This is temporary while certs are stubbed out.
"github.com/mitchellh/go-testing-interface"
) )
// Recommended name for registration. // Recommended name for registration.
@ -97,16 +96,41 @@ func (c *ConnectCALeaf) Fetch(opts cache.FetchOptions, req cache.Request) (cache
// by the above channel). // by the above channel).
} }
// Create a CSR. // Need to lookup RootCAs response to discover trust domain. First just lookup
// TODO(mitchellh): This is obviously not production ready! The host // with no blocking info - this should be a cache hit most of the time.
// needs a correct host ID, and we probably don't want to use TestCSR rawRoots, err := c.Cache.Get(ConnectCARootName, &structs.DCSpecificRequest{
// and want a non-test-specific way to create a CSR.
csr, pk := connect.TestCSR(&testing.RuntimeT{}, &connect.SpiffeIDService{
Host: "11111111-2222-3333-4444-555555555555.consul",
Namespace: "default",
Datacenter: reqReal.Datacenter, Datacenter: reqReal.Datacenter,
Service: reqReal.Service,
}) })
if err != nil {
return result, err
}
roots, ok := rawRoots.(*structs.IndexedCARoots)
if !ok {
return result, errors.New("invalid RootCA response type")
}
if roots.TrustDomain == "" {
return result, errors.New("cluster has no CA bootstrapped")
}
// Build the service ID
serviceID := &connect.SpiffeIDService{
Host: roots.TrustDomain,
Datacenter: reqReal.Datacenter,
Namespace: "default",
Service: reqReal.Service,
}
// Create a new private key
pk, pkPEM, err := connect.GeneratePrivateKey()
if err != nil {
return result, err
}
// Create a CSR.
csr, err := connect.CreateCSR(serviceID, pk)
if err != nil {
return result, err
}
// Request signing // Request signing
var reply structs.IssuedCert var reply structs.IssuedCert
@ -117,7 +141,7 @@ func (c *ConnectCALeaf) Fetch(opts cache.FetchOptions, req cache.Request) (cache
if err := c.RPC.RPC("ConnectCA.Sign", &args, &reply); err != nil { if err := c.RPC.RPC("ConnectCA.Sign", &args, &reply); err != nil {
return result, err return result, err
} }
reply.PrivateKeyPEM = pk reply.PrivateKeyPEM = pkPEM
// Lock the issued certs map so we can insert it. We only insert if // Lock the issued certs map so we can insert it. We only insert if
// we didn't happen to get a newer one. This should never happen since // we didn't happen to get a newer one. This should never happen since

View File

@ -25,10 +25,11 @@ func TestConnectCALeaf_changingRoots(t *testing.T) {
defer close(rootsCh) defer close(rootsCh)
rootsCh <- structs.IndexedCARoots{ rootsCh <- structs.IndexedCARoots{
ActiveRootID: "1", ActiveRootID: "1",
TrustDomain: "fake-trust-domain.consul",
QueryMeta: structs.QueryMeta{Index: 1}, QueryMeta: structs.QueryMeta{Index: 1},
} }
// Instrument ConnectCA.Sign to // Instrument ConnectCA.Sign to return signed cert
var resp *structs.IssuedCert var resp *structs.IssuedCert
var idx uint64 var idx uint64
rpc.On("RPC", "ConnectCA.Sign", mock.Anything, mock.Anything).Return(nil). rpc.On("RPC", "ConnectCA.Sign", mock.Anything, mock.Anything).Return(nil).
@ -67,6 +68,7 @@ func TestConnectCALeaf_changingRoots(t *testing.T) {
// Let's send in new roots, which should trigger the sign req // Let's send in new roots, which should trigger the sign req
rootsCh <- structs.IndexedCARoots{ rootsCh <- structs.IndexedCARoots{
ActiveRootID: "2", ActiveRootID: "2",
TrustDomain: "fake-trust-domain.consul",
QueryMeta: structs.QueryMeta{Index: 2}, QueryMeta: structs.QueryMeta{Index: 2},
} }
select { select {
@ -101,6 +103,7 @@ func TestConnectCALeaf_expiringLeaf(t *testing.T) {
defer close(rootsCh) defer close(rootsCh)
rootsCh <- structs.IndexedCARoots{ rootsCh <- structs.IndexedCARoots{
ActiveRootID: "1", ActiveRootID: "1",
TrustDomain: "fake-trust-domain.consul",
QueryMeta: structs.QueryMeta{Index: 1}, QueryMeta: structs.QueryMeta{Index: 1},
} }

View File

@ -79,7 +79,7 @@ func NewConsulProvider(rawConfig map[string]interface{}, delegate ConsulProvider
// Generate a private key if needed // Generate a private key if needed
if conf.PrivateKey == "" { if conf.PrivateKey == "" {
pk, err := connect.GeneratePrivateKey() _, pk, err := connect.GeneratePrivateKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -247,7 +247,7 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) {
} }
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil { if err != nil {
return "", fmt.Errorf("error encoding private key: %s", err) return "", fmt.Errorf("error encoding certificate: %s", err)
} }
err = c.incrementProviderIndex(providerState) err = c.incrementProviderIndex(providerState)

59
agent/connect/csr.go Normal file
View File

@ -0,0 +1,59 @@
package connect
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"net/url"
)
// CreateCSR returns a CSR to sign the given service along with the PEM-encoded
// private key for this certificate.
func CreateCSR(uri CertURI, privateKey crypto.Signer) (string, error) {
template := &x509.CertificateRequest{
URIs: []*url.URL{uri.URI()},
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
// Create the CSR itself
var csrBuf bytes.Buffer
bs, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
if err != nil {
return "", err
}
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
if err != nil {
return "", err
}
return csrBuf.String(), nil
}
// GeneratePrivateKey generates a new Private key
func GeneratePrivateKey() (crypto.Signer, string, error) {
var pk *ecdsa.PrivateKey
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, "", fmt.Errorf("error generating private key: %s", err)
}
bs, err := x509.MarshalECPrivateKey(pk)
if err != nil {
return nil, "", fmt.Errorf("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 {
return nil, "", fmt.Errorf("error encoding private key: %s", err)
}
return pk, buf.String(), nil
}

View File

@ -161,7 +161,10 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string
} }
// Generate fresh private key // Generate fresh private key
pkSigner, pkPEM := testPrivateKey(t) pkSigner, pkPEM, err := GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key: %s", err)
}
// Cert template for generation // Cert template for generation
template := x509.Certificate{ template := x509.Certificate{

View File

@ -62,7 +62,8 @@ func (id *SpiffeIDSigning) CanSign(cu CertURI) bool {
} }
// SpiffeIDSigningForCluster returns the SPIFFE signing identifier (trust // SpiffeIDSigningForCluster returns the SPIFFE signing identifier (trust
// domain) representation of the given CA config. // domain) representation of the given CA config. If config is nil this function
// will panic.
// //
// NOTE(banks): we intentionally fix the tld `.consul` for now rather than tie // NOTE(banks): we intentionally fix the tld `.consul` for now rather than tie
// this to the `domain` config used for DNS because changing DNS domain can't // this to the `domain` config used for DNS because changing DNS domain can't

View File

@ -224,10 +224,18 @@ func (s *ConnectCA) Roots(
if err != nil { if err != nil {
return err return err
} }
// Check CA is actually bootstrapped...
if config != nil {
// Build TrustDomain based on the ClusterID stored. // Build TrustDomain based on the ClusterID stored.
signingID := connect.SpiffeIDSigningForCluster(config) signingID := connect.SpiffeIDSigningForCluster(config)
if signingID == nil {
// If CA is bootstrapped at all then this should never happen but be
// defensive.
return errors.New("no cluster trust domain setup")
}
reply.TrustDomain = signingID.Host() reply.TrustDomain = signingID.Host()
} }
}
return s.srv.blockingQuery( return s.srv.blockingQuery(
&args.QueryOptions, &reply.QueryMeta, &args.QueryOptions, &reply.QueryMeta,

View File

@ -157,7 +157,7 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) {
// Update the provider config to use a new private key, which should // Update the provider config to use a new private key, which should
// cause a rotation. // cause a rotation.
newKey, err := connect.GeneratePrivateKey() _, newKey, err := connect.GeneratePrivateKey()
assert.NoError(err) assert.NoError(err)
newConfig := &structs.CAConfiguration{ newConfig := &structs.CAConfiguration{
Provider: "consul", Provider: "consul",

View File

@ -16,6 +16,8 @@ import (
"time" "time"
metrics "github.com/armon/go-metrics" metrics "github.com/armon/go-metrics"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
@ -23,7 +25,6 @@ import (
"github.com/hashicorp/consul/lib/freeport" "github.com/hashicorp/consul/lib/freeport"
"github.com/hashicorp/consul/logger" "github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/consul/testutil/retry"
uuid "github.com/hashicorp/go-uuid"
) )
func init() { func init() {