From 616490a9c6b8fc2b914c000c6ac3a7e714bbd939 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Mar 2022 20:21:01 -0800 Subject: [PATCH] Refactor renew after expiry token authorization This changes adds a new authority method that authorizes the renew after expiry tokens. --- api/api.go | 1 + api/api_test.go | 37 +++- api/renew.go | 29 +-- authority/authorize.go | 78 ++++++++ authority/authorize_test.go | 285 +++++++++++++++++++++++++++ authority/config/config.go | 36 ++-- authority/provisioner/jwk.go | 4 +- authority/provisioner/provisioner.go | 10 + ca/client.go | 30 +++ ca/client_test.go | 68 +++++++ go.mod | 2 +- go.sum | 4 +- 12 files changed, 527 insertions(+), 57 deletions(-) diff --git a/api/api.go b/api/api.go index c61e447f..7786bd0d 100644 --- a/api/api.go +++ b/api/api.go @@ -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) diff --git a/api/api_test.go b/api/api_test.go index f2184596..717621cd 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -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`) + `",` + diff --git a/api/renew.go b/api/renew.go index a7449ba1..408d91a3 100644 --- a/api/renew.go +++ b/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") } diff --git a/authority/authorize.go b/authority/authorize.go index 4f64921b..7c1c2ff6 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -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() +} diff --git a/authority/authorize_test.go b/authority/authorize_test.go index b0ab04ec..b631741a 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -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) + } + }) + } +} diff --git a/authority/config/config.go b/authority/config/config.go index c33a2b1d..e4fcc863 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -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 diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 764f5d7d..c014bec0 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -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 diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 0b79bf4f..7438ea17 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -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() diff --git a/ca/client.go b/ca/client.go index 6bc48a42..56f17748 100644 --- a/ca/client.go +++ b/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) { diff --git a/ca/client_test.go b/ca/client_test.go index 29a4848d..e253dab5 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -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)}, diff --git a/go.mod b/go.mod index e6696529..f3ae5a09 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 123df6e4..f634a2ce 100644 --- a/go.sum +++ b/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=