acme: support RFC 8555 account management

Most important change in this CL is that Client is now able
to correctly format and sign requests in KID form with a valid
"kid" value.

According to the RFC, most requests must include KID field
in the protected head of JWS requests. The KID value is the account
identity provided by the CA during registration.

The KID value is also the Account URL. Hence, the CL is tied to
account management.

Updates golang/go#21081

Change-Id: I13f51e1fc52db7596eb933b47fa2014beb93c1ab
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/191602
Run-TryBot: Alex Vaghin <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/acme/acme.go b/acme/acme.go
index 9e28b32..31d07e3 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -119,21 +119,49 @@
 	// identifiable by the server, in case they are causing issues.
 	UserAgent string
 
-	dirMu sync.Mutex // guards writes to dir
-	dir   *Directory // cached result of Client's Discover method
+	cacheMu sync.Mutex
+	dir     *Directory // cached result of Client's Discover method
+	kid     keyID      // cached Account.URI obtained from registerRFC or getAccountRFC
 
 	noncesMu sync.Mutex
 	nonces   map[string]struct{} // nonces collected from previous responses
 }
 
+// accountKID returns a key ID associated with c.Key, the account identity
+// provided by the CA during RFC based registration.
+// It assumes c.Discover has already been called.
+//
+// accountKID requires at most one network roundtrip.
+// It caches only successful result.
+//
+// When in pre-RFC mode or when c.getRegRFC responds with an error, accountKID
+// returns noKeyID.
+func (c *Client) accountKID(ctx context.Context) keyID {
+	c.cacheMu.Lock()
+	defer c.cacheMu.Unlock()
+	if c.dir.OrderURL == "" {
+		// Assume legacy CA.
+		return noKeyID
+	}
+	if c.kid != noKeyID {
+		return c.kid
+	}
+	a, err := c.getRegRFC(ctx)
+	if err != nil {
+		return noKeyID
+	}
+	c.kid = keyID(a.URI)
+	return c.kid
+}
+
 // Discover performs ACME server discovery using c.DirectoryURL.
 //
 // It caches successful result. So, subsequent calls will not result in
 // a network round-trip. This also means mutating c.DirectoryURL after successful call
 // of this method will have no effect.
 func (c *Client) Discover(ctx context.Context) (Directory, error) {
-	c.dirMu.Lock()
-	defer c.dirMu.Unlock()
+	c.cacheMu.Lock()
+	defer c.cacheMu.Unlock()
 	if c.dir != nil {
 		return *c.dir, nil
 	}
@@ -235,7 +263,7 @@
 		req.NotAfter = now.Add(exp).Format(time.RFC3339)
 	}
 
-	res, err := c.post(ctx, c.Key, c.dir.CertURL, req, wantStatus(http.StatusCreated))
+	res, err := c.post(ctx, nil, c.dir.CertURL, req, wantStatus(http.StatusCreated))
 	if err != nil {
 		return nil, "", err
 	}
@@ -289,9 +317,6 @@
 		Cert:     base64.RawURLEncoding.EncodeToString(cert),
 		Reason:   int(reason),
 	}
