Skip to content

Commit d3acbe9

Browse files
committed
Add endpoints that return intermediate certificates
This commit adds new endpoints that return the intermediate certificates used in the CA. Related to smallstep#1848
1 parent 92e95e4 commit d3acbe9

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

api/api.go

+50
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Authority interface {
5252
Revoke(context.Context, *authority.RevokeOptions) error
5353
GetEncryptedKey(kid string) (string, error)
5454
GetRoots() ([]*x509.Certificate, error)
55+
GetIntermediateCertificates() []*x509.Certificate
5556
GetFederation() ([]*x509.Certificate, error)
5657
Version() authority.Version
5758
GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error)
@@ -295,6 +296,11 @@ type RootsResponse struct {
295296
Certificates []Certificate `json:"crts"`
296297
}
297298

299+
// IntermediatesResponse is the response object of the intermediates request.
300+
type IntermediatesResponse struct {
301+
Certificates []Certificate `json:"crts"`
302+
}
303+
298304
// FederationResponse is the response object of the federation request.
299305
type FederationResponse struct {
300306
Certificates []Certificate `json:"crts"`
@@ -330,7 +336,10 @@ func Route(r Router) {
330336
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
331337
r.MethodFunc("GET", "/roots", Roots)
332338
r.MethodFunc("GET", "/roots.pem", RootsPEM)
339+
r.MethodFunc("GET", "/intermediates", Intermediates)
340+
r.MethodFunc("GET", "/intermediates.pem", IntermediatesPEM)
333341
r.MethodFunc("GET", "/federation", Federation)
342+
334343
// SSH CA
335344
r.MethodFunc("POST", "/ssh/sign", SSHSign)
336345
r.MethodFunc("POST", "/ssh/renew", SSHRenew)
@@ -460,6 +469,47 @@ func RootsPEM(w http.ResponseWriter, r *http.Request) {
460469
}
461470
}
462471

472+
// Intermediates returns all the intermediate certificates of the CA.
473+
func Intermediates(w http.ResponseWriter, r *http.Request) {
474+
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
475+
if len(intermediates) == 0 {
476+
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
477+
return
478+
}
479+
480+
certs := make([]Certificate, len(intermediates))
481+
for i := range intermediates {
482+
certs[i] = Certificate{intermediates[i]}
483+
}
484+
485+
render.JSONStatus(w, r, &RootsResponse{
486+
Certificates: certs,
487+
}, http.StatusCreated)
488+
}
489+
490+
// RootsPEM returns all the root certificates for the CA in PEM format.
491+
func IntermediatesPEM(w http.ResponseWriter, r *http.Request) {
492+
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
493+
if len(intermediates) == 0 {
494+
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
495+
return
496+
}
497+
498+
w.Header().Set("Content-Type", "application/x-pem-file")
499+
500+
for _, crt := range intermediates {
501+
block := pem.EncodeToMemory(&pem.Block{
502+
Type: "CERTIFICATE",
503+
Bytes: crt.Raw,
504+
})
505+
506+
if _, err := w.Write(block); err != nil {
507+
log.Error(w, r, err)
508+
return
509+
}
510+
}
511+
}
512+
463513
// Federation returns all the public certificates in the federation.
464514
func Federation(w http.ResponseWriter, r *http.Request) {
465515
federated, err := mustAuthority(r.Context()).GetFederation()

api/api_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/stretchr/testify/assert"
3232
"github.com/stretchr/testify/require"
3333
"go.step.sm/crypto/jose"
34+
"go.step.sm/crypto/minica"
3435
"go.step.sm/crypto/x509util"
3536
"golang.org/x/crypto/ssh"
3637

@@ -147,6 +148,13 @@ nIHOI54lAqDeF7A0y73fPRVCiJEWmuxz0g==
147148
privKey = "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiNEhBYjE0WDQ5OFM4LWxSb29JTnpqZyJ9.RbkJXGzI3kOsaP20KmZs0ELFLgpRddAE49AJHlEblw-uH_gg6SV3QA.M3MArEpHgI171lhm.gBlFySpzK9F7riBJbtLSNkb4nAw_gWokqs1jS-ZK1qxuqTK-9mtX5yILjRnftx9P9uFp5xt7rvv4Mgom1Ed4V9WtIyfNP_Cz3Pme1Eanp5nY68WCe_yG6iSB1RJdMDBUb2qBDZiBdhJim1DRXsOfgedOrNi7GGbppMlD77DEpId118owR5izA-c6Q_hg08hIE3tnMAnebDNQoF9jfEY99_AReVRH8G4hgwZEPCfXMTb3J-lowKGG4vXIbK5knFLh47SgOqG4M2M51SMS-XJ7oBz1Vjoamc90QIqKV51rvZ5m0N_sPFtxzcfV4E9yYH3XVd4O-CG4ydVKfKVyMtQ.mcKFZqBHp_n7Ytj2jz9rvw"
148149
)
149150

151+
func mustJSON(t *testing.T, v any) []byte {
152+
t.Helper()
153+
var buf bytes.Buffer
154+
require.NoError(t, json.NewEncoder(&buf).Encode(v))
155+
return buf.Bytes()
156+
}
157+
150158
func parseCertificate(data string) *x509.Certificate {
151159
block, _ := pem.Decode([]byte(data))
152160
if block == nil {
@@ -199,6 +207,7 @@ type mockAuthority struct {
199207
revoke func(context.Context, *authority.RevokeOptions) error
200208
getEncryptedKey func(kid string) (string, error)
201209
getRoots func() ([]*x509.Certificate, error)
210+
getIntermediateCertificates func() []*x509.Certificate
202211
getFederation func() ([]*x509.Certificate, error)
203212
getCRL func() (*authority.CertificateRevocationListInfo, error)
204213
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) {
321330
return m.ret1.([]*x509.Certificate), m.err
322331
}
323332

333+
func (m *mockAuthority) GetIntermediateCertificates() []*x509.Certificate {
334+
if m.getIntermediateCertificates != nil {
335+
return m.getIntermediateCertificates()
336+
}
337+
return m.ret1.([]*x509.Certificate)
338+
}
339+
324340
func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
325341
if m.getFederation != nil {
326342
return m.getFederation()
@@ -1658,3 +1674,83 @@ func TestLogSSHCertificate(t *testing.T) {
16581674
assert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
16591675
assert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
16601676
}
1677+
1678+
func TestIntermediates(t *testing.T) {
1679+
ca, err := minica.New()
1680+
require.NoError(t, err)
1681+
1682+
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
1683+
mockMustAuthority(t, &mockAuthority{
1684+
ret1: crt,
1685+
})
1686+
return httptest.NewRequest("GET", "/intermediates", http.NoBody)
1687+
}
1688+
1689+
type args struct {
1690+
crts []*x509.Certificate
1691+
}
1692+
tests := []struct {
1693+
name string
1694+
args args
1695+
wantStatusCode int
1696+
wantBody []byte
1697+
}{
1698+
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
1699+
Certificates: []Certificate{{ca.Intermediate}},
1700+
})},
1701+
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
1702+
Certificates: []Certificate{{ca.Root}, {ca.Intermediate}},
1703+
})},
1704+
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
1705+
}
1706+
for _, tt := range tests {
1707+
t.Run(tt.name, func(t *testing.T) {
1708+
w := httptest.NewRecorder()
1709+
r := getRequest(t, tt.args.crts)
1710+
Intermediates(w, r)
1711+
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
1712+
assert.Equal(t, tt.wantBody, w.Body.Bytes())
1713+
})
1714+
}
1715+
}
1716+
1717+
func TestIntermediatesPEM(t *testing.T) {
1718+
ca, err := minica.New()
1719+
require.NoError(t, err)
1720+
1721+
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
1722+
mockMustAuthority(t, &mockAuthority{
1723+
ret1: crt,
1724+
})
1725+
return httptest.NewRequest("GET", "/intermediates.pem", http.NoBody)
1726+
}
1727+
1728+
type args struct {
1729+
crts []*x509.Certificate
1730+
}
1731+
tests := []struct {
1732+
name string
1733+
args args
1734+
wantStatusCode int
1735+
wantBody []byte
1736+
}{
1737+
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusOK, pem.EncodeToMemory(&pem.Block{
1738+
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
1739+
})},
1740+
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusOK, append(pem.EncodeToMemory(&pem.Block{
1741+
Type: "CERTIFICATE", Bytes: ca.Root.Raw,
1742+
}), pem.EncodeToMemory(&pem.Block{
1743+
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
1744+
})...)},
1745+
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
1746+
}
1747+
for _, tt := range tests {
1748+
t.Run(tt.name, func(t *testing.T) {
1749+
w := httptest.NewRecorder()
1750+
r := getRequest(t, tt.args.crts)
1751+
IntermediatesPEM(w, r)
1752+
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
1753+
assert.Equal(t, tt.wantBody, w.Body.Bytes())
1754+
})
1755+
}
1756+
}

0 commit comments

Comments
 (0)