diff --git a/api/api.go b/api/api.go index 37222be8..f83a2354 100644 --- a/api/api.go +++ b/api/api.go @@ -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) diff --git a/api/api_test.go b/api/api_test.go index edefbd47..27aec1b1 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -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 diff --git a/api/rekey.go b/api/rekey.go new file mode 100644 index 00000000..2d24dbb8 --- /dev/null +++ b/api/rekey.go @@ -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) +} diff --git a/authority/tls.go b/authority/tls.go index ef5aa49f..af2e8b41 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -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...) } } diff --git a/authority/tls_test.go b/authority/tls_test.go index 807f970d..59d5ec03 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -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 {