-	if key == nil {
-		key = c.Key
-	}
 	res, err := c.post(ctx, key, c.dir.RevokeURL, body, wantStatus(http.StatusOK))
 	if err != nil {
 		return err
@@ -304,20 +329,32 @@
 // during account registration. See Register method of Client for more details.
 func AcceptTOS(tosURL string) bool { return true }
 
-// Register creates a new account registration by following the "new-reg" flow.
-// It returns the registered account. The account is not modified.
+// Register creates a new account with the CA using c.Key.
+// It returns the registered account. The account acct is not modified.
 //
 // The registration may require the caller to agree to the CA's Terms of Service (TOS).
 // If so, and the account has not indicated the acceptance of the terms (see Account for details),
 // Register calls prompt with a TOS URL provided by the CA. Prompt should report
 // whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS.
-func (c *Client) Register(ctx context.Context, a *Account, prompt func(tosURL string) bool) (*Account, error) {
-	if _, err := c.Discover(ctx); err != nil {
+//
+// When interfacing with RFC compliant CA, non-RFC8555 compliant fields of acct are ignored
+// and prompt is called if Directory's Terms field is non-zero.
+// Also see Error's Instance field for when a CA requires already registered accounts to agree
+// to an updated Terms of Service.
+func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
+	dir, err := c.Discover(ctx)
+	if err != nil {
 		return nil, err
 	}
 
-	var err error
-	if a, err = c.doReg(ctx, c.dir.RegURL, "new-reg", a); err != nil {
+	// RFC8555 compliant account registration.
+	if dir.OrderURL != "" {
+		return c.registerRFC(ctx, acct, prompt)
+	}
+
+	// Legacy ACME draft registration flow.
+	a, err := c.doReg(ctx, dir.RegURL, "new-reg", acct)
+	if err != nil {
 		return nil, err
 	}
 	var accept bool
@@ -331,9 +368,22 @@
 	return a, err
 }
 
-// GetReg retrieves an existing registration.
-// The url argument is an Account URI.
+// GetReg retrieves an existing account associated with c.Key.
+//
+// The url argument is an Account URI used with pre-RFC8555 CAs.
+// It is ignored when interfacing with an RFC compliant CA.
 func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
+	dir, err := c.Discover(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// Assume RFC8555 compliant CA.
+	if dir.OrderURL != "" {
+		return c.getRegRFC(ctx)
+	}
+
+	// Legacy CA.
 	a, err := c.doReg(ctx, url, "reg", nil)
 	if err != nil {
 		return nil, err
@@ -344,9 +394,23 @@
 
 // UpdateReg updates an existing registration.
 // It returns an updated account copy. The provided account is not modified.
-func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) {
-	uri := a.URI
-	a, err := c.doReg(ctx, uri, "reg", a)
+//
+// When interfacing with RFC compliant CAs, a.URI is ignored and the account URL
+// associated with c.Key is used instead.
+func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) {
+	dir, err := c.Discover(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// Assume RFC8555 compliant CA.
+	if dir.OrderURL != "" {
+		return c.updateRegRFC(ctx, acct)
+	}
+
+	// Legacy CA.
+	uri := acct.URI
+	a, err := c.doReg(ctx, uri, "reg", acct)
 	if err != nil {
 		return nil, err
 	}
@@ -391,7 +455,7 @@
 		Resource:   "new-authz",
 		Identifier: authzID{Type: typ, Value: val},
 	}
-	res, err := c.post(ctx, c.Key, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
+	res, err := c.post(ctx, nil, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
 	if err != nil {
 		return nil, err
 	}
@@ -434,6 +498,11 @@
 //
 // It does not revoke existing certificates.
 func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
+	// Required for c.accountKID() when in RFC mode.
+	if _, err := c.Discover(ctx); err != nil {
+		return err
+	}
+
 	req := struct {
 		Resource string `json:"resource"`
 		Status   string `json:"status"`
@@ -443,7 +512,7 @@
 		Status:   "deactivated",
 		Delete:   true,
 	}
-	res, err := c.post(ctx, c.Key, url, req, wantStatus(http.StatusOK))
+	res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
 	if err != nil {
 		return err
 	}
@@ -520,6 +589,11 @@
 //
 // The server will then perform the validation asynchronously.
 func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) {
+	// Required for c.accountKID() when in RFC mode.
+	if _, err := c.Discover(ctx); err != nil {
+		return nil, err
+	}
+
 	auth, err := keyAuth(c.Key.Public(), chal.Token)
 	if err != nil {
 		return nil, err
@@ -534,7 +608,7 @@
 		Type:     chal.Type,
 		Auth:     auth,
 	}
-	res, err := c.post(ctx, c.Key, chal.URI, req, wantStatus(
+	res, err := c.post(ctx, nil, chal.URI, req, wantStatus(
 		http.StatusOK,       // according to the spec
 		http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md)
 	))
@@ -711,7 +785,7 @@
 		req.Contact = acct.Contact
 		req.Agreement = acct.AgreedTerms
 	}
-	res, err := c.post(ctx, c.Key, url, req, wantStatus(
+	res, err := c.post(ctx, nil, url, req, wantStatus(
 		http.StatusOK,       // updates and deletes
 		http.StatusCreated,  // new account creation
 		http.StatusAccepted, // Let's Encrypt divergent implementation
diff --git a/acme/acme_test.go b/acme/acme_test.go
index 99a4bf8..f171f8d 100644
--- a/acme/acme_test.go
+++ b/acme/acme_test.go
@@ -16,6 +16,7 @@
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"io"
 	"math/big"
 	"net/http"
 	"net/http/httptest"
@@ -28,10 +29,10 @@
 
 // Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided
 // interface.
-func decodeJWSRequest(t *testing.T, v interface{}, r *http.Request) {
+func decodeJWSRequest(t *testing.T, v interface{}, r io.Reader) {
 	// Decode request
 	var req struct{ Payload string }
-	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+	if err := json.NewDecoder(r).Decode(&req); err != nil {
 		t.Fatal(err)
 	}
 	payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
@@ -47,12 +48,14 @@
 type jwsHead struct {
 	Alg   string
 	Nonce string
+	URL   string            `json:"url"`
+	KID   string            `json:"kid"`
 	JWK   map[string]string `json:"jwk"`
 }
 
-func decodeJWSHead(r *http.Request) (*jwsHead, error) {
+func decodeJWSHead(r io.Reader) (*jwsHead, error) {
 	var req struct{ Protected string }
-	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+	if err := json.NewDecoder(r).Decode(&req); err != nil {
 		return nil, err
 	}
 	b, err := base64.RawURLEncoding.DecodeString(req.Protected)
@@ -123,7 +126,7 @@
 			Contact   []string
 			Agreement string
 		}
-		decodeJWSRequest(t, &j, r)
+		decodeJWSRequest(t, &j, r.Body)
 
 		// Test request
 		if j.Resource != "new-reg" {
@@ -193,7 +196,7 @@
 			Contact   []string
 			Agreement string
 		}
-		decodeJWSRequest(t, &j, r)
+		decodeJWSRequest(t, &j, r.Body)
 
 		// Test request
 		if j.Resource != "reg" {
@@ -215,7 +218,11 @@
 	}))
 	defer ts.Close()
 
-	c := Client{Key: testKeyEC}
+	c := Client{
+		Key:          testKeyEC,
+		DirectoryURL: ts.URL,       // don't dial outside of localhost
+		dir:          &Directory{}, // don't do discovery
+	}
 	a := &Account{URI: ts.URL, Contact: contacts, AgreedTerms: terms}
 	var err error
 	if a, err = c.UpdateReg(context.Background(), a); err != nil {
@@ -254,7 +261,7 @@
 			Contact   []string
 			Agreement string
 		}
-		decodeJWSRequest(t, &j, r)
+		decodeJWSRequest(t, &j, r.Body)
 
 		// Test request
 		if j.Resource != "reg" {
@@ -276,7 +283,11 @@
 	}))
 	defer ts.Close()
 
-	c := Client{Key: testKeyEC}
+	c := Client{
+		Key:          testKeyEC,
+		DirectoryURL: ts.URL,       // don't dial outside of localhost
+		dir:          &Directory{}, // don't do discovery
+	}
 	a, err := c.GetReg(context.Background(), ts.URL)
 	if err != nil {
 		t.Fatal(err)
@@ -318,7 +329,7 @@
 						Value string
 					}
 				}
-				decodeJWSRequest(t, &j, r)
+				decodeJWSRequest(t, &j, r.Body)
 
 				// Test request
 				if j.Resource != "new-authz" {
@@ -629,7 +640,7 @@
 				Status   string
 				Delete   bool
 			}
-			decodeJWSRequest(t, &req, r)
+			decodeJWSRequest(t, &req, r.Body)
 			if req.Resource != "authz" {
 				t.Errorf("req.Resource = %q; want authz", req.Resource)
 			}
@@ -644,7 +655,11 @@
 		}
 	}))
 	defer ts.Close()
-	client := &Client{Key: testKey}
+	client := &Client{
+		Key:          testKey,
+		DirectoryURL: ts.URL,       // don't dial outside of localhost
+		dir:          &Directory{}, // don't do discovery
+	}
 	ctx := context.Background()
 	if err := client.RevokeAuthorization(ctx, ts.URL+"/1"); err != nil {
 		t.Errorf("err = %v", err)
@@ -704,7 +719,7 @@
 			Type     string
 			Auth     string `json:"keyAuthorization"`
 		}
-		decodeJWSRequest(t, &j, r)
+		decodeJWSRequest(t, &j, r.Body)
 
 		// Test request
 		if j.Resource != "challenge" {
@@ -730,7 +745,11 @@
 	}))
 	defer ts.Close()
 
-	cl := Client{Key: testKeyEC}
+	cl := Client{
+		Key:          testKeyEC,
+		DirectoryURL: ts.URL,       // don't dial outside of localhost
+		dir:          &Directory{}, // don't do discovery
+	}
 	c, err := cl.Accept(context.Background(), &Challenge{
 		URI:   ts.URL,
 		Token: "token1",
@@ -771,7 +790,7 @@
 			NotBefore string `json:"notBefore,omitempty"`
 			NotAfter  string `json:"notAfter,omitempty"`
 		}
-		decodeJWSRequest(t, &j, r)
+		decodeJWSRequest(t, &j, r.Body)
 
 		// Test request
 		if j.Resource != "new-cert" {
@@ -956,7 +975,7 @@
 			Certificate string
 			Reason      int
 		}
-		decodeJWSRequest(t, &req, r)
+		decodeJWSRequest(t, &req, r.Body)
 		if req.Resource != "revoke-cert" {
 			t.Errorf("req.Resource = %q; want revoke-cert", req.Resource)
 		}
@@ -1117,7 +1136,7 @@
 			w.Write([]byte(`{"status":"valid"}`))
 		}()
 
-		head, err := decodeJWSHead(r)
+		head, err := decodeJWSHead(r.Body)
 		if err != nil {
 			t.Errorf("decodeJWSHead: %v", err)
 			return
diff --git a/acme/http.go b/acme/http.go
index c6535fd..b145292 100644
--- a/acme/http.go
+++ b/acme/http.go
@@ -156,7 +156,7 @@
 }
 
 // post issues a signed POST request in JWS format using the provided key
-// to the specified URL.
+// to the specified URL. If key is nil, c.Key is used instead.
 // It returns a non-error value only when ok reports true.
 //
 // post retries unsuccessful attempts according to c.RetryBackoff
@@ -193,14 +193,28 @@
 }
 
 // postNoRetry signs the body with the given key and POSTs it to the provided url.
-// The body argument must be JSON-serializable.
 // It is used by c.post to retry unsuccessful attempts.
+// The body argument must be JSON-serializable.
+//
+// If key argument is nil, c.Key is used to sign the request.
+// If key argument is nil and c.accountKID returns a non-zero keyID,
+// the request is sent in KID form. Otherwise, JWK form is used.
+//
+// In practice, when interfacing with RFC compliant CAs most requests are sent in KID form
+// and JWK is used only when KID is unavailable: new account endpoint and certificate
+// revocation requests authenticated by a cert key.
+// See jwsEncodeJSON for other details.
 func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
+	kid := noKeyID
+	if key == nil {
+		key = c.Key
+		kid = c.accountKID(ctx)
+	}
 	nonce, err := c.popNonce(ctx, url)
 	if err != nil {
 		return nil, nil, err
 	}
-	b, err := jwsEncodeJSON(body, key, noKeyID, nonce, url)
+	b, err := jwsEncodeJSON(body, key, kid, nonce, url)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/acme/http_test.go b/acme/http_test.go
index a5dc35f..79095cc 100644
--- a/acme/http_test.go
+++ b/acme/http_test.go
@@ -87,7 +87,7 @@
 			return
 		}
 
-		head, err := decodeJWSHead(r)
+		head, err := decodeJWSHead(r.Body)
 		switch {
 		case err != nil:
 			t.Errorf("decodeJWSHead: %v", err)
diff --git a/acme/jws.go b/acme/jws.go
index f26bd5b..f8bc2c4 100644
--- a/acme/jws.go
+++ b/acme/jws.go
@@ -30,7 +30,7 @@
 //
 // If kid is non-empty, its quoted value is inserted in the protected head
 // as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
-// as "jwk" field value.
+// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
 //
 // See https://tools.ietf.org/html/rfc7515#section-7.
 func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
diff --git a/acme/rfc8555.go b/acme/rfc8555.go
new file mode 100644
index 0000000..51839a0
--- /dev/null
+++ b/acme/rfc8555.go
@@ -0,0 +1,122 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package acme
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// DeactivateReg permanently disables an existing account associated with c.Key.
+// A deactivated account can no longer request certificate issuance or access
+// resources related to the account, such as orders or authorizations.
+//
+// It works only with RFC8555 compliant CAs.
+func (c *Client) DeactivateReg(ctx context.Context) error {
+	url := string(c.accountKID(ctx))
+	if url == "" {
+		return ErrNoAccount
+	}
+	req := json.RawMessage(`{"status": "deactivated"}`)
+	res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
+	if err != nil {
+		return err
+	}
+	res.Body.Close()
+	return nil
+}
+
+// registerRFC is quivalent to c.Register but for RFC-compliant CAs.
+// It expects c.Discover to have already been called.
+// TODO: Implement externalAccountBinding.
+func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
+	c.cacheMu.Lock() // guard c.kid access
+	defer c.cacheMu.Unlock()
+
+	req := struct {
+		TermsAgreed bool     `json:"termsOfServiceAgreed,omitempty"`
+		Contact     []string `json:"contact,omitempty"`
+	}{
+		Contact: acct.Contact,
+	}
+	if c.dir.Terms != "" {
+		req.TermsAgreed = prompt(c.dir.Terms)
+	}
+	res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(
+		http.StatusOK,      // account with this key already registered
+		http.StatusCreated, // new account created
+	))
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	a, err := responseAccount(res)
+	if err != nil {
+		return nil, err
+	}
+	// Cache Account URL even if we return an error to the caller.
+	// It is by all means a valid and usable "kid" value for future requests.
+	c.kid = keyID(a.URI)
+	if res.StatusCode == http.StatusOK {
+		return nil, ErrAccountAlreadyExists
+	}
+	return a, nil
+}
+
+// updateGegRFC is equivalent to c.UpdateReg but for RFC-compliant CAs.
+// It expects c.Discover to have already been called.
+func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) {
+	url := string(c.accountKID(ctx))
+	if url == "" {
+		return nil, ErrNoAccount
+	}
+	req := struct {
+		Contact []string `json:"contact,omitempty"`
+	}{
+		Contact: a.Contact,
+	}
+	res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	return responseAccount(res)
+}
+
+// getGegRFC is equivalent to c.GetReg but for RFC-compliant CAs.
+// It expects c.Discover to have already been called.
+func (c *Client) getRegRFC(ctx context.Context) (*Account, error) {
+	req := json.RawMessage(`{"onlyReturnExisting": true}`)
+	res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(http.StatusOK))
+	if e, ok := err.(*Error); ok && e.ProblemType == "urn:ietf:params:acme:error:accountDoesNotExist" {
+		return nil, ErrNoAccount
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	return responseAccount(res)
+}
+
+func responseAccount(res *http.Response) (*Account, error) {
+	var v struct {
+		Status  string
+		Contact []string
+		Orders  string
+	}
+	if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
+		return nil, fmt.Errorf("acme: invalid response: %v", err)
+	}
+	return &Account{
+		URI:       res.Header.Get("Location"),
+		Status:    v.Status,
+		Contact:   v.Contact,
+		OrdersURL: v.Orders,
+	}, nil
+}
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index 4a8d9f5..c366380 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -5,11 +5,17 @@
 package acme
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"reflect"
+	"sync"
 	"testing"
