Skip to content

Commit 616490a

Browse files
committed
Refactor renew after expiry token authorization
This changes adds a new authority method that authorizes the renew after expiry tokens.
1 parent 41ea67c commit 616490a

File tree

12 files changed

+527
-57
lines changed

12 files changed

+527
-57
lines changed

api/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Authority interface {
3333
// context specifies the Authorize[Sign|Revoke|etc.] method.
3434
Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error)
3535
AuthorizeSign(ott string) ([]provisioner.SignOption, error)
36+
AuthorizeRenewToken(ctx context.Context, token string) (*x509.Certificate, error)
3637
GetTLSOptions() *config.TLSOptions
3738
Root(shasum string) (*x509.Certificate, error)
3839
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)

api/api_test.go

+30-7
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ type mockAuthority struct {
173173
ret1, ret2 interface{}
174174
err error
175175
authorizeSign func(ott string) ([]provisioner.SignOption, error)
176+
authorizeRenewToken func(ctx context.Context, ott string) (*x509.Certificate, error)
176177
getTLSOptions func() *authority.TLSOptions
177178
root func(shasum string) (*x509.Certificate, error)
178179
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
@@ -210,6 +211,13 @@ func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, err
210211
return m.ret1.([]provisioner.SignOption), m.err
211212
}
212213

214+
func (m *mockAuthority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
215+
if m.authorizeRenewToken != nil {
216+
return m.authorizeRenewToken(ctx, ott)
217+
}
218+
return m.ret1.(*x509.Certificate), m.err
219+
}
220+
213221
func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions {
214222
if m.getTLSOptions != nil {
215223
return m.getTLSOptions()
@@ -1010,8 +1018,21 @@ func Test_caHandler_Renew(t *testing.T) {
10101018
t.Run(tt.name, func(t *testing.T) {
10111019
h := New(&mockAuthority{
10121020
ret1: tt.cert, ret2: tt.root, err: tt.err,
1013-
getRoots: func() ([]*x509.Certificate, error) {
1014-
return []*x509.Certificate{tt.root}, nil
1021+
authorizeRenewToken: func(ctx context.Context, ott string) (*x509.Certificate, error) {
1022+
jwt, chain, err := jose.ParseX5cInsecure(ott, []*x509.Certificate{tt.root})
1023+
if err != nil {
1024+
return nil, errs.Unauthorized(err.Error())
1025+
}
1026+
var claims jose.Claims
1027+
if err := jwt.Claims(chain[0][0].PublicKey, &claims); err != nil {
1028+
return nil, errs.Unauthorized(err.Error())
1029+
}
1030+
if err := claims.ValidateWithLeeway(jose.Expected{
1031+
Time: now,
1032+
}, time.Minute); err != nil {
1033+
return nil, errs.Unauthorized(err.Error())
1034+
}
1035+
return chain[0][0], nil
10151036
},
10161037
getTLSOptions: func() *authority.TLSOptions {
10171038
return nil
@@ -1022,17 +1043,19 @@ func Test_caHandler_Renew(t *testing.T) {
10221043
req.Header = tt.header
10231044
w := httptest.NewRecorder()
10241045
h.Renew(logging.NewResponseLogger(w), req)
1025-
res := w.Result()
10261046

1027-
if res.StatusCode != tt.statusCode {
1028-
t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
1029-
}
1047+
res := w.Result()
1048+
defer res.Body.Close()
10301049

10311050
body, err := io.ReadAll(res.Body)
1032-
res.Body.Close()
10331051
if err != nil {
10341052
t.Errorf("caHandler.Renew unexpected error = %v", err)
10351053
}
1054+
if res.StatusCode != tt.statusCode {
1055+
t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
1056+
t.Errorf("%s", body)
1057+
}
1058+
10361059
if tt.statusCode < http.StatusBadRequest {
10371060
expected := []byte(`{"crt":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.cert.Raw})), "\n", `\n`) + `",` +
10381061
`"ca":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.root.Raw})), "\n", `\n`) + `",` +

api/renew.go

+1-28
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import (
44
"crypto/x509"
55
"net/http"
66
"strings"
7-
"time"
87

98
"github.com/smallstep/certificates/errs"
10-
"go.step.sm/crypto/jose"
119
)
1210

1311
const (
@@ -48,35 +46,10 @@ func (h *caHandler) getPeerCertificate(r *http.Request) (*x509.Certificate, erro
4846
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
4947
return r.TLS.PeerCertificates[0], nil
5048
}
51-
5249
if s := r.Header.Get(authorizationHeader); s != "" {
5350
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
51+
return h.Authority.AuthorizeRenewToken(r.Context(), parts[1])
7852
}
7953
}
80-
8154
return nil, errs.BadRequest("missing client certificate")
8255
}

authority/authorize.go

+78
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/x509"
77
"encoding/hex"
88
"net/http"
9+
"net/url"
910
"strconv"
1011
"strings"
1112
"time"
@@ -371,3 +372,80 @@ func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error
371372
}
372373
return nil
373374
}
375+
376+
// AuthorizeRenewToken validates the renew token and returns the leaf
377+
// certificate in the x5cInsecure header.
378+
func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
379+
var claims jose.Claims
380+
jwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs)
381+
if err != nil {
382+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
383+
}
384+
leaf := chain[0][0]
385+
if err := jwt.Claims(leaf.PublicKey, &claims); err != nil {
386+
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token"))
387+
}
388+
389+
p, ok := a.provisioners.LoadByCertificate(leaf)
390+
if !ok {
391+
return nil, errs.Unauthorized("error validating renew token: cannot get provisioner from certificate")
392+
}
393+
if err := a.UseToken(ott, p); err != nil {
394+
return nil, err
395+
}
396+
397+
if err := claims.ValidateWithLeeway(jose.Expected{
398+
Issuer: p.GetName(),
399+
Subject: leaf.Subject.CommonName,
400+
Time: time.Now().UTC(),
401+
}, time.Minute); err != nil {
402+
switch err {
403+
case jose.ErrInvalidIssuer:
404+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
405+
case jose.ErrInvalidSubject:
406+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
407+
case jose.ErrNotValidYet:
408+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
409+
case jose.ErrExpired:
410+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
411+
case jose.ErrIssuedInTheFuture:
412+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
413+
default:
414+
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
415+
}
416+
}
417+
418+
audiences := a.config.GetAudiences().Renew
419+
if !matchesAudience(claims.Audience, audiences) {
420+
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
421+
}
422+
423+
return leaf, nil
424+
}
425+
426+
// matchesAudience returns true if A and B share at least one element.
427+
func matchesAudience(as, bs []string) bool {
428+
if len(bs) == 0 || len(as) == 0 {
429+
return false
430+
}
431+
432+
for _, b := range bs {
433+
for _, a := range as {
434+
if b == a || stripPort(a) == stripPort(b) {
435+
return true
436+
}
437+
}
438+
}
439+
return false
440+
}
441+
442+
// stripPort attempts to strip the port from the given url. If parsing the url
443+
// produces errors it will just return the passed argument.
444+
func stripPort(rawurl string) string {
445+
u, err := url.Parse(rawurl)
446+
if err != nil {
447+
return rawurl
448+
}
449+
u.Host = u.Hostname()
450+
return u.String()
451+
}

0 commit comments

Comments
 (0)