mirror of
https://github.com/outbackdingo/certificates.git
synced 2026-01-27 10:18:34 +00:00
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:
50
api/api.go
50
api/api.go
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user