+	"time"
 )
 
 // While contents of this file is pertinent only to RFC8555,
@@ -121,3 +127,350 @@
 		t.Error("last cl.popNonce returned nil error")
 	}
 }
+
+func TestRFC_postKID(t *testing.T) {
+	var ts *httptest.Server
+	ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case "/new-nonce":
+			w.Header().Set("Replay-Nonce", "nonce")
+		case "/new-account":
+			w.Header().Set("Location", "/account-1")
+			w.Write([]byte(`{"status":"valid"}`))
+		case "/post":
+			b, _ := ioutil.ReadAll(r.Body) // check err later in decodeJWSxxx
+			head, err := decodeJWSHead(bytes.NewReader(b))
+			if err != nil {
+				t.Errorf("decodeJWSHead: %v", err)
+				return
+			}
+			if head.KID != "/account-1" {
+				t.Errorf("head.KID = %q; want /account-1", head.KID)
+			}
+			if len(head.JWK) != 0 {
+				t.Errorf("head.JWK = %q; want zero map", head.JWK)
+			}
+			if v := ts.URL + "/post"; head.URL != v {
+				t.Errorf("head.URL = %q; want %q", head.URL, v)
+			}
+
+			var payload struct{ Msg string }
+			decodeJWSRequest(t, &payload, bytes.NewReader(b))
+			if payload.Msg != "ping" {
+				t.Errorf("payload.Msg = %q; want ping", payload.Msg)
+			}
+			w.Write([]byte("pong"))
+		default:
+			t.Errorf("unhandled %s %s", r.Method, r.URL)
+			w.WriteHeader(http.StatusBadRequest)
+		}
+	}))
+	defer ts.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	cl := &Client{
+		Key:          testKey,
+		DirectoryURL: ts.URL,
+		dir: &Directory{
+			NonceURL: ts.URL + "/new-nonce",
+			RegURL:   ts.URL + "/new-account",
+			OrderURL: "/force-rfc-mode",
+		},
+	}
+	req := json.RawMessage(`{"msg":"ping"}`)
+	res, err := cl.post(ctx, nil /* use kid */, ts.URL+"/post", req, wantStatus(http.StatusOK))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer res.Body.Close()
+	b, _ := ioutil.ReadAll(res.Body) // don't care about err - just checking b
+	if string(b) != "pong" {
+		t.Errorf("res.Body = %q; want pong", b)
+	}
+}
+
+// acmeServer simulates a subset of RFC8555 compliant CA.
+//
+// TODO: We also have x/crypto/acme/autocert/acmetest and startACMEServerStub in autocert_test.go.
+// It feels like this acmeServer is a sweet spot between usefulness and added complexity.
+// Also, acmetest and startACMEServerStub were both written for draft-02, no RFC support.
+// The goal is to consolidate all into one ACME test server.
+type acmeServer struct {
+	ts      *httptest.Server
+	handler map[string]http.HandlerFunc // keyed by r.URL.Path
+
+	mu     sync.Mutex
+	nnonce int
+}
+
+func newACMEServer() *acmeServer {
+	return &acmeServer{handler: make(map[string]http.HandlerFunc)}
+}
+
+func (s *acmeServer) handle(path string, f func(http.ResponseWriter, *http.Request)) {
+	s.handler[path] = http.HandlerFunc(f)
+}
+
+func (s *acmeServer) start() {
+	s.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+
+		// Directory request.
+		if r.URL.Path == "/" {
+			fmt.Fprintf(w, `{
+				"newNonce": %q,
+				"newAccount": %q,
+				"newOrder": %q,
+				"newAuthz": %q,
+				"revokeCert": %q,
+				"meta": {"termsOfService": %q}
+				}`,
+				s.url("/acme/new-nonce"),
+				s.url("/acme/new-account"),
+				s.url("/acme/new-order"),
+				s.url("/acme/new-authz"),
+				s.url("/acme/revoke-cert"),
+				s.url("/terms"),
+			)
+			return
+		}
+
+		// All other responses contain a nonce value unconditionally.
+		w.Header().Set("Replay-Nonce", s.nonce())
+		if r.URL.Path == "/acme/new-nonce" {
+			return
+		}
+
+		h := s.handler[r.URL.Path]
+		if h == nil {
+			w.WriteHeader(http.StatusBadRequest)
+			fmt.Fprintf(w, "Unhandled %s", r.URL.Path)
+			return
+		}
+		h.ServeHTTP(w, r)
+	}))
+}
+
+func (s *acmeServer) close() {
+	s.ts.Close()
+}
+
+func (s *acmeServer) url(path string) string {
+	return s.ts.URL + path
+}
+
+func (s *acmeServer) nonce() string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.nnonce++
+	return fmt.Sprintf("nonce%d", s.nnonce)
+}
+
+func (s *acmeServer) error(w http.ResponseWriter, e *wireError) {
+	w.WriteHeader(e.Status)
+	json.NewEncoder(w).Encode(e)
+}
+
+func TestRFC_Register(t *testing.T) {
+	const email = "mailto:user@example.org"
+
+	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.StatusCreated) // 201 means new account created
+		fmt.Fprintf(w, `{
+			"status": "valid",
+			"contact": [%q],
+			"orders": %q
+		}`, email, s.url("/accounts/1/orders"))
+
+		b, _ := ioutil.ReadAll(r.Body) // check err later in decodeJWSxxx
+		head, err := decodeJWSHead(bytes.NewReader(b))
+		if err != nil {
+			t.Errorf("decodeJWSHead: %v", err)
+			return
+		}
+		if len(head.JWK) == 0 {
+			t.Error("head.JWK is empty")
+		}
+
+		var req struct{ Contact []string }
+		decodeJWSRequest(t, &req, bytes.NewReader(b))
+		if len(req.Contact) != 1 || req.Contact[0] != email {
+			t.Errorf("req.Contact = %q; want [%q]", req.Contact, email)
+		}
+	})
+	s.start()
+	defer s.close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	cl := &Client{
+		Key:          testKeyEC,
+		DirectoryURL: s.url("/"),
+	}
+
+	var didPrompt bool
+	a := &Account{Contact: []string{email}}
+	acct, err := cl.Register(ctx, a, func(tos string) bool {
+		didPrompt = true
+		terms := s.url("/terms")
+		if tos != terms {
+			t.Errorf("tos = %q; want %q", tos, terms)
+		}
+		return true
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	okAccount := &Account{
+		URI:       s.url("/accounts/1"),
+		Status:    StatusValid,
+		Contact:   []string{email},
+		OrdersURL: s.url("/accounts/1/orders"),
+	}
+	if !reflect.DeepEqual(acct, okAccount) {
+		t.Errorf("acct = %+v; want %+v", acct, okAccount)
+	}
+	if !didPrompt {
+		t.Error("tos prompt wasn't called")
+	}
+	if v := cl.accountKID(ctx); v != keyID(okAccount.URI) {
+		t.Errorf("account kid = %q; want %q", v, okAccount.URI)
+	}
+}
+
+func TestRFC_RegisterExisting(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) // 200 means account already exists
+		w.Write([]byte(`{"status": "valid"}`))
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	_, err := cl.Register(context.Background(), &Account{}, AcceptTOS)
+	if err != ErrAccountAlreadyExists {
+		t.Errorf("err = %v; want %v", err, ErrAccountAlreadyExists)
+	}
+	kid := keyID(s.url("/accounts/1"))
+	if v := cl.accountKID(context.Background()); v != kid {
+		t.Errorf("account kid = %q; want %q", v, kid)
+	}
+}
+
+func TestRFC_UpdateReg(t *testing.T) {
+	const email = "mailto:user@example.org"
+
+	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"}`))
+	})
+	var didUpdate bool
+	s.handle("/accounts/1", func(w http.ResponseWriter, r *http.Request) {
+		didUpdate = true
+		w.Header().Set("Location", s.url("/accounts/1"))
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{"status": "valid"}`))
+
+		b, _ := ioutil.ReadAll(r.Body) // check err later in decodeJWSxxx
+		head, err := decodeJWSHead(bytes.NewReader(b))
+		if err != nil {
+			t.Errorf("decodeJWSHead: %v", err)
+			return
+		}
+		if len(head.JWK) != 0 {
+			t.Error("head.JWK is non-zero")
+		}
+		kid := s.url("/accounts/1")
+		if head.KID != kid {
+			t.Errorf("head.KID = %q; want %q", head.KID, kid)
+		}
+
+		var req struct{ Contact []string }
+		decodeJWSRequest(t, &req, bytes.NewReader(b))
+		if len(req.Contact) != 1 || req.Contact[0] != email {
+			t.Errorf("req.Contact = %q; want [%q]", req.Contact, email)
+		}
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	_, err := cl.UpdateReg(context.Background(), &Account{Contact: []string{email}})
+	if err != nil {
+		t.Error(err)
+	}
+	if !didUpdate {
+		t.Error("UpdateReg didn't update the account")
+	}
+}
+
+func TestRFC_GetReg(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"}`))
+
+		head, err := decodeJWSHead(r.Body)
+		if err != nil {
+			t.Errorf("decodeJWSHead: %v", err)
+			return
+		}
+		if len(head.JWK) == 0 {
+			t.Error("head.JWK is empty")
+		}
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	acct, err := cl.GetReg(context.Background(), "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	okAccount := &Account{
+		URI:    s.url("/accounts/1"),
+		Status: StatusValid,
+	}
+	if !reflect.DeepEqual(acct, okAccount) {
+		t.Errorf("acct = %+v; want %+v", acct, okAccount)
+	}
+}
+
+func TestRFC_GetRegNoAccount(t *testing.T) {
+	s := newACMEServer()
+	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
+		s.error(w, &wireError{
+			Status: http.StatusBadRequest,
+			Type:   "urn:ietf:params:acme:error:accountDoesNotExist",
+		})
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	if _, err := cl.GetReg(context.Background(), ""); err != ErrNoAccount {
+		t.Errorf("err = %v; want %v", err, ErrNoAccount)
+	}
+}
+
+func TestRFC_GetRegOtherError(t *testing.T) {
+	s := newACMEServer()
+	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusBadRequest)
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	if _, err := cl.GetReg(context.Background(), ""); err == nil || err == ErrNoAccount {
+		t.Errorf("GetReg: %v; want any other non-nil err", err)
+	}
+}
diff --git a/acme/types.go b/acme/types.go
index a411487..4432afb 100644
--- a/acme/types.go
+++ b/acme/types.go
@@ -16,12 +16,13 @@
 
 // ACME server response statuses used to describe Authorization and Challenge states.
 const (
-	StatusUnknown    = "unknown"
-	StatusPending    = "pending"
-	StatusProcessing = "processing"
-	StatusValid      = "valid"
-	StatusInvalid    = "invalid"
-	StatusRevoked    = "revoked"
+	StatusDeactivated = "deactivated"
+	StatusInvalid     = "invalid"
+	StatusPending     = "pending"
+	StatusProcessing  = "processing"
+	StatusRevoked     = "revoked"
+	StatusUnknown     = "unknown"
+	StatusValid       = "valid"
 )
 
 // CRLReasonCode identifies the reason for a certificate revocation.
