diff --git a/.changelog/8774.txt b/.changelog/8774.txt new file mode 100644 index 000000000..35f641a61 --- /dev/null +++ b/.changelog/8774.txt @@ -0,0 +1,4 @@ +```release-note:improvement +api: The `v1/connect/ca/roots` endpoint now accepts a `pem=true` query parameter and will return a PEM encoded certificate chain of + all the certificates that would normally be in the JSON version of the response. +``` \ No newline at end of file diff --git a/agent/connect_ca_endpoint.go b/agent/connect_ca_endpoint.go index 0b58ef72b..3075c8fa1 100644 --- a/agent/connect_ca_endpoint.go +++ b/agent/connect_ca_endpoint.go @@ -3,6 +3,7 @@ package agent import ( "fmt" "net/http" + "strconv" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/structs" @@ -15,13 +16,39 @@ func (s *HTTPHandlers) ConnectCARoots(resp http.ResponseWriter, req *http.Reques return nil, nil } + pemResponse := false + if pemParam := req.URL.Query().Get("pem"); pemParam != "" { + val, err := strconv.ParseBool(pemParam) + if err != nil { + return nil, BadRequestError{Reason: "The 'pem' query parameter must be a boolean value"} + } + pemResponse = val + } + var reply structs.IndexedCARoots defer setMeta(resp, &reply.QueryMeta) if err := s.agent.RPC("ConnectCA.Roots", &args, &reply); err != nil { return nil, err } - return reply, nil + if !pemResponse { + return reply, nil + } + + // defined in RFC 8555 and registered with the IANA + resp.Header().Set("Content-Type", "application/pem-certificate-chain") + for _, root := range reply.Roots { + if _, err := resp.Write([]byte(root.RootCert)); err != nil { + return nil, err + } + for _, intermediate := range root.IntermediateCerts { + if _, err := resp.Write([]byte(intermediate)); err != nil { + return nil, err + } + } + } + + return nil, nil } // /v1/connect/ca/configuration diff --git a/agent/connect_ca_endpoint_test.go b/agent/connect_ca_endpoint_test.go index adab05a0c..a06db3556 100644 --- a/agent/connect_ca_endpoint_test.go +++ b/agent/connect_ca_endpoint_test.go @@ -2,6 +2,8 @@ package agent import ( "bytes" + "crypto/x509" + "io/ioutil" "net/http" "net/http/httptest" "testing" @@ -258,3 +260,34 @@ func TestConnectCAConfig(t *testing.T) { }) } } + +func TestConnectCARoots_PEMEncoding(t *testing.T) { + primary := NewTestAgent(t, "") + defer primary.Shutdown() + testrpc.WaitForActiveCARoot(t, primary.RPC, "dc1", nil) + + secondary := NewTestAgent(t, ` + primary_datacenter = "dc1" + datacenter = "dc2" + retry_join_wan = ["`+primary.Config.SerfBindAddrWAN.String()+`"] + `) + defer secondary.Shutdown() + testrpc.WaitForActiveCARoot(t, secondary.RPC, "dc2", nil) + + req, _ := http.NewRequest("GET", "/v1/connect/ca/roots?pem=true", nil) + recorder := httptest.NewRecorder() + obj, err := secondary.srv.ConnectCARoots(recorder, req) + require.NoError(t, err) + require.Nil(t, obj, "Endpoint returned an object for serialization when it should have returned nil and written to the responses") + resp := recorder.Result() + require.Equal(t, resp.Header.Get("Content-Type"), "application/pem-certificate-chain") + + data, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + pool := x509.NewCertPool() + + require.True(t, pool.AppendCertsFromPEM(data)) + // expecting the root cert from dc1 and an intermediate in dc2 + require.Len(t, pool.Subjects(), 2) +} diff --git a/website/pages/api-docs/connect/ca.mdx b/website/pages/api-docs/connect/ca.mdx index 113a4f65d..02ba96fd6 100644 --- a/website/pages/api-docs/connect/ca.mdx +++ b/website/pages/api-docs/connect/ca.mdx @@ -31,6 +31,13 @@ The table below shows this endpoint's support for | ---------------- | ----------------- | ------------- | ------------ | | `YES` | `all` | `none` | `none` | +### Parameters + +- `pem` `(boolean: false)` - Specifies that the return body should be a PEM encoded + certificate chain suitable for use by applications needing to trust Connect CA + signed certificates. The Content-Type will be set to `application/pem-certificate-chain` + to indicate the format of the response. + ### Sample Request ```shell-session @@ -65,6 +72,39 @@ $ curl \ } ``` +### Sample PEM Encoded Response + +``` +-----BEGIN CERTIFICATE----- +MIICDzCCAbWgAwIBAgIBCDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktMWNq +OHphbW0uY29uc3VsLmNhLjA3OTMzYTEzLmNvbnN1bDAeFw0yMDEwMDgxOTQ4MzZa +Fw0zMDEwMDgxOTQ4MzZaMDExLzAtBgNVBAMTJnByaS0xY2o4emFtbS5jb25zdWwu +Y2EuMDc5MzNhMTMuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDCkT +IIxSDhA3XCKIuDcj4s9IVjf0NQT6QHPAzFBb964/4fTtX/J8x2n6A1lOXowFIWtx +GvAD/IJF74zn5ZA/wqOBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zApBgNVHQ4EIgQguPlAkrIkOnLr9+8DZ4afZWrYZUd2LB6nMJP72jDVxmcw +KwYDVR0jBCQwIoAguPlAkrIkOnLr9+8DZ4afZWrYZUd2LB6nMJP72jDVxmcwPwYD +VR0RBDgwNoY0c3BpZmZlOi8vMDc5MzNhMTMtYTYyYi1iZTkwLTQ0ZjEtZGVkOWE2 +NjczNzZlLmNvbnN1bDAKBggqhkjOPQQDAgNIADBFAiEA0ExkvLESG1I1TMFVronr +2fjoORukgzBgRMbWAEC2DJ0CIACsxeFS6tprHiRv4cEa2Md75h1iIisb2V2U7dvY +Z7Rr +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICEzCCAbigAwIBAgIBCTAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktMWNq +OHphbW0uY29uc3VsLmNhLjA3OTMzYTEzLmNvbnN1bDAeFw0yMDEwMDgxOTQ3Mzda +Fw0yMTEwMDgxOTQ3MzdaMDExLzAtBgNVBAMTJnNlYy0xbmIxMHZ0by5jb25zdWwu +Y2EuMDc5MzNhMTMuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9zWs +UxEYvLZUySoflz6e+HqLcaXM8heNRRkAiLiGkmn6nan6olnnrVBLyHAfHaHWJQ9W +wI8HwSZf0g4Ms16LWKOBwDCBvTAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgw +BgEB/wIBADApBgNVHQ4EIgQg+csK9Sg6odIfLLk3aiRY2OB4O0DiOa1XRTVdOVDE +t6QwKwYDVR0jBCQwIoAguPlAkrIkOnLr9+8DZ4afZWrYZUd2LB6nMJP72jDVxmcw +PwYDVR0RBDgwNoY0c3BpZmZlOi8vMDc5MzNhMTMtYTYyYi1iZTkwLTQ0ZjEtZGVk +OWE2NjczNzZlLmNvbnN1bDAKBggqhkjOPQQDAgNJADBGAiEAqJ60KJepAP4Xe4Ak +5UYB1huu/B8Lyz5yEYUpUplgdD4CIQCrrkoXoD4SGJ4HaIjy6a5eNf3YkhLpmbXO +6DL6FXVa1Q== +-----END CERTIFICATE----- +``` + ## Get CA Configuration This endpoint returns the current CA configuration.