Add endpoints that return intermediate certificates

This commit adds new endpoints that return the intermediate
certificates used in the CA.

Related to #1848
This commit is contained in:
Mariano Cano
2024-08-13 12:09:05 -07:00
parent 92e95e4df3
commit d3acbe9cbd
2 changed files with 146 additions and 0 deletions

View File

@@ -52,6 +52,7 @@ type Authority interface {
Revoke(context.Context, *authority.RevokeOptions) error
GetEncryptedKey(kid string) (string, error)
GetRoots() ([]*x509.Certificate, error)
GetIntermediateCertificates() []*x509.Certificate
GetFederation() ([]*x509.Certificate, error)
Version() authority.Version
GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error)
@@ -295,6 +296,11 @@ type RootsResponse struct {
Certificates []Certificate `json:"crts"`
}
// IntermediatesResponse is the response object of the intermediates request.
type IntermediatesResponse struct {
Certificates []Certificate `json:"crts"`
}
// FederationResponse is the response object of the federation request.
type FederationResponse struct {
Certificates []Certificate `json:"crts"`
@@ -330,7 +336,10 @@ func Route(r Router) {
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
r.MethodFunc("GET", "/roots", Roots)
r.MethodFunc("GET", "/roots.pem", RootsPEM)
r.MethodFunc("GET", "/intermediates", Intermediates)
r.MethodFunc("GET", "/intermediates.pem", IntermediatesPEM)
r.MethodFunc("GET", "/federation", Federation)
// SSH CA
r.MethodFunc("POST", "/ssh/sign", SSHSign)
r.MethodFunc("POST", "/ssh/renew", SSHRenew)
@@ -460,6 +469,47 @@ func RootsPEM(w http.ResponseWriter, r *http.Request) {
}
}
// Intermediates returns all the intermediate certificates of the CA.
func Intermediates(w http.ResponseWriter, r *http.Request) {
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
if len(intermediates) == 0 {
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
return
}
certs := make([]Certificate, len(intermediates))
for i := range intermediates {
certs[i] = Certificate{intermediates[i]}
}
render.JSONStatus(w, r, &RootsResponse{
Certificates: certs,
}, http.StatusCreated)
}
// RootsPEM returns all the root certificates for the CA in PEM format.
func IntermediatesPEM(w http.ResponseWriter, r *http.Request) {
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
if len(intermediates) == 0 {
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
for _, crt := range intermediates {
block := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
})
if _, err := w.Write(block); err != nil {
log.Error(w, r, err)
return
}
}
}
// Federation returns all the public certificates in the federation.
func Federation(w http.ResponseWriter, r *http.Request) {
federated, err := mustAuthority(r.Context()).GetFederation()

View File

@@ -31,6 +31,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
@@ -147,6 +148,13 @@ nIHOI54lAqDeF7A0y73fPRVCiJEWmuxz0g==
privKey = "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiNEhBYjE0WDQ5OFM4LWxSb29JTnpqZyJ9.RbkJXGzI3kOsaP20KmZs0ELFLgpRddAE49AJHlEblw-uH_gg6SV3QA.M3MArEpHgI171lhm.gBlFySpzK9F7riBJbtLSNkb4nAw_gWokqs1jS-ZK1qxuqTK-9mtX5yILjRnftx9P9uFp5xt7rvv4Mgom1Ed4V9WtIyfNP_Cz3Pme1Eanp5nY68WCe_yG6iSB1RJdMDBUb2qBDZiBdhJim1DRXsOfgedOrNi7GGbppMlD77DEpId118owR5izA-c6Q_hg08hIE3tnMAnebDNQoF9jfEY99_AReVRH8G4hgwZEPCfXMTb3J-lowKGG4vXIbK5knFLh47SgOqG4M2M51SMS-XJ7oBz1Vjoamc90QIqKV51rvZ5m0N_sPFtxzcfV4E9yYH3XVd4O-CG4ydVKfKVyMtQ.mcKFZqBHp_n7Ytj2jz9rvw"
)
func mustJSON(t *testing.T, v any) []byte {
t.Helper()
var buf bytes.Buffer
require.NoError(t, json.NewEncoder(&buf).Encode(v))
return buf.Bytes()
}
func parseCertificate(data string) *x509.Certificate {
block, _ := pem.Decode([]byte(data))
if block == nil {
@@ -199,6 +207,7 @@ type mockAuthority struct {
revoke func(context.Context, *authority.RevokeOptions) error
getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error)
getIntermediateCertificates func() []*x509.Certificate
getFederation func() ([]*x509.Certificate, error)
getCRL func() (*authority.CertificateRevocationListInfo, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
@@ -321,6 +330,13 @@ func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) {
return m.ret1.([]*x509.Certificate), m.err
}
func (m *mockAuthority) GetIntermediateCertificates() []*x509.Certificate {
if m.getIntermediateCertificates != nil {
return m.getIntermediateCertificates()
}
return m.ret1.([]*x509.Certificate)
}
func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
if m.getFederation != nil {
return m.getFederation()
@@ -1658,3 +1674,83 @@ func TestLogSSHCertificate(t *testing.T) {
assert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
assert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
}
func TestIntermediates(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
mockMustAuthority(t, &mockAuthority{
ret1: crt,
})
return httptest.NewRequest("GET", "/intermediates", http.NoBody)
}
type args struct {
crts []*x509.Certificate
}
tests := []struct {
name string
args args
wantStatusCode int
wantBody []byte
}{
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
Certificates: []Certificate{{ca.Intermediate}},
})},
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
Certificates: []Certificate{{ca.Root}, {ca.Intermediate}},
})},
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := getRequest(t, tt.args.crts)
Intermediates(w, r)
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
assert.Equal(t, tt.wantBody, w.Body.Bytes())
})
}
}
func TestIntermediatesPEM(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
mockMustAuthority(t, &mockAuthority{
ret1: crt,
})
return httptest.NewRequest("GET", "/intermediates.pem", http.NoBody)
}
type args struct {
crts []*x509.Certificate
}
tests := []struct {
name string
args args
wantStatusCode int
wantBody []byte
}{
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusOK, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
})},
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusOK, append(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Root.Raw,
}), pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
})...)},
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := getRequest(t, tt.args.crts)
IntermediatesPEM(w, r)
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
assert.Equal(t, tt.wantBody, w.Body.Bytes())
})
}
}