@@ -41,8 +42,17 @@
 	CRLReasonAACompromise         CRLReasonCode = 10
 )
 
-// ErrUnsupportedKey is returned when an unsupported key type is encountered.
-var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
+var (
+	// ErrUnsupportedKey is returned when an unsupported key type is encountered.
+	ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
+
+	// ErrAccountAlreadyExists indicates that the Client's key has already been registered
+	// with the CA. It is returned by Register method.
+	ErrAccountAlreadyExists = errors.New("acme: account already exists")
+
+	// ErrNoAccount indicates that the Client's key has not been registered with the CA.
+	ErrNoAccount = errors.New("acme: account does not exist")
+)
 
 // Error is an ACME error, defined in Problem Details for HTTP APIs doc
 // http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
@@ -54,6 +64,12 @@
 	ProblemType string
 	// Detail is a human-readable explanation specific to this occurrence of the problem.
 	Detail string
+	// Instance indicates a URL that the client should direct a human user to visit
+	// in order for instructions on how to agree to the updated Terms of Service.
+	// In such an event CA sets StatusCode to 403, ProblemType to
+	// "urn:ietf:params:acme:error:userActionRequired" and a Link header with relation
+	// "terms-of-service" containing the latest TOS URL.
+	Instance string
 	// Header is the original server error response headers.
 	// It may be nil.
 	Header http.Header
