Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Authority interface {
AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error)
GetTLSOptions() *config.TLSOptions
Root(shasum string) (*x509.Certificate, error)
Intermediate(shasum string) (*x509.Certificate, error)
SignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
Expand Down Expand Up @@ -227,6 +228,12 @@ type RootResponse struct {
RootPEM Certificate `json:"ca"`
}

// IntermediateResponse is the response object that returns the PEM of an
// intermediate certificate.
type IntermediateResponse struct {
IntermediatePEM Certificate `json:"ca"`
}

// ProvisionersResponse is the response object that returns the list of
// provisioners.
type ProvisionersResponse struct {
Expand Down Expand Up @@ -340,6 +347,7 @@ func Route(r Router) {
r.MethodFunc("GET", "/intermediates", Intermediates)
r.MethodFunc("GET", "/intermediates.pem", IntermediatesPEM)
r.MethodFunc("GET", "/federation", Federation)
r.MethodFunc("GET", "/intermediate/{sha}", Intermediate)

// SSH CA
r.MethodFunc("POST", "/ssh/sign", SSHSign)
Expand All @@ -360,6 +368,25 @@ func Route(r Router) {
r.MethodFunc("GET", "/ssh/get-hosts", SSHGetHosts)
}

// normalizeFingerprint lowercases the input and strips any dashes so that
// callers can pass fingerprints in the common colon-/dash-separated form.
func normalizeFingerprint(sha string) string {
return strings.ToLower(strings.ReplaceAll(sha, "-", ""))
}

// Intermediate is an HTTP handler that using the SHA256 from the URL, returns
// the intermediate certificate for the given SHA256.
func Intermediate(w http.ResponseWriter, r *http.Request) {
sum := normalizeFingerprint(chi.URLParam(r, "sha"))
cert, err := mustAuthority(r.Context()).Intermediate(sum)
if err != nil {
render.Error(w, r, errs.NotFoundErr(err, errs.WithMessage("intermediate certificate with fingerprint %q was not found", sum)))
return
}

render.JSON(w, r, &IntermediateResponse{IntermediatePEM: Certificate{cert}})
}

// Version is an HTTP handler that returns the version of the server.
func Version(w http.ResponseWriter, r *http.Request) {
v := mustAuthority(r.Context()).Version()
Expand All @@ -377,8 +404,7 @@ func Health(w http.ResponseWriter, r *http.Request) {
// Root is an HTTP handler that using the SHA256 from the URL, returns the root
// certificate for the given SHA256.
func Root(w http.ResponseWriter, r *http.Request) {
sha := chi.URLParam(r, "sha")
sum := strings.ToLower(strings.ReplaceAll(sha, "-", ""))
sum := normalizeFingerprint(chi.URLParam(r, "sha"))
// Load root certificate with the
cert, err := mustAuthority(r.Context()).Root(sum)
if err != nil {
Expand Down
67 changes: 67 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
Expand Down Expand Up @@ -197,6 +198,7 @@ type mockAuthority struct {
authorizeRenewToken func(ctx context.Context, ott string) (*x509.Certificate, error)
getTLSOptions func() *authority.TLSOptions
root func(shasum string) (*x509.Certificate, error)
intermediate func(shasum string) (*x509.Certificate, error)
signWithContext func(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
Expand Down Expand Up @@ -260,6 +262,13 @@ func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
return m.ret1.(*x509.Certificate), m.err
}

func (m *mockAuthority) Intermediate(shasum string) (*x509.Certificate, error) {
if m.intermediate != nil {
return m.intermediate(shasum)
}
return m.ret1.(*x509.Certificate), m.err
}

func (m *mockAuthority) SignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
if m.signWithContext != nil {
return m.signWithContext(ctx, cr, opts, signOpts...)
Expand Down Expand Up @@ -1758,3 +1767,61 @@ func TestIntermediatesPEM(t *testing.T) {
})
}
}

func TestIntermediate(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)

intermediateFP := fmt.Sprintf("%x", sha256.Sum256(ca.Intermediate.Raw))

tests := []struct {
name string
sha string
mockAuth *mockAuthority
wantStatusCode int
wantBody []byte
}{
{
name: "ok",
sha: intermediateFP,
mockAuth: &mockAuthority{
intermediate: func(sum string) (*x509.Certificate, error) {
if sum == intermediateFP {
return ca.Intermediate, nil
}
return nil, errs.NotFound("certificate with fingerprint %s was not found", sum)
},
},
wantStatusCode: http.StatusOK,
wantBody: mustJSON(t, IntermediateResponse{IntermediatePEM: Certificate{ca.Intermediate}}),
},
{
name: "not found",
sha: "0000000000000000000000000000000000000000000000000000000000000000",
mockAuth: &mockAuthority{
intermediate: func(sum string) (*x509.Certificate, error) {
return nil, errs.NotFound("certificate with fingerprint %s was not found", sum)
},
},
wantStatusCode: http.StatusNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, tt.mockAuth)

w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/intermediate/"+tt.sha, http.NoBody)

// Use a chi router so that URLParam works correctly.
router := chi.NewRouter()
router.Get("/intermediate/{sha}", Intermediate)
router.ServeHTTP(w, r)

assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
if tt.wantBody != nil {
assert.Equal(t, tt.wantBody, w.Body.Bytes())
}
})
}
}
14 changes: 14 additions & 0 deletions authority/root.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package authority

import (
"crypto/sha256"
"crypto/x509"
"fmt"
"strings"

"github.com/smallstep/certificates/errs"
)
Expand All @@ -20,6 +23,17 @@ func (a *Authority) Root(sum string) (*x509.Certificate, error) {
return crt, nil
}

// Intermediate returns the certificate corresponding to the given SHA sum
// argument, from the list of intermediate certificates configured in the CA.
func (a *Authority) Intermediate(sum string) (*x509.Certificate, error) {
for _, crt := range a.intermediateX509Certs {
if strings.EqualFold(fmt.Sprintf("%x", sha256.Sum256(crt.Raw)), sum) {
return crt, nil
}
}
return nil, errs.NotFound("certificate with fingerprint %s was not found", sum)
}

// GetRootCertificate returns the server root certificate.
func (a *Authority) GetRootCertificate() *x509.Certificate {
return a.rootX509Certs[0]
Expand Down
82 changes: 82 additions & 0 deletions authority/root_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package authority

import (
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"net/http"
"reflect"
"testing"
Expand Down Expand Up @@ -49,6 +51,86 @@ func TestRoot(t *testing.T) {
}
}

func TestAuthority_Intermediate(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)

// Create a second intermediate certificate for multi-cert testing.
signer, err := keyutil.GenerateDefaultSigner()
require.NoError(t, err)
secondIntermediate, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "Second Intermediate CA"},
PublicKey: signer.Public(),
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
})
require.NoError(t, err)

// Pre-compute fingerprints.
firstFP := fmt.Sprintf("%x", sha256.Sum256(ca.Intermediate.Raw))
secondFP := fmt.Sprintf("%x", sha256.Sum256(secondIntermediate.Raw))

tests := []struct {
name string
intermediates []*x509.Certificate
sum string
want *x509.Certificate
wantErr bool
errCode int
}{
{
name: "found",
intermediates: []*x509.Certificate{ca.Intermediate},
sum: firstFP,
want: ca.Intermediate,
},
{
name: "not found",
intermediates: []*x509.Certificate{ca.Intermediate},
sum: "0000000000000000000000000000000000000000000000000000000000000000",
wantErr: true,
errCode: http.StatusNotFound,
},
{
name: "empty list",
intermediates: []*x509.Certificate{},
sum: firstFP,
wantErr: true,
errCode: http.StatusNotFound,
},
{
name: "multiple intermediates - select first",
intermediates: []*x509.Certificate{ca.Intermediate, secondIntermediate},
sum: firstFP,
want: ca.Intermediate,
},
{
name: "multiple intermediates - select second",
intermediates: []*x509.Certificate{ca.Intermediate, secondIntermediate},
sum: secondFP,
want: secondIntermediate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authority{
intermediateX509Certs: tt.intermediates,
}
got, err := a.Intermediate(tt.sum)
if tt.wantErr {
require.Error(t, err)
var sc render.StatusCodedError
require.ErrorAs(t, err, &sc)
require.Equal(t, tt.errCode, sc.StatusCode())
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
}

func TestAuthority_GetRootCertificate(t *testing.T) {
cert, err := pemutil.ReadCertificate("testdata/certs/root_ca.crt")
if err != nil {
Expand Down