diff --git a/agent/connect/testing.go b/agent/connect/testing_ca.go similarity index 95% rename from agent/connect/testing.go rename to agent/connect/testing_ca.go index 96b13dcf5..b6140bb04 100644 --- a/agent/connect/testing.go +++ b/agent/connect/testing_ca.go @@ -17,6 +17,7 @@ import ( "time" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/go-uuid" "github.com/mitchellh/go-testing-interface" ) @@ -32,14 +33,15 @@ const testClusterID = "11111111-2222-3333-4444-555555555555" 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. +// in the CARoot structure format. The returned CA will be set as Active = true. // // 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.ID = testUUID(t) + result.Active = true result.Name = fmt.Sprintf("Test CA %d", atomic.AddUint64(&testCACounter, 1)) // Create the private key we'll use for this CA cert. @@ -276,3 +278,13 @@ func testPrivateKey(t testing.T, ca *structs.CARoot) crypto.Signer { func testSerialNumber() (*big.Int, error) { return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) } + +// testUUID generates a UUID for testing. +func testUUID(t testing.T) string { + ret, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("Unable to generate a UUID, %s", err) + } + + return ret +} diff --git a/agent/connect/testing_test.go b/agent/connect/testing_ca_test.go similarity index 100% rename from agent/connect/testing_test.go rename to agent/connect/testing_ca_test.go diff --git a/agent/connect/testing_spiffe.go b/agent/connect/testing_spiffe.go new file mode 100644 index 000000000..e2e7a470f --- /dev/null +++ b/agent/connect/testing_spiffe.go @@ -0,0 +1,15 @@ +package connect + +import ( + "github.com/mitchellh/go-testing-interface" +) + +// TestSpiffeIDService returns a SPIFFE ID representing a service. +func TestSpiffeIDService(t testing.T, service string) *SpiffeIDService { + return &SpiffeIDService{ + Host: testClusterID + ".consul", + Namespace: "default", + Datacenter: "dc01", + Service: service, + } +} diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index f07fbd90f..2e26c4e2b 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -88,7 +88,12 @@ func (s *ConnectCA) Sign( return fmt.Errorf("SPIFFE ID in CSR must be a service ID") } - var root *structs.CARoot + // Get the currently active root + state := s.srv.fsm.State() + _, root, err := state.CARootActive(nil) + if err != nil { + return err + } // Determine the signing certificate. It is the set signing cert // unless that is empty, in which case it is identically to the public diff --git a/agent/consul/connect_ca_endpoint_test.go b/agent/consul/connect_ca_endpoint_test.go new file mode 100644 index 000000000..a08e31e04 --- /dev/null +++ b/agent/consul/connect_ca_endpoint_test.go @@ -0,0 +1,42 @@ +package consul + +import ( + "os" + "testing" + + "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/testrpc" + "github.com/hashicorp/net-rpc-msgpackrpc" + "github.com/stretchr/testify/assert" +) + +// Test CA signing +// +// NOTE(mitchellh): Just testing the happy path and not all the other validation +// issues because the internals of this method will probably be gutted for the +// CA plugins then we can just test mocks. +func TestConnectCASign(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Insert a CA + state := s1.fsm.State() + assert.Nil(state.CARootSet(1, connect.TestCA(t, nil))) + + // Generate a CSR and request signing + args := &structs.CASignRequest{ + Datacenter: "dc01", + CSR: connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")), + } + var reply interface{} + assert.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)) +} diff --git a/agent/consul/state/connect_ca.go b/agent/consul/state/connect_ca.go index 9e3195918..9c19b65c2 100644 --- a/agent/consul/state/connect_ca.go +++ b/agent/consul/state/connect_ca.go @@ -55,6 +55,24 @@ func (s *Store) CARoots(ws memdb.WatchSet) (uint64, structs.CARoots, error) { return idx, results, nil } +// CARootActive returns the currently active CARoot. +func (s *Store) CARootActive(ws memdb.WatchSet) (uint64, *structs.CARoot, error) { + // Get all the roots since there should never be that many and just + // do the filtering in this method. + var result *structs.CARoot + idx, roots, err := s.CARoots(ws) + if err == nil { + for _, r := range roots { + if r.Active { + result = r + break + } + } + } + + return idx, result, err +} + // CARootSet creates or updates a CA root. // // NOTE(mitchellh): I have a feeling we'll want a CARootMultiSetCAS to diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 992fce85a..045ebea90 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -33,6 +33,12 @@ type CARoot struct { SigningCert string SigningKey string + // Active is true if this is the current active CA. This must only + // be true for exactly one CA. For any method that modifies roots in the + // state store, tests should be written to verify that multiple roots + // cannot be active. + Active bool + RaftIndex }