acme: add AccountKeyRollover

Add support for AccountKeyRollover. API only returns an error since acme.Error
will contain appropriate KID lookup information. Due to the requirements
of double JWS encoding jwsEncodeJSON is also modified to support a
missing Nonce header and raw string embedding in the payload.

Fixes golang/go#42516

Change-Id: I959660a1a39b2c469b959accd48fda519daf4eb3
GitHub-Last-Rev: 8e8cc5b094743262939c145f56d3a3b57a057d64
GitHub-Pull-Request: golang/crypto#215
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/400274
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
diff --git a/acme/acme.go b/acme/acme.go
index 2c86df3..df57430 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -306,6 +306,20 @@
 	return c.updateRegRFC(ctx, acct)
 }
 
+// AccountKeyRollover attempts to transition a client's account key to a new key.
+// On success client's Key is updated which is not concurrency safe.
+// On failure an error will be returned.
+// The new key is already registered with the ACME provider if the following is true:
+//  - error is of type acme.Error
+//  - StatusCode should be 409 (Conflict)
+//  - Location header will have the KID of the associated account
+//
+// More about account key rollover can be found at
+// https://tools.ietf.org/html/rfc8555#section-7.3.5.
+func (c *Client) AccountKeyRollover(ctx context.Context, newKey crypto.Signer) error {
+	return c.accountKeyRollover(ctx, newKey)
+}
+
 // Authorize performs the initial step in the pre-authorization flow,
 // as opposed to order-based flow.
 // The caller will then need to choose from and perform a set of returned
diff --git a/acme/jws.go b/acme/jws.go
index 403e5b0..b38828d 100644
--- a/acme/jws.go
+++ b/acme/jws.go
@@ -33,6 +33,10 @@
 // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
 const noPayload = ""
 
+// noNonce indicates that the nonce should be omitted from the protected header.
+// See jwsEncodeJSON for details.
+const noNonce = ""
+
 // jsonWebSignature can be easily serialized into a JWS following
 // https://tools.ietf.org/html/rfc7515#section-3.2.
 type jsonWebSignature struct {
@@ -45,10 +49,15 @@
 // The result is serialized in JSON format containing either kid or jwk
 // fields based on the provided KeyID value.
 //
-// If kid is non-empty, its quoted value is inserted in the protected head
+// The claimset is marshalled using json.Marshal unless it is a string.
+// In which case it is inserted directly into the message.
+//
+// If kid is non-empty, its quoted value is inserted in the protected header
 // as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
 // as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
 //
+// If nonce is non-empty, its quoted value is inserted in the protected header.
+//
 // See https://tools.ietf.org/html/rfc7515#section-7.
 func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) {
 	if key == nil {
@@ -58,20 +67,36 @@
 	if alg == "" || !sha.Available() {
 		return nil, ErrUnsupportedKey
 	}
-	var phead string
+	headers := struct {
+		Alg   string          `json:"alg"`
+		KID   string          `json:"kid,omitempty"`
+		JWK   json.RawMessage `json:"jwk,omitempty"`
+		Nonce string          `json:"nonce,omitempty"`
+		URL   string          `json:"url"`
+	}{
+		Alg:   alg,
+		Nonce: nonce,
+		URL:   url,
+	}
 	switch kid {
 	case noKeyID:
 		jwk, err := jwkEncode(key.Public())
 		if err != nil {
 			return nil, err
 		}
-		phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url)
+		headers.JWK = json.RawMessage(jwk)
 	default:
-		phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, kid, nonce, url)
+		headers.KID = string(kid)
 	}
-	phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
+	phJSON, err := json.Marshal(headers)
+	if err != nil {
+		return nil, err
+	}
+	phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON))
 	var payload string
