acme: DeactivateReg fix panic

Currently discover is not called which results in a panic if just a key
is added to an ACME client and then deactivation is attempted.
This patch adds a discover call as well as missing unit tests for the
API.

Change-Id: I0719e5376eb2fccf62182e5f91e5b5eaa7bdd518
GitHub-Last-Rev: 501d7c6c1b75a3069dcad4254b4d4a0d2ccb02c8
GitHub-Pull-Request: golang/crypto#217
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/406734
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
diff --git a/acme/rfc8555.go b/acme/rfc8555.go
index 320d83b..940e70b 100644
--- a/acme/rfc8555.go
+++ b/acme/rfc8555.go
@@ -24,6 +24,9 @@
 //
 // It only works with CAs implementing RFC 8555.
 func (c *Client) DeactivateReg(ctx context.Context) error {
+	if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
+		return err
+	}
 	url := string(c.accountKID(ctx))
 	if url == "" {
 		return ErrNoAccount
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index 7a53608..6ea77df 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -15,12 +15,14 @@
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math/big"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
+	"strings"
 	"sync"
 	"testing"
 	"time"
@@ -644,6 +646,107 @@
 	}
 }
 
+func TestRFC_DeactivateReg(t *testing.T) {
+	const email = "mailto:user@example.org"
+	curStatus := StatusValid
+
+	type account struct {
+		Status    string   `json:"status"`
+		Contact   []string `json:"contact"`
+		AcceptTOS bool     `json:"termsOfServiceAgreed"`
+		Orders    string   `json:"orders"`
+	}
+
+	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 existing account
+		json.NewEncoder(w).Encode(account{
+			Status:    curStatus,
+			Contact:   []string{email},
+			AcceptTOS: true,
+			Orders:    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 {
+			Status       string   `json:"status"`
+			Contact      []string `json:"contact"`
+			AcceptTOS    bool     `json:"termsOfServiceAgreed"`
+			OnlyExisting bool     `json:"onlyReturnExisting"`
+		}
+		decodeJWSRequest(t, &req, bytes.NewReader(b))
+		if !req.OnlyExisting {
+			t.Errorf("req.OnlyReturnExisting = %t; want = %t", req.OnlyExisting, true)
+		}
+	})
+	s.handle("/accounts/1", func(w http.ResponseWriter, r *http.Request) {
+		if curStatus == StatusValid {
+			curStatus = StatusDeactivated
+			w.WriteHeader(http.StatusOK)
+		} else {
+			s.error(w, &wireError{
+				Status: http.StatusUnauthorized,
+				Type:   "urn:ietf:params:acme:error:unauthorized",
+			})
+		}
+		var req account
+		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 not empty")
+		}
+		if !strings.HasSuffix(head.KID, "/accounts/1") {
+			t.Errorf("head.KID = %q; want suffix /accounts/1", head.KID)
+		}
+
+		decodeJWSRequest(t, &req, bytes.NewReader(b))
+		if req.Status != StatusDeactivated {
+			t.Errorf("req.Status = %q; want = %q", req.Status, StatusDeactivated)
+		}
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	if err := cl.DeactivateReg(context.Background()); err != nil {
+		t.Errorf("DeactivateReg: %v, wanted no error", err)
+	}
+	if err := cl.DeactivateReg(context.Background()); err == nil {
+		t.Errorf("DeactivateReg: %v, wanted error for unauthorized", err)
+	}
+}
+
+func TestRF_DeactivateRegNoAccount(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.DeactivateReg(context.Background()); !errors.Is(err, ErrNoAccount) {
+		t.Errorf("DeactivateReg: %v, wanted ErrNoAccount", err)
+	}
+}
+
 func TestRFC_AuthorizeOrder(t *testing.T) {
 	s := newACMEServer()
 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {