diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 97c52512a9..bc684f115c 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/ipaddr" @@ -21,6 +22,9 @@ import ( "github.com/hashicorp/serf/serf" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + + // NOTE(mitcehllh): This is temporary while certs are stubbed out. + "github.com/mitchellh/go-testing-interface" ) type Self struct { @@ -844,3 +848,40 @@ func (s *HTTPServer) AgentConnectCARoots(resp http.ResponseWriter, req *http.Req // behavior will differ. return s.ConnectCARoots(resp, req) } + +// AgentConnectCALeafCert returns the certificate bundle for a service +// instance. This supports blocking queries to update the returned bundle. +func (s *HTTPServer) AgentConnectCALeafCert(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Test the method + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + + // Get the service ID. Note that this is the ID of a service instance. + id := strings.TrimPrefix(req.URL.Path, "/v1/agent/connect/ca/leaf/") + + // Retrieve the service specified + service := s.agent.State.Service(id) + if service == nil { + return nil, fmt.Errorf("unknown service ID: %s", id) + } + + // Create a CSR. + // TODO(mitchellh): This is obviously not production ready! + csr, pk := connect.TestCSR(&testing.RuntimeT{}, &connect.SpiffeIDService{ + Host: "1234.consul", + Namespace: "default", + Datacenter: s.agent.config.Datacenter, + Service: service.Service, + }) + + // Request signing + var reply structs.IssuedCert + args := structs.CASignRequest{CSR: csr} + if err := s.agent.RPC("ConnectCA.Sign", &args, &reply); err != nil { + return nil, err + } + reply.PrivateKeyPEM = pk + + return &reply, nil +} diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 4c2f9f1d60..15267107ad 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -2,6 +2,7 @@ package agent import ( "bytes" + "crypto/x509" "fmt" "io" "io/ioutil" @@ -2074,3 +2075,58 @@ func TestAgentConnectCARoots_list(t *testing.T) { assert.Equal("", r.SigningKey) } } + +func TestAgentConnectCALeafCert_good(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Set CAs + var reply interface{} + ca1 := connect.TestCA(t, nil) + assert.Nil(a.RPC("Test.ConnectCASetRoots", []*structs.CARoot{ca1}, &reply)) + + { + // Register a local service + args := &structs.ServiceDefinition{ + ID: "foo", + Name: "test", + Address: "127.0.0.1", + Port: 8000, + Check: structs.CheckType{ + TTL: 15 * time.Second, + }, + } + req, _ := http.NewRequest("PUT", "/v1/agent/service/register", jsonReader(args)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentRegisterService(resp, req) + assert.Nil(err) + if !assert.Equal(200, resp.Code) { + t.Log("Body: ", resp.Body.String()) + } + } + + // List + req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/leaf/foo", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.AgentConnectCALeafCert(resp, req) + assert.Nil(err) + + // Get the issued cert + issued, ok := obj.(*structs.IssuedCert) + assert.True(ok) + + // Verify that the cert is signed by the CA + roots := x509.NewCertPool() + assert.True(roots.AppendCertsFromPEM([]byte(ca1.RootCert))) + leaf, err := connect.ParseCert(issued.CertPEM) + assert.Nil(err) + _, err = leaf.Verify(x509.VerifyOptions{ + Roots: roots, + }) + assert.Nil(err) + + // TODO(mitchellh): verify the private key matches the cert +} diff --git a/agent/connect/testing_ca.go b/agent/connect/testing_ca.go index b7f4368348..f79849016f 100644 --- a/agent/connect/testing_ca.go +++ b/agent/connect/testing_ca.go @@ -187,29 +187,47 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string { return buf.String() } -// TestCSR returns a CSR to sign the given service. -func TestCSR(t testing.T, id SpiffeID) string { +// TestCSR returns a CSR to sign the given service along with the PEM-encoded +// private key for this certificate. +func TestCSR(t testing.T, id SpiffeID) (string, string) { template := &x509.CertificateRequest{ URIs: []*url.URL{id.URI()}, SignatureAlgorithm: x509.ECDSAWithSHA256, } + // Result buffers + var csrBuf, pkBuf bytes.Buffer + // Create the private key we'll use signer := testPrivateKey(t, nil) - // Create the CSR itself - bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer) - if err != nil { - t.Fatalf("error creating CSR: %s", err) + { + // Create the private key PEM + bs, err := x509.MarshalECPrivateKey(signer.(*ecdsa.PrivateKey)) + if err != nil { + t.Fatalf("error marshalling PK: %s", err) + } + + err = pem.Encode(&pkBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) + if err != nil { + t.Fatalf("error encoding PK: %s", err) + } } - var buf bytes.Buffer - err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) - if err != nil { - t.Fatalf("error encoding CSR: %s", err) + { + // Create the CSR itself + bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer) + if err != nil { + t.Fatalf("error creating CSR: %s", err) + } + + err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) + if err != nil { + t.Fatalf("error encoding CSR: %s", err) + } } - return buf.String() + return csrBuf.String(), pkBuf.String() } // testKeyID returns a KeyID from the given public key. The "raw" must be diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index d6ddaef58b..1f732490b7 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -171,7 +171,7 @@ func (s *ConnectCA) Sign( // Set the response *reply = structs.IssuedCert{ SerialNumber: template.SerialNumber, - Cert: buf.String(), + CertPEM: buf.String(), } return nil diff --git a/agent/http_oss.go b/agent/http_oss.go index 3cb18b2e11..d2e86622f8 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -30,6 +30,7 @@ func init() { registerEndpoint("/v1/agent/check/fail/", []string{"PUT"}, (*HTTPServer).AgentCheckFail) registerEndpoint("/v1/agent/check/update/", []string{"PUT"}, (*HTTPServer).AgentCheckUpdate) registerEndpoint("/v1/agent/connect/ca/roots", []string{"GET"}, (*HTTPServer).AgentConnectCARoots) + registerEndpoint("/v1/agent/connect/ca/leaf/", []string{"GET"}, (*HTTPServer).AgentConnectCALeafCert) registerEndpoint("/v1/agent/service/register", []string{"PUT"}, (*HTTPServer).AgentRegisterService) registerEndpoint("/v1/agent/service/deregister/", []string{"PUT"}, (*HTTPServer).AgentDeregisterService) registerEndpoint("/v1/agent/service/maintenance/", []string{"PUT"}, (*HTTPServer).AgentServiceMaintenance) diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 0437b27cf9..6dc2dbf30e 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -2,6 +2,7 @@ package structs import ( "math/big" + "time" ) // IndexedCARoots is the list of currently trusted CA Roots. @@ -72,9 +73,23 @@ type IssuedCert struct { // SerialNumber is the unique serial number for this certificate. SerialNumber *big.Int - // Cert is the PEM-encoded certificate. This should not be stored in the + // CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private + // key for that cert, respectively. This should not be stored in the // state store, but is present in the sign API response. - Cert string `json:",omitempty"` + CertPEM string `json:",omitempty"` + PrivateKeyPEM string + + // Service is the name of the service for which the cert was issued. + // ServiceURI is the cert URI value. + Service string + ServiceURI string + + // ValidAfter and ValidBefore are the validity periods for the + // certificate. + ValidAfter time.Time + ValidBefore time.Time + + RaftIndex } // CAOp is the operation for a request related to intentions.