-	if claimset != noPayload {
+	if val, ok := claimset.(string); ok {
+		payload = val
+	} else {
 		cs, err := json.Marshal(claimset)
 		if err != nil {
 			return nil, err
diff --git a/acme/jws_test.go b/acme/jws_test.go
index 738f1ef..d5f00ba 100644
--- a/acme/jws_test.go
+++ b/acme/jws_test.go
@@ -195,6 +195,44 @@
 	}
 }
 
+func TestJWSEncodeNoNonce(t *testing.T) {
+	kid := KeyID("https://example.org/account/1")
+	claims := "RawString"
+	const (
+		// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
+		protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYWNjb3VudC8xIiwidXJsIjoidXJsIn0"
+		// "Raw String"
+		payload = "RawString"
+	)
+
+	b, err := jwsEncodeJSON(claims, testKeyEC, kid, "", "url")
+	if err != nil {
+		t.Fatal(err)
+	}
+	var jws struct{ Protected, Payload, Signature string }
+	if err := json.Unmarshal(b, &jws); err != nil {
+		t.Fatal(err)
+	}
+	if jws.Protected != protected {
+		t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
+	}
+	if jws.Payload != payload {
+		t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
+	}
+
+	sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
+	if err != nil {
+		t.Fatalf("jws.Signature: %v", err)
+	}
+	r, s := big.NewInt(0), big.NewInt(0)
+	r.SetBytes(sig[:len(sig)/2])
+	s.SetBytes(sig[len(sig)/2:])
+	h := sha256.Sum256([]byte(protected + "." + payload))
+	if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
+		t.Error("invalid signature")
+	}
+}
+
 func TestJWSEncodeKID(t *testing.T) {
 	kid := KeyID("https://example.org/account/1")
 	claims := struct{ Msg string }{"Hello JWS"}
diff --git a/acme/rfc8555.go b/acme/rfc8555.go
index 928a5aa..320d83b 100644
--- a/acme/rfc8555.go
+++ b/acme/rfc8555.go
@@ -148,6 +148,42 @@
 	}, nil
 }
 
+// accountKeyRollover attempts to perform account key rollover.
+// On success it will change client.Key to the new key.
+func (c *Client) accountKeyRollover(ctx context.Context, newKey crypto.Signer) error {
+	dir, err := c.Discover(ctx) // Also required by c.accountKID
+	if err != nil {
+		return err
+	}
+	kid := c.accountKID(ctx)
+	if kid == noKeyID {
+		return ErrNoAccount
+	}
+	oldKey, err := jwkEncode(c.Key.Public())
+	if err != nil {
+		return err
+	}
+	payload := struct {
+		Account string          `json:"account"`
+		OldKey  json.RawMessage `json:"oldKey"`
+	}{
+		Account: string(kid),
+		OldKey:  json.RawMessage(oldKey),
+	}
+	inner, err := jwsEncodeJSON(payload, newKey, noKeyID, noNonce, dir.KeyChangeURL)
+	if err != nil {
+		return err
+	}
+
+	res, err := c.post(ctx, nil, dir.KeyChangeURL, base64.RawURLEncoding.EncodeToString(inner), wantStatus(http.StatusOK))
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+	c.Key = newKey
+	return nil
+}
+
 // AuthorizeOrder initiates the order-based application for certificate issuance,
 // as opposed to pre-authorization in Authorize.
 // It is only supported by CAs implementing RFC 8555.
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index 6762f2a..7a53608 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -232,6 +232,7 @@
 				"newOrder": %q,
 				"newAuthz": %q,
 				"revokeCert": %q,
+				"keyChange": %q,
 				"meta": {"termsOfService": %q}
 				}`,
 				s.url("/acme/new-nonce"),
@@ -239,6 +240,7 @@
 				s.url("/acme/new-order"),
 				s.url("/acme/new-authz"),
 				s.url("/acme/revoke-cert"),
+				s.url("/acme/key-change"),
 				s.url("/terms"),
 			)
 			return
@@ -621,6 +623,27 @@
 	}
 }
 
+func TestRFC_AccountKeyRollover(t *testing.T) {
+	s := newACMEServer()
+	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Location", s.url("/accounts/1"))
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{"status": "valid"}`))
+	})
+	s.handle("/acme/key-change", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	if err := cl.AccountKeyRollover(context.Background(), testKeyEC384); err != nil {
+		t.Errorf("AccountKeyRollover: %v, wanted no error", err)
+	} else if cl.Key != testKeyEC384 {
+		t.Error("AccountKeyRollover did not rotate the client key")
+	}
+}
+
 func TestRFC_AuthorizeOrder(t *testing.T) {
 	s := newACMEServer()
 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {