Generate CSR using real trust-domain

This commit is contained in:
Paul Banks 2018-05-09 17:15:29 +01:00 committed by Mitchell Hashimoto
parent 5a1408f186
commit 30d90b3be4
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()
assert := assert.New(t)
require := require.New(t)
a := NewTestAgent(t.Name(), "connect { enabled = false }")
defer a.Shutdown()
req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.AgentConnectCARoots(resp, req)
assert.Nil(err)
require.NoError(err)
value := obj.(structs.IndexedCARoots)
assert.Equal(value.ActiveRootID, "")
@ -2122,6 +2123,7 @@ func TestAgentConnectCARoots_empty(t *testing.T) {
func TestAgentConnectCARoots_list(t *testing.T) {
t.Parallel()
assert := assert.New(t)
require := require.New(t)
a := NewTestAgent(t.Name(), "")
defer a.Shutdown()
@ -2137,30 +2139,34 @@ func TestAgentConnectCARoots_list(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/roots", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.AgentConnectCARoots(resp, req)
require.Nil(err)
require.NoError(err)
value := obj.(structs.IndexedCARoots)
require.Equal(value.ActiveRootID, ca2.ID)
require.Len(value.Roots, 2)
assert.Equal(value.ActiveRootID, ca2.ID)
// 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
for _, r := range value.Roots {
require.Equal("", r.SigningCert)
require.Equal("", r.SigningKey)
assert.Equal("", r.SigningCert)
assert.Equal("", r.SigningKey)
}
// 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
{
// List it again
obj2, err := a.srv.AgentConnectCARoots(httptest.NewRecorder(), req)
require.Nil(err)
require.Equal(obj, obj2)
require.NoError(err)
assert.Equal(obj, obj2)
// Should cache hit this time and not make request
require.Equal(cacheHits+1, a.cache.Hits())
assert.Equal(cacheHits+1, a.cache.Hits())
cacheHits++
}

View File

@ -1,6 +1,7 @@
package cachetype
import (
"errors"
"fmt"
"sync"
"sync/atomic"
@ -9,9 +10,7 @@ import (
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
// NOTE(mitcehllh): This is temporary while certs are stubbed out.
"github.com/mitchellh/go-testing-interface"
)
// Recommended name for registration.
@ -97,16 +96,41 @@ func (c *ConnectCALeaf) Fetch(opts cache.FetchOptions, req cache.Request) (cache
// by the above channel).
}
// Create a CSR.
// TODO(mitchellh): This is obviously not production ready! The host
// needs a correct host ID, and we probably don't want to use TestCSR
// 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",
// Need to lookup RootCAs response to discover trust domain. First just lookup
// with no blocking info - this should be a cache hit most of the time.
rawRoots, err := c.Cache.Get(ConnectCARootName, &structs.DCSpecificRequest{
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
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 {
return result, err
}
reply.PrivateKeyPEM = pk
reply.PrivateKeyPEM = pkPEM
// 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

View File

@ -25,10 +25,11 @@ func TestConnectCALeaf_changingRoots(t *testing.T) {
defer close(rootsCh)
rootsCh <- structs.IndexedCARoots{
ActiveRootID: "1",
TrustDomain: "fake-trust-domain.consul",
QueryMeta: structs.QueryMeta{Index: 1},
}
// Instrument ConnectCA.Sign to
// Instrument ConnectCA.Sign to return signed cert
var resp *structs.IssuedCert
var idx uint64
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
rootsCh <- structs.IndexedCARoots{
ActiveRootID: "2",
TrustDomain: "fake-trust-domain.consul",
QueryMeta: structs.QueryMeta{Index: 2},
}
select {
@ -101,6 +103,7 @@ func TestConnectCALeaf_expiringLeaf(t *testing.T) {
defer close(rootsCh)
rootsCh <- structs.IndexedCARoots{
ActiveRootID: "1",
TrustDomain: "fake-trust-domain.consul",
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
if conf.PrivateKey == "" {
pk, err := connect.GeneratePrivateKey()
_, pk, err := connect.GeneratePrivateKey()
if err != nil {
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})
if err != nil {
return "", fmt.Errorf("error encoding private key: %s", err)
return "", fmt.Errorf("error encoding certificate: %s", err)
}
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
pkSigner, pkPEM := testPrivateKey(t)
pkSigner, pkPEM, err := GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key: %s", err)
}
// Cert template for generation
template := x509.Certificate{

View File

@ -62,7 +62,8 @@ func (id *SpiffeIDSigning) CanSign(cu CertURI) bool {
}
// 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
// this to the `domain` config used for DNS because changing DNS domain can't

View File

@ -224,9 +224,17 @@ func (s *ConnectCA) Roots(
if err != nil {
return err
}
// Build TrustDomain based on the ClusterID stored.
signingID := connect.SpiffeIDSigningForCluster(config)
reply.TrustDomain = signingID.Host()
// Check CA is actually bootstrapped...
if config != nil {
// Build TrustDomain based on the ClusterID stored.
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()
}
}
return s.srv.blockingQuery(

View File

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

View File

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