Skip to content

Commit afb5d36

Browse files
committed
Allow to renew certificates using an x5c-like token.
1 parent 259e959 commit afb5d36

File tree

3 files changed

+138
-11
lines changed

3 files changed

+138
-11
lines changed

Diff for: api/api.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Authority interface {
4343
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
4444
Revoke(context.Context, *authority.RevokeOptions) error
4545
GetEncryptedKey(kid string) (string, error)
46-
GetRoots() (federation []*x509.Certificate, err error)
46+
GetRoots() ([]*x509.Certificate, error)
4747
GetFederation() ([]*x509.Certificate, error)
4848
Version() authority.Version
4949
}

Diff for: api/api_test.go

+87-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"crypto/tls"
1414
"crypto/x509"
1515
"crypto/x509/pkix"
16+
"encoding/base64"
1617
"encoding/json"
1718
"encoding/pem"
1819
"fmt"
@@ -34,6 +35,7 @@ import (
3435
"github.com/smallstep/certificates/logging"
3536
"github.com/smallstep/certificates/templates"
3637
"go.step.sm/crypto/jose"
38+
"go.step.sm/crypto/x509util"
3739
"golang.org/x/crypto/ssh"
3840
)
3941

@@ -920,32 +922,104 @@ func Test_caHandler_Renew(t *testing.T) {
920922
cs := &tls.ConnectionState{
921923
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
922924
}
925+
926+
// Prepare root and leaf for renew after expiry test.
927+
now := time.Now()
928+
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
929+
if err != nil {
930+
t.Fatal(err)
931+
}
932+
leafPub, leafPriv, err := ed25519.GenerateKey(rand.Reader)
933+
if err != nil {
934+
t.Fatal(err)
935+
}
936+
root := &x509.Certificate{
937+
Subject: pkix.Name{CommonName: "Test Root CA"},
938+
PublicKey: rootPub,
939+
KeyUsage: x509.KeyUsageCertSign,
940+
BasicConstraintsValid: true,
941+
IsCA: true,
942+
NotBefore: now.Add(-2 * time.Hour),
943+
NotAfter: now.Add(time.Hour),
944+
}
945+
root, err = x509util.CreateCertificate(root, root, rootPub, rootPriv)
946+
if err != nil {
947+
t.Fatal(err)
948+
}
949+
expiredLeaf := &x509.Certificate{
950+
Subject: pkix.Name{CommonName: "Leaf certificate"},
951+
PublicKey: leafPub,
952+
KeyUsage: x509.KeyUsageDigitalSignature,
953+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
954+
NotBefore: now.Add(-time.Hour),
955+
NotAfter: now.Add(-time.Minute),
956+
EmailAddresses: []string{"test@example.org"},
957+
}
958+
expiredLeaf, err = x509util.CreateCertificate(expiredLeaf, root, leafPub, rootPriv)
959+
if err != nil {
960+
t.Fatal(err)
961+
}
962+
963+
// Generate renew after expiry token
964+
so := new(jose.SignerOptions)
965+
so.WithType("JWT")
966+
so.WithHeader("x5cInsecure", []string{base64.StdEncoding.EncodeToString(expiredLeaf.Raw)})
967+
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: leafPriv}, so)
968+
if err != nil {
969+
t.Fatal(err)
970+
}
971+
generateX5cToken := func(claims jose.Claims) string {
972+
s, err := jose.Signed(sig).Claims(claims).CompactSerialize()
973+
if err != nil {
974+
t.Fatal(err)
975+
}
976+
return s
977+
}
978+
923979
tests := []struct {
924980
name string
925981
tls *tls.ConnectionState
982+
header http.Header
926983
cert *x509.Certificate
927984
root *x509.Certificate
928985
err error
929986
statusCode int
930987
}{
931-
{"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
932-
{"no tls", nil, nil, nil, nil, http.StatusBadRequest},
933-
{"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest},
934-
{"renew error", cs, nil, nil, errs.Forbidden("an error"), http.StatusForbidden},
988+
{"ok", cs, nil, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
989+
{"ok renew after expiry", &tls.ConnectionState{}, http.Header{
990+
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
991+
NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
992+
})},
993+
}, expiredLeaf, root, nil, http.StatusCreated},
994+
{"no tls", nil, nil, nil, nil, nil, http.StatusBadRequest},
995+
{"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, nil, http.StatusBadRequest},
996+
{"renew error", cs, nil, nil, nil, errs.Forbidden("an error"), http.StatusForbidden},
997+
{"fail expired token", &tls.ConnectionState{}, http.Header{
998+
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
999+
NotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),
1000+
})},
1001+
}, expiredLeaf, root, errs.Forbidden("an error"), http.StatusUnauthorized},
1002+
{"fail invalid root", &tls.ConnectionState{}, http.Header{
1003+
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
1004+
NotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),
1005+
})},
1006+
}, expiredLeaf, parseCertificate(rootPEM), errs.Forbidden("an error"), http.StatusUnauthorized},
9351007
}
9361008