@@ -108,31 +124,57 @@
 }
 
 // Account is a user account. It is associated with a private key.
+// Non-RFC8555 fields are empty when interfacing with a compliant CA.
 type Account struct {
 	// URI is the account unique ID, which is also a URL used to retrieve
 	// account data from the CA.
+	// When interfacing with RFC8555-compliant CAs, URI is the "kid" field
+	// value in JWS signed requests.
 	URI string
 
 	// Contact is a slice of contact info used during registration.
+	// See https://tools.ietf.org/html/rfc8555#section-7.3 for supported
+	// formats.
 	Contact []string
 
+	// Status indicates current account status as returned by the CA.
+	// Possible values are "valid", "deactivated", and "revoked".
+	Status string
+
+	// OrdersURL is a URL from which a list of orders submitted by this account
+	// can be fetched.
+	OrdersURL string
+
 	// The terms user has agreed to.
 	// A value not matching CurrentTerms indicates that the user hasn't agreed
 	// to the actual Terms of Service of the CA.
+	//
+	// It is non-RFC8555 compliant. Package users can store the ToS they agree to
+	// during Client's Register call in the prompt callback function.
 	AgreedTerms string
 
 	// Actual terms of a CA.
+	//
+	// It is non-RFC8555 compliant. Use Directory's Terms field.
+	// When a CA updates their terms and requires an account agreement,
+	// a URL at which instructions to do so is available in Error's Instance field.
 	CurrentTerms string
 
 	// Authz is the authorization URL used to initiate a new authz flow.
+	//
+	// It is non-RFC8555 compliant. Use Directory's AuthzURL or OrderURL.
 	Authz string
 
 	// Authorizations is a URI from which a list of authorizations
 	// granted to this account can be fetched via a GET request.
+	//
+	// It is non-RFC8555 compliant and is obsoleted by OrdersURL.
 	Authorizations string
 
 	// Certificates is a URI from which a list of certificates
 	// issued for this account can be fetched via a GET request.
+	//
+	// It is non-RFC8555 compliant and is obsoleted by OrdersURL.
 	Certificates string
 }
 
@@ -142,8 +184,8 @@
 	// NonceURL indicates an endpoint where to fetch fresh nonce values from.
 	NonceURL string
 
-	// RegURL is an account endpoint URL, allowing for creating new
-	// and modifying existing accounts.
+	// RegURL is an account endpoint URL, allowing for creating new accounts.
+	// Pre-RFC8555 CAs also allow modifying existing accounts at this URL.
 	RegURL string
 
 	// OrderURL is used to initiate the certificate issuance flow
@@ -299,9 +341,10 @@
 // wireError is a subset of fields of the Problem Details object
 // as described in https://tools.ietf.org/html/rfc7807#section-3.1.
 type wireError struct {
-	Status int
-	Type   string
-	Detail string
+	Status   int
+	Type     string
+	Detail   string
+	Instance string
 }
 
 func (e *wireError) error(h http.Header) *Error {
@@ -309,6 +352,7 @@
 		StatusCode:  e.Status,
 		ProblemType: e.Type,
 		Detail:      e.Detail,
+		Instance:    e.Instance,
 		Header:      h,
 	}
 }