forked from TrueCloudLab/certificates
Merge pull request #307 from dharanikumar-s/master
Add support for rekeying Fixes #292
This commit is contained in:
commit
ea9bc493b8
5 changed files with 184 additions and 17 deletions
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
|
@ -35,6 +36,7 @@ type Authority interface {
|
|||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
||||
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
LoadProvisionerByID(string) (provisioner.Interface, error)
|
||||
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
||||
|
@ -249,6 +251,7 @@ func (h *caHandler) Route(r Router) {
|
|||
r.MethodFunc("GET", "/root/{sha}", h.Root)
|
||||
r.MethodFunc("POST", "/sign", h.Sign)
|
||||
r.MethodFunc("POST", "/renew", h.Renew)
|
||||
r.MethodFunc("POST", "/rekey", h.Rekey)
|
||||
r.MethodFunc("POST", "/revoke", h.Revoke)
|
||||
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
|
@ -551,6 +552,7 @@ type mockAuthority struct {
|
|||
root func(shasum string) (*x509.Certificate, error)
|
||||
sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
||||
renewOrRekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||
loadProvisionerByID func(provID string) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
|
@ -611,6 +613,13 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, erro
|
|||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
if m.renewOrRekey != nil {
|
||||
return m.renewOrRekey(oldcert, pk)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {
|
||||
if m.getProvisioners != nil {
|
||||
return m.getProvisioners(nextCursor, limit)
|
||||
|
@ -952,6 +961,67 @@ func Test_caHandler_Renew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_caHandler_Rekey(t *testing.T) {
|
||||
cs := &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
|
||||
}
|
||||
csr := parseCertificateRequest(csrPEM)
|
||||
valid, err := json.Marshal(RekeyRequest{
|
||||
CsrPEM: CertificateRequest{csr},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
tls *tls.ConnectionState
|
||||
cert *x509.Certificate
|
||||
root *x509.Certificate
|
||||
err error
|
||||
statusCode int
|
||||
}{
|
||||
{"ok", string(valid), cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
|
||||
{"no tls", string(valid), nil, nil, nil, nil, http.StatusBadRequest},
|
||||
{"no peer certificates", string(valid), &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest},
|
||||
{"rekey error", string(valid), cs, nil, nil, errs.Forbidden("an error"), http.StatusForbidden},
|
||||
{"json read error", "{", cs, nil, nil, nil, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := New(&mockAuthority{
|
||||
ret1: tt.cert, ret2: tt.root, err: tt.err,
|
||||
getTLSOptions: func() *tlsutil.TLSOptions {
|
||||
return nil
|
||||
},
|
||||
}).(*caHandler)
|
||||
req := httptest.NewRequest("POST", "http://example.com/rekey", strings.NewReader(tt.input))
|
||||
req.TLS = tt.tls
|
||||
w := httptest.NewRecorder()
|
||||
h.Rekey(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.Rekey StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.Rekey unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), expected) {
|
||||
t.Errorf("caHandler.Rekey Body = %s, wants %s", body, expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_caHandler_Provisioners(t *testing.T) {
|
||||
type fields struct {
|
||||
Authority Authority
|
||||
|
|
64
api/rekey.go
Normal file
64
api/rekey.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// RekeyRequest is the request body for a certificate rekey request.
|
||||
type RekeyRequest struct {
|
||||
CsrPEM CertificateRequest `json:"csr"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the RekeyRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (s *RekeyRequest) Validate() error {
|
||||
if s.CsrPEM.CertificateRequest == nil {
|
||||
return errs.BadRequest("missing csr")
|
||||
}
|
||||
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
|
||||
return errs.Wrap(http.StatusBadRequest, err, "invalid csr")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rekey is similar to renew except that the certificate will be renewed with new key from csr.
|
||||
func (h *caHandler) Rekey(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
WriteError(w, errs.BadRequest("missing peer certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
var body RekeyRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
certChain, err := h.Authority.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey)
|
||||
if err != nil {
|
||||
WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey"))
|
||||
return
|
||||
}
|
||||
certChainPEM := certChainToPEM(certChain)
|
||||
var caPEM Certificate
|
||||
if len(certChainPEM) > 1 {
|
||||
caPEM = certChainPEM[1]
|
||||
}
|
||||
|
||||
logCertificate(w, certChain[0])
|
||||
JSONStatus(w, &SignResponse{
|
||||
ServerPEM: certChainPEM[0],
|
||||
CaPEM: caPEM,
|
||||
CertChainPEM: certChainPEM,
|
||||
TLSOptions: h.Authority.GetTLSOptions(),
|
||||
}, http.StatusCreated)
|
||||
}
|
|
@ -2,6 +2,7 @@ package authority
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
|
@ -27,6 +28,7 @@ func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions {
|
|||
}
|
||||
|
||||
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
||||
var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
||||
|
||||
func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
|
||||
return func(p x509util.Profile) error {
|
||||
|
@ -135,11 +137,16 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
|
|||
// Renew creates a new Certificate identical to the old certificate, except
|
||||
// with a validity window that begins 'now'.
|
||||
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
|
||||
return a.Rekey(oldCert, nil)
|
||||
}
|
||||
|
||||
// Func is used for renewing or rekeying based on the public key passed.
|
||||
func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
opts := []interface{}{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())}
|
||||
|
||||
// Check step provisioner extensions
|
||||
if err := a.authorizeRenew(oldCert); err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...)
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
|
||||
}
|
||||
|
||||
// Durations
|
||||
|
@ -148,7 +155,6 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
|
|||
now := time.Now().UTC()
|
||||
|
||||
newCert := &x509.Certificate{
|
||||
PublicKey: oldCert.PublicKey,
|
||||
Issuer: a.x509Issuer.Subject,
|
||||
Subject: oldCert.Subject,
|
||||
NotBefore: now.Add(-1 * backdate),
|
||||
|
@ -180,34 +186,47 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
|
|||
PolicyIdentifiers: oldCert.PolicyIdentifiers,
|
||||
}
|
||||
|
||||
// Copy all extensions except for Authority Key Identifier. This one might
|
||||
// be different if we rotate the intermediate certificate and it will cause
|
||||
// a TLS bad certificate error.
|
||||
if pk == nil {
|
||||
newCert.PublicKey = oldCert.PublicKey
|
||||
} else {
|
||||
newCert.PublicKey = pk
|
||||
}
|
||||
|
||||
// Copy all extensions except:
|
||||
// 1. Authority Key Identifier - This one might be different if we rotate the intermediate certificate
|
||||
// and it will cause a TLS bad certificate error.
|
||||
// 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier extension will be calculated
|
||||
// for the new public key by NewLeafProfilewithTemplate()
|
||||
for _, ext := range oldCert.Extensions {
|
||||
if !ext.Id.Equal(oidAuthorityKeyIdentifier) {
|
||||
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
|
||||
if ext.Id.Equal(oidAuthorityKeyIdentifier) {
|
||||
continue
|
||||
}
|
||||
if ext.Id.Equal(oidSubjectKeyIdentifier) && (pk != nil) {
|
||||
newCert.SubjectKeyId = nil
|
||||
continue
|
||||
}
|
||||
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
|
||||
}
|
||||
|
||||
leaf, err := x509util.NewLeafProfileWithTemplate(newCert, a.x509Issuer, a.x509Signer)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...)
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
|
||||
}
|
||||
crtBytes, err := leaf.CreateCertificate()
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err,
|
||||
"authority.Renew; error renewing certificate from existing server certificate", opts...)
|
||||
"authority.Rekey; error renewing certificate from existing server certificate", opts...)
|
||||
}
|
||||
|
||||
serverCert, err := x509.ParseCertificate(crtBytes)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err,
|
||||
"authority.Renew; error parsing new server certificate", opts...)
|
||||
"authority.Rekey; error parsing new server certificate", opts...)
|
||||
}
|
||||
|
||||
if err = a.db.StoreCertificate(serverCert); err != nil {
|
||||
if err != db.ErrNotImplemented {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew; error storing certificate in db", opts...)
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey; error storing certificate in db", opts...)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -397,9 +397,11 @@ ZYtQ9Ot36qc=
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthority_Renew(t *testing.T) {
|
||||
func TestAuthority_Rekey(t *testing.T) {
|
||||
pub, _, err := keys.GenerateDefaultKeyPair()
|
||||
assert.FatalError(t, err)
|
||||
pub1, _, err := keys.GenerateDefaultKeyPair()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
a := testAuthority(t)
|
||||
a.config.AuthorityConfig.Template = &x509util.ASN1DN{
|
||||
|
@ -455,14 +457,14 @@ func TestAuthority_Renew(t *testing.T) {
|
|||
return &renewTest{
|
||||
auth: _a,
|
||||
cert: cert,
|
||||
err: errors.New("authority.Renew; error renewing certificate from existing server certificate"),
|
||||
err: errors.New("authority.Rekey; error renewing certificate from existing server certificate"),
|
||||
code: http.StatusInternalServerError,
|
||||
}, nil
|
||||
},
|
||||
"fail-unauthorized": func() (*renewTest, error) {
|
||||
return &renewTest{
|
||||
cert: certNoRenew,
|
||||
err: errors.New("authority.Renew: authority.authorizeRenew: jwk.AuthorizeRenew; renew is disabled for jwk provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
|
||||
err: errors.New("authority.Rekey: authority.authorizeRenew: jwk.AuthorizeRenew; renew is disabled for jwk provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
|
||||
code: http.StatusUnauthorized,
|
||||
}, nil
|
||||
},
|
||||
|
@ -505,9 +507,9 @@ func TestAuthority_Renew(t *testing.T) {
|
|||
|
||||
var certChain []*x509.Certificate
|
||||
if tc.auth != nil {
|
||||
certChain, err = tc.auth.Renew(tc.cert)
|
||||
certChain, err = tc.auth.Rekey(tc.cert, pub1)
|
||||
} else {
|
||||
certChain, err = a.Renew(tc.cert)
|
||||
certChain, err = a.Rekey(tc.cert, pub1)
|
||||
}
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
||||
|
@ -551,8 +553,9 @@ func TestAuthority_Renew(t *testing.T) {
|
|||
assert.Equals(t, leaf.ExtKeyUsage,
|
||||
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
|
||||
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
|
||||
assert.Equals(t, leaf.PublicKey, pub1)
|
||||
|
||||
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
||||
pubBytes, err := x509.MarshalPKIXPublicKey(pub1)
|
||||
assert.FatalError(t, err)
|
||||
hash := sha1.Sum(pubBytes)
|
||||
assert.Equals(t, leaf.SubjectKeyId, hash[:])
|
||||
|
@ -562,6 +565,10 @@ func TestAuthority_Renew(t *testing.T) {
|
|||
assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId)
|
||||
// Compare extensions: they can be in a different order
|
||||
for _, ext1 := range tc.cert.Extensions {
|
||||
//skip SubjectKeyIdentifier
|
||||
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, ext2 := range leaf.Extensions {
|
||||
if reflect.DeepEqual(ext1, ext2) {
|
||||
|
@ -578,6 +585,10 @@ func TestAuthority_Renew(t *testing.T) {
|
|||
assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId)
|
||||
// Compare extensions: they can be in a different order
|
||||
for _, ext1 := range tc.cert.Extensions {
|
||||
//skip SubjectKeyIdentifier
|
||||
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
||||
continue
|
||||
}
|
||||
// The authority key id extension should be different b/c the intermediates are different.
|
||||
if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
|
||||
for _, ext2 := range leaf.Extensions {
|
||||
|
|
Loading…
Reference in a new issue