forked from TrueCloudLab/certificates
Refactor renew after expiry token authorization
This changes adds a new authority method that authorizes the renew after expiry tokens.
This commit is contained in:
parent
41ea67ce10
commit
616490a9c6
12 changed files with 527 additions and 57 deletions
|
@ -33,6 +33,7 @@ type Authority interface {
|
|||
// context specifies the Authorize[Sign|Revoke|etc.] method.
|
||||
Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
||||
AuthorizeSign(ott string) ([]provisioner.SignOption, error)
|
||||
AuthorizeRenewToken(ctx context.Context, token string) (*x509.Certificate, error)
|
||||
GetTLSOptions() *config.TLSOptions
|
||||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
|
|
|
@ -173,6 +173,7 @@ type mockAuthority struct {
|
|||
ret1, ret2 interface{}
|
||||
err error
|
||||
authorizeSign func(ott string) ([]provisioner.SignOption, error)
|
||||
authorizeRenewToken func(ctx context.Context, ott string) (*x509.Certificate, error)
|
||||
getTLSOptions func() *authority.TLSOptions
|
||||
root func(shasum string) (*x509.Certificate, error)
|
||||
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
|
|||
return m.ret1.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
|
||||
if m.authorizeRenewToken != nil {
|
||||
return m.authorizeRenewToken(ctx, ott)
|
||||
}
|
||||
return m.ret1.(*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions {
|
||||
if m.getTLSOptions != nil {
|
||||
return m.getTLSOptions()
|
||||
|
@ -1010,8 +1018,21 @@ func Test_caHandler_Renew(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := New(&mockAuthority{
|
||||
ret1: tt.cert, ret2: tt.root, err: tt.err,
|
||||
getRoots: func() ([]*x509.Certificate, error) {
|
||||
return []*x509.Certificate{tt.root}, nil
|
||||
authorizeRenewToken: func(ctx context.Context, ott string) (*x509.Certificate, error) {
|
||||
jwt, chain, err := jose.ParseX5cInsecure(ott, []*x509.Certificate{tt.root})
|
||||
if err != nil {
|
||||
return nil, errs.Unauthorized(err.Error())
|
||||
}
|
||||
var claims jose.Claims
|
||||
if err := jwt.Claims(chain[0][0].PublicKey, &claims); err != nil {
|
||||
return nil, errs.Unauthorized(err.Error())
|
||||
}
|
||||
if err := claims.ValidateWithLeeway(jose.Expected{
|
||||
Time: now,
|
||||
}, time.Minute); err != nil {
|
||||
return nil, errs.Unauthorized(err.Error())
|
||||
}
|
||||
return chain[0][0], nil
|
||||
},
|
||||
getTLSOptions: func() *authority.TLSOptions {
|
||||
return nil
|
||||
|
@ -1022,17 +1043,19 @@ func Test_caHandler_Renew(t *testing.T) {
|
|||
req.Header = tt.header
|
||||
w := httptest.NewRecorder()
|
||||
h.Renew(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.Renew unexpected error = %v", err)
|
||||
}
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
t.Errorf("%s", body)
|
||||
}
|
||||
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
expected := []byte(`{"crt":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.cert.Raw})), "\n", `\n`) + `",` +
|
||||
`"ca":"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.root.Raw})), "\n", `\n`) + `",` +
|
||||
|
|
29
api/renew.go
29
api/renew.go
|
@ -4,10 +4,8 @@ import (
|
|||
"crypto/x509"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -48,35 +46,10 @@ func (h *caHandler) getPeerCertificate(r *http.Request) (*x509.Certificate, erro
|
|||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
return r.TLS.PeerCertificates[0], nil
|
||||
}
|
||||
|
||||
if s := r.Header.Get(authorizationHeader); s != "" {
|
||||
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
||||
roots, err := h.Authority.GetRoots()
|
||||
if err != nil {
|
||||
return nil, errs.BadRequestErr(err, "missing client certificate")
|
||||
}
|
||||
jwt, chain, err := jose.ParseX5cInsecure(parts[1], roots)
|
||||
if err != nil {
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating client certificate"))
|
||||
}
|
||||
|
||||
var claims jose.Claims
|
||||
leaf := chain[0][0]
|
||||
if err := jwt.Claims(leaf.PublicKey, &claims); err != nil {
|
||||
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating client certificate"))
|
||||
}
|
||||
|
||||
// According to "rfc7519 JSON Web Token" acceptable skew should be no
|
||||
// more than a few minutes.
|
||||
if err = claims.ValidateWithLeeway(jose.Expected{
|
||||
Time: time.Now().UTC(),
|
||||
}, time.Minute); err != nil {
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating client certificate"))
|
||||
}
|
||||
|
||||
return leaf, nil
|
||||
return h.Authority.AuthorizeRenewToken(r.Context(), parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errs.BadRequest("missing client certificate")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -371,3 +372,80 @@ func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeRenewToken validates the renew token and returns the leaf
|
||||
// certificate in the x5cInsecure header.
|
||||
func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
|
||||
var claims jose.Claims
|
||||
jwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs)
|
||||
if err != nil {
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
||||
}
|
||||
leaf := chain[0][0]
|
||||
if err := jwt.Claims(leaf.PublicKey, &claims); err != nil {
|
||||
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token"))
|
||||
}
|
||||
|
||||
p, ok := a.provisioners.LoadByCertificate(leaf)
|
||||
if !ok {
|
||||
return nil, errs.Unauthorized("error validating renew token: cannot get provisioner from certificate")
|
||||
}
|
||||
if err := a.UseToken(ott, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := claims.ValidateWithLeeway(jose.Expected{
|
||||
Issuer: p.GetName(),
|
||||
Subject: leaf.Subject.CommonName,
|
||||
Time: time.Now().UTC(),
|
||||
}, time.Minute); err != nil {
|
||||
switch err {
|
||||
case jose.ErrInvalidIssuer:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
|
||||
case jose.ErrInvalidSubject:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
|
||||
case jose.ErrNotValidYet:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
|
||||
case jose.ErrExpired:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
|
||||
case jose.ErrIssuedInTheFuture:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
|
||||
default:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
||||
}
|
||||
}
|
||||
|
||||
audiences := a.config.GetAudiences().Renew
|
||||
if !matchesAudience(claims.Audience, audiences) {
|
||||
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
|
||||
}
|
||||
|
||||
return leaf, nil
|
||||
}
|
||||
|
||||
// matchesAudience returns true if A and B share at least one element.
|
||||
func matchesAudience(as, bs []string) bool {
|
||||
if len(bs) == 0 || len(as) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, b := range bs {
|
||||
for _, a := range as {
|
||||
if b == a || stripPort(a) == stripPort(b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stripPort attempts to strip the port from the given url. If parsing the url
|
||||
// produces errors it will just return the passed argument.
|
||||
func stripPort(rawurl string) string {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return rawurl
|
||||
}
|
||||
u.Host = u.Hostname()
|
||||
return u.String()
|
||||
}
|
||||
|
|
|
@ -3,11 +3,15 @@ package authority
|
|||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -20,6 +24,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
@ -1320,3 +1325,283 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type stepProvisionerASN1 struct {
|
||||
Type int
|
||||
Name []byte
|
||||
CredentialID []byte
|
||||
KeyValuePairs []string `asn1:"optional,omitempty"`
|
||||
}
|
||||
|
||||
_, signer, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
csr, err := x509util.CreateCertificateRequest("test.example.com", []string{"test.example.com"}, signer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, otherSigner, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
generateX5cToken := func(a *Authority, key crypto.Signer, claims jose.Claims, opts ...provisioner.SignOption) (string, *x509.Certificate) {
|
||||
chain, err := a.Sign(csr, provisioner.SignOptions{}, opts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var x5c []string
|
||||
for _, c := range chain {
|
||||
x5c = append(x5c, base64.StdEncoding.EncodeToString(c.Raw))
|
||||
}
|
||||
|
||||
so := new(jose.SignerOptions)
|
||||
so.WithType("JWT")
|
||||
so.WithHeader("x5cInsecure", x5c)
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: key}, so)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, chain[0]
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
a1 := testAuthority(t)
|
||||
t1, c1 := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
t2, c2 := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
IssuedAt: jose.NewNumericDate(now),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now.Add(-time.Hour)
|
||||
cert.NotAfter = now.Add(-time.Minute)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("foobar"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badProvisioner, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("foobar"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badIssuer, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "bad-issuer",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badSubject, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "bad-subject",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badNotBefore, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/sign"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
Expiry: jose.NewNumericDate(now.Add(10 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badExpiry, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/sign"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now.Add(-5 * time.Minute)),
|
||||
Expiry: jose.NewNumericDate(now.Add(-time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badIssuedAt, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/sign"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
IssuedAt: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badAudience, _ := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/sign"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-cli",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ott string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
authority *Authority
|
||||
args args
|
||||
want *x509.Certificate
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", a1, args{ctx, t1}, c1, false},
|
||||
{"ok expired cert", a1, args{ctx, t2}, c2, false},
|
||||
{"fail token", a1, args{ctx, "not.a.token"}, nil, true},
|
||||
{"fail token reuse", a1, args{ctx, t1}, nil, true},
|
||||
{"fail token signature", a1, args{ctx, badSigner}, nil, true},
|
||||
{"fail token provisioner", a1, args{ctx, badProvisioner}, nil, true},
|
||||
{"fail token iss", a1, args{ctx, badIssuer}, nil, true},
|
||||
{"fail token sub", a1, args{ctx, badSubject}, nil, true},
|
||||
{"fail token iat", a1, args{ctx, badNotBefore}, nil, true},
|
||||
{"fail token iat", a1, args{ctx, badExpiry}, nil, true},
|
||||
{"fail token iat", a1, args{ctx, badIssuedAt}, nil, true},
|
||||
{"fail token aud", a1, args{ctx, badAudience}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.authority.AuthorizeRenewToken(tt.args.ctx, tt.args.ott)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authority.AuthorizeRenewToken() error = %+v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Authority.AuthorizeRenewToken() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,28 +272,32 @@ func (c *Config) GetAudiences() provisioner.Audiences {
|
|||
}
|
||||
|
||||
for _, name := range c.DNSNames {
|
||||
hostname := toHostname(name)
|
||||
audiences.Sign = append(audiences.Sign,
|
||||
fmt.Sprintf("https://%s/1.0/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/ssh/sign", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/sign", hostname),
|
||||
fmt.Sprintf("https://%s/sign", hostname),
|
||||
fmt.Sprintf("https://%s/1.0/ssh/sign", hostname),
|
||||
fmt.Sprintf("https://%s/ssh/sign", hostname))
|
||||
audiences.Renew = append(audiences.Renew,
|
||||
fmt.Sprintf("https://%s/1.0/renew", hostname),
|
||||
fmt.Sprintf("https://%s/renew", hostname))
|
||||
audiences.Revoke = append(audiences.Revoke,
|
||||
fmt.Sprintf("https://%s/1.0/revoke", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/revoke", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/revoke", hostname),
|
||||
fmt.Sprintf("https://%s/revoke", hostname))
|
||||
audiences.SSHSign = append(audiences.SSHSign,
|
||||
fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/ssh/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/1.0/sign", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/sign", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/ssh/sign", hostname),
|
||||
fmt.Sprintf("https://%s/ssh/sign", hostname),
|
||||
fmt.Sprintf("https://%s/1.0/sign", hostname),
|
||||
fmt.Sprintf("https://%s/sign", hostname))
|
||||
audiences.SSHRevoke = append(audiences.SSHRevoke,
|
||||
fmt.Sprintf("https://%s/1.0/ssh/revoke", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/ssh/revoke", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/ssh/revoke", hostname),
|
||||
fmt.Sprintf("https://%s/ssh/revoke", hostname))
|
||||
audiences.SSHRenew = append(audiences.SSHRenew,
|
||||
fmt.Sprintf("https://%s/1.0/ssh/renew", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/ssh/renew", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/ssh/renew", hostname),
|
||||
fmt.Sprintf("https://%s/ssh/renew", hostname))
|
||||
audiences.SSHRekey = append(audiences.SSHRekey,
|
||||
fmt.Sprintf("https://%s/1.0/ssh/rekey", toHostname(name)),
|
||||
fmt.Sprintf("https://%s/ssh/rekey", toHostname(name)))
|
||||
fmt.Sprintf("https://%s/1.0/ssh/rekey", hostname),
|
||||
fmt.Sprintf("https://%s/ssh/rekey", hostname))
|
||||
}
|
||||
|
||||
return audiences
|
||||
|
|
|
@ -35,9 +35,7 @@ type JWK struct {
|
|||
EncryptedKey string `json:"encryptedKey,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
// claimer *Claimer
|
||||
// audiences Audiences
|
||||
ctl *Controller
|
||||
ctl *Controller
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
|
|
|
@ -46,6 +46,7 @@ var ErrAllowTokenReuse = stderrors.New("allow token reuse")
|
|||
// Audiences stores all supported audiences by request type.
|
||||
type Audiences struct {
|
||||
Sign []string
|
||||
Renew []string
|
||||
Revoke []string
|
||||
SSHSign []string
|
||||
SSHRevoke []string
|
||||
|
@ -56,6 +57,7 @@ type Audiences struct {
|
|||
// All returns all supported audiences across all request types in one list.
|
||||
func (a Audiences) All() (auds []string) {
|
||||
auds = a.Sign
|
||||
auds = append(auds, a.Renew...)
|
||||
auds = append(auds, a.Revoke...)
|
||||
auds = append(auds, a.SSHSign...)
|
||||
auds = append(auds, a.SSHRevoke...)
|
||||
|
@ -69,6 +71,7 @@ func (a Audiences) All() (auds []string) {
|
|||
func (a Audiences) WithFragment(fragment string) Audiences {
|
||||
ret := Audiences{
|
||||
Sign: make([]string, len(a.Sign)),
|
||||
Renew: make([]string, len(a.Renew)),
|
||||
Revoke: make([]string, len(a.Revoke)),
|
||||
SSHSign: make([]string, len(a.SSHSign)),
|
||||
SSHRevoke: make([]string, len(a.SSHRevoke)),
|
||||
|
@ -82,6 +85,13 @@ func (a Audiences) WithFragment(fragment string) Audiences {
|
|||
ret.Sign[i] = s
|
||||
}
|
||||
}
|
||||
for i, s := range a.Renew {
|
||||
if u, err := url.Parse(s); err == nil {
|
||||
ret.Renew[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
|
||||
} else {
|
||||
ret.Renew[i] = s
|
||||
}
|
||||
}
|
||||
for i, s := range a.Revoke {
|
||||
if u, err := url.Parse(s); err == nil {
|
||||
ret.Revoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
|
||||
|
|
30
ca/client.go
30
ca/client.go
|
@ -723,6 +723,36 @@ retry:
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// RenewWithToken performs the renew request to the CA with the given
|
||||
// authorization token and returns the api.SignResponse struct. This method is
|
||||
// generally used to renew an expired certificate.
|
||||
func (c *Client) RenewWithToken(token string) (*api.SignResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"})
|
||||
req, err := http.NewRequest("POST", u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.RenewWithToken; error creating request")
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
retry:
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.RenewWithToken; client POST %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readError(resp.Body)
|
||||
}
|
||||
var sign api.SignResponse
|
||||
if err := readJSON(resp.Body, &sign); err != nil {
|
||||
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.RenewWithToken; error reading %s", u)
|
||||
}
|
||||
return &sign, nil
|
||||
}
|
||||
|
||||
// Rekey performs the rekey request to the CA and returns the api.SignResponse
|
||||
// struct.
|
||||
func (c *Client) Rekey(req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {
|
||||
|
|
|
@ -529,6 +529,74 @@ func TestClient_Renew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_RenewWithToken(t *testing.T) {
|
||||
ok := &api.SignResponse{
|
||||
ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)},
|
||||
CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)},
|
||||
CertChainPEM: []api.Certificate{
|
||||
{Certificate: parseCertificate(certPEM)},
|
||||
{Certificate: parseCertificate(rootPEM)},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
response interface{}
|
||||
responseCode int
|
||||
wantErr bool
|
||||
err error
|
||||
}{
|
||||
{"ok", ok, 200, false, nil},
|
||||
{"unauthorized", errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
|
||||
{"empty request", errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestPrefix)},
|
||||
{"nil request", errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestPrefix)},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(nil)
|
||||
defer srv.Close()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))
|
||||
if err != nil {
|
||||
t.Errorf("NewClient() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
api.JSONStatus(w, errs.InternalServer("force"), 500)
|
||||
} else {
|
||||
api.JSONStatus(w, tt.response, tt.responseCode)
|
||||
}
|
||||
})
|
||||
|
||||
got, err := c.RenewWithToken("token")
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Printf("%+v", err)
|
||||
t.Errorf("Client.RenewWithToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
if got != nil {
|
||||
t.Errorf("Client.RenewWithToken() = %v, want nil", got)
|
||||
}
|
||||
|
||||
sc, ok := err.(errs.StatusCoder)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.responseCode)
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
default:
|
||||
if !reflect.DeepEqual(got, tt.response) {
|
||||
t.Errorf("Client.RenewWithToken() = %v, want %v", got, tt.response)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Rekey(t *testing.T) {
|
||||
ok := &api.SignResponse{
|
||||
ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)},
|
||||
|
|
2
go.mod
2
go.mod
|
@ -34,7 +34,7 @@ require (
|
|||
github.com/urfave/cli v1.22.4
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.step.sm/cli-utils v0.7.0
|
||||
go.step.sm/crypto v0.15.0
|
||||
go.step.sm/crypto v0.15.3
|
||||
go.step.sm/linkedca v0.10.0
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d
|
||||
|
|
4
go.sum
4
go.sum
|
@ -683,8 +683,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
|
|||
go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds=
|
||||
go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E=
|
||||
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||
go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0=
|
||||
go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
||||
go.step.sm/crypto v0.15.3 h1:f3GMl+aCydt294BZRjTYwpaXRqwwndvoTY2NLN4wu10=
|
||||
go.step.sm/crypto v0.15.3/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
||||
go.step.sm/linkedca v0.10.0 h1:+bqymMRulHYkVde4l16FnqFVskoS6HCWJN5Z5cxAqF8=
|
||||
go.step.sm/linkedca v0.10.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
|
|
Loading…
Reference in a new issue