Skip to content

Commit 9fd4dff

Browse files
authored
Merge pull request #1962 from smallstep/mariano/intermediates
Add endpoints that return intermediate certificates
2 parents 92e95e4 + 6e7a186 commit 9fd4dff

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

api/api.go

Lines changed: 50 additions & 0 deletions
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, &IntermediatesResponse{
486+
Certificates: certs,
487+
}, http.StatusCreated)
488+
}
489+
490+
// IntermediatesPEM returns all the intermediate 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

Lines changed: 96 additions & 0 deletions
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)