937-
expected := []byte(`{"crt":"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n","certChain":["` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`)
938-
9391009
for _, tt := range tests {
9401010
t.Run(tt.name, func(t *testing.T) {
9411011
h := New(&mockAuthority{
9421012
ret1: tt.cert, ret2: tt.root, err: tt.err,
1013+
getRoots: func() ([]*x509.Certificate, error) {
1014+
return []*x509.Certificate{tt.root}, nil
1015+
},
9431016
getTLSOptions: func() *authority.TLSOptions {
9441017
return nil
9451018
},
9461019
}).(*caHandler)
9471020
req := httptest.NewRequest("POST", "http://example.com/renew", nil)
9481021
req.TLS = tt.tls
1022+
req.Header = tt.header
9491023
w := httptest.NewRecorder()
9501024
h.Renew(logging.NewResponseLogger(w), req)
9511025
res := w.Result()
@@ -960,8 +1034,14 @@ func Test_caHandler_Renew(t *testing.T) {
9601034
t.Errorf("caHandler.Renew unexpected error = %v", err)
9611035
}
9621036
if tt.statusCode < http.StatusBadRequest {
1037+
expected := []byte(`{"crt":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.cert.Raw})), "\n", `\n`) + `",` +
1038+
`"ca":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.root.Raw})), "\n", `\n`) + `",` +
1039+
`"certChain":["` +
1040+
strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.cert.Raw})), "\n", `\n`) + `","` +
1041+
strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.root.Raw})), "\n", `\n`) + `"]}`)
1042+
9631043
if !bytes.Equal(bytes.TrimSpace(body), expected) {
964-
t.Errorf("caHandler.Root Body = %s, wants %s", body, expected)
1044+
t.Errorf("caHandler.Root Body = \n%s, wants \n%s", body, expected)
9651045
}
9661046
}
9671047
})

Diff for: api/renew.go

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package api
22

33
import (
4+
"crypto/x509"
45
"net/http"
6+
"strings"
7+
"time"
58

69
"github.com/smallstep/certificates/errs"
10+
"go.step.sm/crypto/jose"
11+
)
12+
13+
const (
14+
authorizationHeader = "Authorization"
15+
bearerScheme = "Bearer"
716
)
817

918
// Renew uses the information of certificate in the TLS connection to create a
1019
// new one.
1120
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
12-
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
13-
WriteError(w, errs.BadRequest("missing client certificate"))
21+
cert, err := h.getPeerCertificate(r)
22+
if err != nil {
23+
WriteError(w, err)
1424
return
1525
}
1626

17-
certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0])
27+
certChain, err := h.Authority.Renew(cert)
1828
if err != nil {
1929
WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
2030
return
@@ -33,3 +43,40 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
3343
TLSOptions: h.Authority.GetTLSOptions(),
3444
}, http.StatusCreated)
3545
}
46+
47+
func (h *caHandler) getPeerCertificate(r *http.Request) (*x509.Certificate, error) {
48+
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
49+
return r.TLS.PeerCertificates[0], nil
50+
}
51+
52+
if s := r.Header.Get(authorizationHeader); s != "" {
53+
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
54+
roots, err := h.Authority.GetRoots()
55+
if err != nil {
56+
return nil, errs.BadRequestErr(err, "missing client certificate")
57+
}
58+
jwt, chain, err := jose.ParseX5cInsecure(parts[1], roots)
59+
if err != nil {
60+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating client certificate"))
61+
}
62+
63+
var claims jose.Claims
64+
leaf := chain[0][0]
65+
if err := jwt.Claims(leaf.PublicKey, &claims); err != nil {
66+
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating client certificate"))
67+
}
68+
69+
// According to "rfc7519 JSON Web Token" acceptable skew should be no
70+
// more than a few minutes.
71+
if err = claims.ValidateWithLeeway(jose.Expected{
72+
Time: time.Now().UTC(),
73+
}, time.Minute); err != nil {
74+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating client certificate"))
75+
}
76+
77+
return leaf, nil
78+
}
79+
}
80+
81+
return nil, errs.BadRequest("missing client certificate")
82+
}

0 commit comments

Comments
 (0)