forked from TrueCloudLab/certificates
Allow to renew certificates using an x5c-like token.
This commit is contained in:
parent
259e95947c
commit
afb5d36206
3 changed files with 138 additions and 11 deletions
|
@ -43,7 +43,7 @@ type Authority interface {
|
||||||
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
||||||
Revoke(context.Context, *authority.RevokeOptions) error
|
Revoke(context.Context, *authority.RevokeOptions) error
|
||||||
GetEncryptedKey(kid string) (string, error)
|
GetEncryptedKey(kid string) (string, error)
|
||||||
GetRoots() (federation []*x509.Certificate, err error)
|
GetRoots() ([]*x509.Certificate, error)
|
||||||
GetFederation() ([]*x509.Certificate, error)
|
GetFederation() ([]*x509.Certificate, error)
|
||||||
Version() authority.Version
|
Version() authority.Version
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -34,6 +35,7 @@ import (
|
||||||
"github.com/smallstep/certificates/logging"
|
"github.com/smallstep/certificates/logging"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -920,32 +922,104 @@ func Test_caHandler_Renew(t *testing.T) {
|
||||||
cs := &tls.ConnectionState{
|
cs := &tls.ConnectionState{
|
||||||
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
|
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare root and leaf for renew after expiry test.
|
||||||
|
now := time.Now()
|
||||||
|
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
leafPub, leafPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "Test Root CA"},
|
||||||
|
PublicKey: rootPub,
|
||||||
|
KeyUsage: x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
NotBefore: now.Add(-2 * time.Hour),
|
||||||
|
NotAfter: now.Add(time.Hour),
|
||||||
|
}
|
||||||
|
root, err = x509util.CreateCertificate(root, root, rootPub, rootPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expiredLeaf := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "Leaf certificate"},
|
||||||
|
PublicKey: leafPub,
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||||
|
NotBefore: now.Add(-time.Hour),
|
||||||
|
NotAfter: now.Add(-time.Minute),
|
||||||
|
EmailAddresses: []string{"test@example.org"},
|
||||||
|
}
|
||||||
|
expiredLeaf, err = x509util.CreateCertificate(expiredLeaf, root, leafPub, rootPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate renew after expiry token
|
||||||
|
so := new(jose.SignerOptions)
|
||||||
|
so.WithType("JWT")
|
||||||
|
so.WithHeader("x5cInsecure", []string{base64.StdEncoding.EncodeToString(expiredLeaf.Raw)})
|
||||||
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: leafPriv}, so)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
generateX5cToken := func(claims jose.Claims) string {
|
||||||
|
s, err := jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tls *tls.ConnectionState
|
tls *tls.ConnectionState
|
||||||
|
header http.Header
|
||||||
cert *x509.Certificate
|
cert *x509.Certificate
|
||||||
root *x509.Certificate
|
root *x509.Certificate
|
||||||
err error
|
err error
|
||||||
statusCode int
|
statusCode int
|
||||||
}{
|
}{
|
||||||
{"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
|
{"ok", cs, nil, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
|
||||||
{"no tls", nil, nil, nil, nil, http.StatusBadRequest},
|
{"ok renew after expiry", &tls.ConnectionState{}, http.Header{
|
||||||
{"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest},
|
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
|
||||||
{"renew error", cs, nil, nil, errs.Forbidden("an error"), http.StatusForbidden},
|
NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||||
|
})},
|
||||||
|
}, expiredLeaf, root, nil, http.StatusCreated},
|
||||||
|
{"no tls", nil, nil, nil, nil, nil, http.StatusBadRequest},
|
||||||
|
{"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, nil, http.StatusBadRequest},
|
||||||
|
{"renew error", cs, nil, nil, nil, errs.Forbidden("an error"), http.StatusForbidden},
|
||||||
|
{"fail expired token", &tls.ConnectionState{}, http.Header{
|
||||||
|
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
|
||||||
|
NotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),
|
||||||
|
})},
|
||||||
|
}, expiredLeaf, root, errs.Forbidden("an error"), http.StatusUnauthorized},
|
||||||
|
{"fail invalid root", &tls.ConnectionState{}, http.Header{
|
||||||
|
"Authorization": []string{"Bearer " + generateX5cToken(jose.Claims{
|
||||||
|
NotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),
|
||||||
|
})},
|
||||||
|
}, expiredLeaf, parseCertificate(rootPEM), errs.Forbidden("an error"), http.StatusUnauthorized},
|
||||||
}
|
}
|
||||||
|
|
||||||
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"]}`)
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
h := New(&mockAuthority{
|
h := New(&mockAuthority{
|
||||||
ret1: tt.cert, ret2: tt.root, err: tt.err,
|
ret1: tt.cert, ret2: tt.root, err: tt.err,
|
||||||
|
getRoots: func() ([]*x509.Certificate, error) {
|
||||||
|
return []*x509.Certificate{tt.root}, nil
|
||||||
|
},
|
||||||
getTLSOptions: func() *authority.TLSOptions {
|
getTLSOptions: func() *authority.TLSOptions {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}).(*caHandler)
|
}).(*caHandler)
|
||||||
req := httptest.NewRequest("POST", "http://example.com/renew", nil)
|
req := httptest.NewRequest("POST", "http://example.com/renew", nil)
|
||||||
req.TLS = tt.tls
|
req.TLS = tt.tls
|
||||||
|
req.Header = tt.header
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.Renew(logging.NewResponseLogger(w), req)
|
h.Renew(logging.NewResponseLogger(w), req)
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
|
@ -960,8 +1034,14 @@ func Test_caHandler_Renew(t *testing.T) {
|
||||||
t.Errorf("caHandler.Renew unexpected error = %v", err)
|
t.Errorf("caHandler.Renew unexpected error = %v", err)
|
||||||
}
|
}
|
||||||
if tt.statusCode < http.StatusBadRequest {
|
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`) + `",` +
|
||||||
|
`"certChain":["` +
|
||||||
|
strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.cert.Raw})), "\n", `\n`) + `","` +
|
||||||
|
strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: tt.root.Raw})), "\n", `\n`) + `"]}`)
|
||||||
|
|
||||||
if !bytes.Equal(bytes.TrimSpace(body), expected) {
|
if !bytes.Equal(bytes.TrimSpace(body), expected) {
|
||||||
t.Errorf("caHandler.Root Body = %s, wants %s", body, expected)
|
t.Errorf("caHandler.Root Body = \n%s, wants \n%s", body, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
53
api/renew.go
53
api/renew.go
|
@ -1,20 +1,30 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
authorizationHeader = "Authorization"
|
||||||
|
bearerScheme = "Bearer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Renew uses the information of certificate in the TLS connection to create a
|
// Renew uses the information of certificate in the TLS connection to create a
|
||||||
// new one.
|
// new one.
|
||||||
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
|
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
cert, err := h.getPeerCertificate(r)
|
||||||
WriteError(w, errs.BadRequest("missing client certificate"))
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0])
|
certChain, err := h.Authority.Renew(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
||||||
return
|
return
|
||||||
|
@ -33,3 +43,40 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
|
||||||
TLSOptions: h.Authority.GetTLSOptions(),
|
TLSOptions: h.Authority.GetTLSOptions(),
|
||||||
}, http.StatusCreated)
|
}, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *caHandler) getPeerCertificate(r *http.Request) (*x509.Certificate, error) {
|
||||||
|
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 nil, errs.BadRequest("missing client certificate")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue