acme: implement new order-based issuance methods

The order based issuance flow is different from pre-authorization
in that users tell upfront which identifiers they want a future
certificate to contain and the CA responds with a set of authorizations
to satisfy.

Similar to pre-authorization where users start with Client's
Authorize method, fulfill challenges and then call GetAuthorization
or WaitAuthorization, the order based flow starts with AuthorizeOrder
and then GetOrder or WaitOrder.

Once all order authorizations are satisfied, users can call
CreateOrderCert, as opposed to the old CreateCert, and FetchCert as before.
The new method implementation and updates to the existing methods
is in golang.org/cl/194379.

More on order based flow can be found in
https://tools.ietf.org/html/rfc8555#section-7.4.

Updates golang/go#21081

Change-Id: I37c37203b50785d7681f65f815d7b19d9c15b96d
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/192779
Run-TryBot: Alex Vaghin <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/acme/acme_test.go b/acme/acme_test.go
index f171f8d..e17f375 100644
--- a/acme/acme_test.go
+++ b/acme/acme_test.go
@@ -900,7 +900,7 @@
 func TestFetchCertCancel(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Retry-After", "0")
-		w.WriteHeader(http.StatusAccepted)
+		w.WriteHeader(http.StatusBadRequest)
 	}))
 	defer ts.Close()
 	ctx, cancel := context.WithCancel(context.Background())
diff --git a/acme/jws.go b/acme/jws.go
index f8bc2c4..cac8b67 100644
--- a/acme/jws.go
+++ b/acme/jws.go
@@ -24,6 +24,12 @@
 // See jwsEncodeJSON for details.
 const noKeyID = keyID("")
 
+// noPayload indicates jwsEncodeJSON will encode zero-length octet string
+// in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
+// authenticated GET requests via POSTing with an empty payload.
+// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
+const noPayload = ""
+
 // jwsEncodeJSON signs claimset using provided key and a nonce.
 // The result is serialized in JSON format containing either kid or jwk
 // fields based on the provided keyID value.
@@ -50,11 +56,14 @@
 		phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, kid, nonce, url)
 	}
 	phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
-	cs, err := json.Marshal(claimset)
-	if err != nil {
-		return nil, err
+	var payload string
+	if claimset != noPayload {
+		cs, err := json.Marshal(claimset)
+		if err != nil {
+			return nil, err
+		}
+		payload = base64.RawURLEncoding.EncodeToString(cs)
 	}
-	payload := base64.RawURLEncoding.EncodeToString(cs)
 	hash := sha.New()
 	hash.Write([]byte(phead + "." + payload))
 	sig, err := jwsSign(key, sha, hash.Sum(nil))
diff --git a/acme/rfc8555.go b/acme/rfc8555.go
index 51839a0..7517b05 100644
--- a/acme/rfc8555.go
+++ b/acme/rfc8555.go
@@ -9,6 +9,7 @@
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"time"
 )
 
 // DeactivateReg permanently disables an existing account associated with c.Key.
@@ -111,7 +112,7 @@
 		Orders  string
 	}
 	if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
-		return nil, fmt.Errorf("acme: invalid response: %v", err)
+		return nil, fmt.Errorf("acme: invalid account response: %v", err)
 	}
 	return &Account{
 		URI:       res.Header.Get("Location"),
@@ -120,3 +121,143 @@
 		OrdersURL: v.Orders,
 	}, nil
 }
+
+// AuthorizeOrder initiates the order-based application for certificate issuance,
+// as opposed to pre-authorization in Authorize.
+//
+// The caller then needs to fetch each required authorization with GetAuthorization
+// and fulfill a challenge using Accept. Once all authorizations are satisfied,
+// the caller will typically want to poll order status using WaitOrder until it's in StatusReady state.
+// To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert.
+func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
+	dir, err := c.Discover(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	req := struct {
+		Identifiers []wireAuthzID `json:"identifiers"`
+		NotBefore   string        `json:"notBefore,omitempty"`
+		NotAfter    string        `json:"notAfter,omitempty"`
+	}{}
+	for _, v := range id {
+		req.Identifiers = append(req.Identifiers, wireAuthzID{
+			Type:  v.Type,
+			Value: v.Value,
+		})
+	}
+	for _, o := range opt {
+		switch o := o.(type) {
+		case orderNotBeforeOpt:
+			req.NotBefore = time.Time(o).Format(time.RFC3339)
+		case orderNotAfterOpt:
+			req.NotAfter = time.Time(o).Format(time.RFC3339)
+		default:
+			// Package's fault if we let this happen.
+			panic(fmt.Sprintf("unsupported order option type %T", o))
+		}
+	}
+
+	res, err := c.post(ctx, nil, dir.OrderURL, req, wantStatus(http.StatusCreated))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	return responseOrder(res)
+}
+
+// GetOrder retrives an order identified by the given URL.
+// For orders created with AuthorizeOrder, the url value is Order.URI.
+//
+// If a caller needs to poll an order until its status is final,
+// see the WaitOrder method.
+func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
+	if _, err := c.Discover(ctx); err != nil {
+		return nil, err
+	}
+
+	res, err := c.post(ctx, nil, url, noPayload, wantStatus(http.StatusOK))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	return responseOrder(res)
+}
+
+// WaitOrder polls an order from the given URL until it is in one of the final states,
+// StatusReady, StatusValid or StatusInvalid, the CA responded with a non-retryable error
+// or the context is done.
+//
+// It returns a non-nil Order only if its Status is StatusReady or StatusValid.
+// In all other cases WaitOrder returns an error.
+// If the Status is StatusInvalid, the returned error is of type *WaitOrderError.
+func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
+	if _, err := c.Discover(ctx); err != nil {
+		return nil, err
+	}
+	for {
+		res, err := c.post(ctx, nil, url, noPayload, wantStatus(http.StatusOK))
+		if err != nil {
+			return nil, err
+		}
+		o, err := responseOrder(res)
+		res.Body.Close()
+		switch {
+		case err != nil:
+			// Skip and retry.
+		case o.Status == StatusInvalid:
+			return nil, &WaitOrderError{OrderURL: o.URI, Status: o.Status}
+		case o.Status == StatusReady || o.Status == StatusValid:
+			return o, nil
+		}
+
+		d := retryAfter(res.Header.Get("Retry-After"))
+		if d == 0 {
+			// Default retry-after.
+			// Same reasoning as in WaitAuthorization.
+			d = time.Second
+		}
+		t := time.NewTimer(d)
+		select {
+		case <-ctx.Done():
+			t.Stop()
+			return nil, ctx.Err()
+		case <-t.C:
+			// Retry.
+		}
+	}
+}
+
+func responseOrder(res *http.Response) (*Order, error) {
+	var v struct {
+		Status         string
+		Expires        time.Time
+		Identifiers    []wireAuthzID
+		NotBefore      time.Time
+		NotAfter       time.Time
+		Error          *wireError
+		Authorizations []string
+		Finalize       string
+		Certificate    string
+	}
+	if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
+		return nil, fmt.Errorf("acme: error reading order: %v", err)
+	}
+	o := &Order{
+		URI:         res.Header.Get("Location"),
+		Status:      v.Status,
+		Expires:     v.Expires,
+		NotBefore:   v.NotBefore,
+		NotAfter:    v.NotAfter,
+		AuthzURLs:   v.Authorizations,
+		FinalizeURL: v.Finalize,
+		CertURL:     v.Certificate,
+	}
+	for _, id := range v.Identifiers {
+		o.Identifiers = append(o.Identifiers, AuthzID{Type: id.Type, Value: id.Value})
+	}
+	if v.Error != nil {
+		o.Error = v.Error.error(nil /* headers */)
+	}
+	return o, nil
+}
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index c366380..a109c8d 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -474,3 +474,192 @@
 		t.Errorf("GetReg: %v; want any other non-nil err", err)
 	}
 }
+
+func TestRFC_AuthorizeOrder(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/new-order", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Location", s.url("/orders/1"))
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{
+			"status": "pending",
+			"expires": "2019-09-01T00:00:00Z",
+			"notBefore": "2019-08-31T00:00:00Z",
+			"notAfter": "2019-09-02T00:00:00Z",
+			"identifiers": [{"type":"dns", "value":"example.org"}],
+			"authorizations": [%q]
+		}`, s.url("/authz/1"))
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	o, err := cl.AuthorizeOrder(context.Background(), DomainIDs("example.org"),
+		WithOrderNotBefore(time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC)),
+		WithOrderNotAfter(time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC)),
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	okOrder := &Order{
+		URI:         s.url("/orders/1"),
+		Status:      StatusPending,
+		Expires:     time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC),
+		NotBefore:   time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC),
+		NotAfter:    time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC),
+		Identifiers: []AuthzID{AuthzID{Type: "dns", Value: "example.org"}},
+		AuthzURLs:   []string{s.url("/authz/1")},
+	}
+	if !reflect.DeepEqual(o, okOrder) {
+		t.Errorf("AuthorizeOrder = %+v; want %+v", o, okOrder)
+	}
+}
+
+func TestRFC_GetOrder(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("/orders/1", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Location", s.url("/orders/1"))
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{
+			"status": "invalid",
+			"expires": "2019-09-01T00:00:00Z",
+			"notBefore": "2019-08-31T00:00:00Z",
+			"notAfter": "2019-09-02T00:00:00Z",
+			"identifiers": [{"type":"dns", "value":"example.org"}],
+			"authorizations": ["/authz/1"],
+			"finalize": "/orders/1/fin",
+			"certificate": "/orders/1/cert",
+			"error": {"type": "badRequest"}
+		}`))
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	o, err := cl.GetOrder(context.Background(), s.url("/orders/1"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	okOrder := &Order{
+		URI:         s.url("/orders/1"),
+		Status:      StatusInvalid,
+		Expires:     time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC),
+		NotBefore:   time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC),
+		NotAfter:    time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC),
+		Identifiers: []AuthzID{AuthzID{Type: "dns", Value: "example.org"}},
+		AuthzURLs:   []string{"/authz/1"},
+		FinalizeURL: "/orders/1/fin",
+		CertURL:     "/orders/1/cert",
+		Error:       &Error{ProblemType: "badRequest"},
+	}
+	if !reflect.DeepEqual(o, okOrder) {
+		t.Errorf("GetOrder = %+v\nwant %+v", o, okOrder)
+	}
+}
+
+func TestRFC_WaitOrder(t *testing.T) {
+	for _, st := range []string{StatusReady, StatusValid} {
+		t.Run(st, func(t *testing.T) {
+			testWaitOrder(t, st)
+		})
+	}
+}
+
+func testWaitOrder(t *testing.T, okStatus string) {
+	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 count int
+	s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Location", s.url("/orders/1"))
+		w.WriteHeader(http.StatusOK)
+		s := StatusPending
+		if count > 0 {
+			s = okStatus
+		}
+		fmt.Fprintf(w, `{"status": %q}`, s)
+		count++
+	})
+	s.start()
+	defer s.close()
+
+	var order *Order
+	var err error
+	done := make(chan struct{})
+	go func() {
+		cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+		order, err = cl.WaitOrder(context.Background(), s.url("/orders/1"))
+		close(done)
+	}()
+	select {
+	case <-time.After(3 * time.Second):
+		t.Fatal("WaitOrder took too long to return")
+	case <-done:
+		if err != nil {
+			t.Fatalf("WaitOrder: %v", err)
+		}
+		if order.Status != okStatus {
+			t.Errorf("order.Status = %q; want %q", order.Status, okStatus)
+		}
+	}
+}
+
+func TestRFC_WaitOrderError(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"}`))
+	})
+	var count int
+	s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Location", s.url("/orders/1"))
+		w.WriteHeader(http.StatusOK)
+		s := StatusPending
+		if count > 0 {
+			s = StatusInvalid
+		}
+		fmt.Fprintf(w, `{"status": %q}`, s)
+		count++
+	})
+	s.start()
+	defer s.close()
+
+	var err error
+	done := make(chan struct{})
+	go func() {
+		cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+		_, err = cl.WaitOrder(context.Background(), s.url("/orders/1"))
+		close(done)
+	}()
+	select {
+	case <-time.After(3 * time.Second):
+		t.Fatal("WaitOrder took too long to return")
+	case <-done:
+		if err == nil {
+			t.Fatal("WaitOrder returned nil error")
+		}
+		e, ok := err.(*WaitOrderError)
+		if !ok {
+			t.Fatalf("err = %v (%T); want WaitOrderError", err, err)
+		}
+		if e.OrderURL != s.url("/orders/1") {
+			t.Errorf("e.OrderURL = %q; want %q", e.OrderURL, s.url("/orders/1"))
+		}
+		if e.Status != StatusInvalid {
+			t.Errorf("e.Status = %q; want %q", e.Status, StatusInvalid)
+		}
+	}
+}
diff --git a/acme/types.go b/acme/types.go
index 4432afb..c654fff 100644
--- a/acme/types.go
+++ b/acme/types.go
@@ -14,10 +14,12 @@
 	"time"
 )
 
-// ACME server response statuses used to describe Authorization and Challenge states.
+// ACME status values of Account, Order, Authorization and Challenge objects.
+// See https://tools.ietf.org/html/rfc8555#section-7.1.6 for details.
 const (
 	StatusDeactivated = "deactivated"
 	StatusInvalid     = "invalid"
+	StatusReady       = "ready"
 	StatusPending     = "pending"
 	StatusProcessing  = "processing"
 	StatusRevoked     = "revoked"
@@ -102,6 +104,18 @@
 	return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
 }
 
+// WaitOrderError is returned from Client's WaitOrder.
+// It indicates the order is unusable and the clients should start over with
+// AuthorizeOrder.
+type WaitOrderError struct {
+	OrderURL string
+	Status   string
+}
+
+func (we *WaitOrderError) Error() string {
+	return fmt.Sprintf("acme: wait order %s: status %s", we.OrderURL, we.Status)
+}
+
 // RateLimit reports whether err represents a rate limit error and
 // any Retry-After duration returned by the server.
 //
@@ -138,7 +152,7 @@
 	Contact []string
 
 	// Status indicates current account status as returned by the CA.
-	// Possible values are "valid", "deactivated", and "revoked".
+	// Possible values are StatusValid, StatusDeactivated, and StatusRevoked.
 	Status string
 
 	// OrdersURL is a URL from which a list of orders submitted by this account
@@ -223,28 +237,87 @@
 	ExternalAccountRequired bool
 }
 
-// Challenge encodes a returned CA challenge.
-// Its Error field may be non-nil if the challenge is part of an Authorization
-// with StatusInvalid.
-type Challenge struct {
-	// Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01".
-	Type string
-
-	// URI is where a challenge response can be posted to.
+// Order represents a client's request for a certificate.
+// It tracks the request flow progress through to issuance.
+type Order struct {
+	// URI uniquely identifies an order.
 	URI string
 
-	// Token is a random value that uniquely identifies the challenge.
-	Token string
-
-	// Status identifies the status of this challenge.
+	// Status represents the current status of the order.
+	// It indicates which action the client should take.
+	//
+	// Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid.
+	// Pending means the CA does not believe that the client has fulfilled the requirements.
+	// Ready indicates that the client has fulfilled all the requirements and can submit a CSR
+	// to obtain a certificate. This is done with Client's CreateOrderCert.
+	// Processing means the certificate is being issued.
+	// Valid indicates the CA has issued the certificate. It can be downloaded
+	// from the Order's CertURL. This is done with Client's FetchCert.
+	// Invalid means the certificate will not be issued. Users should consider this order
+	// abandoned.
 	Status string
 
-	// Error indicates the reason for an authorization failure
-	// when this challenge was used.
-	// The type of a non-nil value is *Error.
-	Error error
+	// Expires is the timestamp after which CA considers this order invalid.
+	Expires time.Time
+
+	// Identifiers contains all identifier objects which the order pertains to.
+	Identifiers []AuthzID
+
+	// NotBefore is the requested value of the notBefore field in the certificate.
+	NotBefore time.Time
+
+	// NotAfter is the requested value of the notAfter field in the certificate.
+	NotAfter time.Time
+
+	// AuthzURLs represents authorizations to complete before a certificate
+	// for identifiers specified in the order can be issued.
+	// It also contains unexpired authorizations that the client has completed
+	// in the past.
+	//
+	// Authorization objects can be fetched using Client's GetAuthorization method.
+	//
+	// The required authorizations are dictated by CA policies.
+	// There may not be a 1:1 relationship between the identifiers and required authorizations.
+	// Required authorizations can be identified by their StatusPending status.
+	//
+	// For orders in the StatusValid or StatusInvalid state these are the authorizations
+	// which were completed.
+	AuthzURLs []string
+
+	// FinalizeURL is the endpoint at which a CSR is submitted to obtain a certificate
+	// once all the authorizations are satisfied.
+	FinalizeURL string
+
+	// CertURL points to the certificate that has been issued in response to this order.
+	CertURL string
+
+	// The error that occurred while processing the order as received from a CA, if any.
+	Error *Error
 }
 
+// OrderOption allows customizing Client.AuthorizeOrder call.
+type OrderOption interface {
+	privateOrderOpt()
+}
+
+// WithOrderNotBefore sets order's NotBefore field.
+func WithOrderNotBefore(t time.Time) OrderOption {
+	return orderNotBeforeOpt(t)
+}
+
+// WithOrderNotAfter sets order's NotAfter field.
+func WithOrderNotAfter(t time.Time) OrderOption {
+	return orderNotAfterOpt(t)
+}
+
+type orderNotBeforeOpt time.Time
+
+func (orderNotBeforeOpt) privateOrderOpt() {}
+
+type orderNotAfterOpt time.Time
+
+func (orderNotAfterOpt) privateOrderOpt() {}
+
 // Authorization encodes an authorization response.
 type Authorization struct {
 	// URI uniquely identifies a authorization.
@@ -271,19 +344,42 @@
 
 // AuthzID is an identifier that an account is authorized to represent.
 type AuthzID struct {
-	Type  string // The type of identifier, e.g. "dns".
+	Type  string // The type of identifier, "dns" or "ip".
 	Value string // The identifier itself, e.g. "example.org".
 }
 
+// DomainIDs creates a slice of AuthzID with "dns" identifier type.
+func DomainIDs(names ...string) []AuthzID {
+	a := make([]AuthzID, len(names))
+	for i, v := range names {
+		a[i] = AuthzID{Type: "dns", Value: v}
+	}
+	return a
+}
+
+// IPIDs creates a slice of AuthzID with "ip" identifier type.
+// Each element of addr is textual form of an address as defined
+// in RFC1123 Section 2.1 for IPv4 and in RFC5952 Section 4 for IPv6.
+func IPIDs(addr ...string) []AuthzID {
+	a := make([]AuthzID, len(addr))
+	for i, v := range addr {
+		a[i] = AuthzID{Type: "ip", Value: v}
+	}
+	return a
+}
+
+// wireAuthzID is ACME JSON representation of authorization identifier objects.
+type wireAuthzID struct {
+	Type  string `json:"type"`
+	Value string `json:"value"`
+}
+
 // wireAuthz is ACME JSON representation of Authorization objects.
 type wireAuthz struct {
 	Status       string
 	Challenges   []wireChallenge
 	Combinations [][]int
-	Identifier   struct {
-		Type  string
-		Value string
-	}
+	Identifier   wireAuthzID
 }
 
 func (z *wireAuthz) authorization(uri string) *Authorization {
@@ -313,6 +409,28 @@
 	return err
 }
 
+// Challenge encodes a returned CA challenge.
+// Its Error field may be non-nil if the challenge is part of an Authorization
+// with StatusInvalid.
+type Challenge struct {
+	// Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01".
+	Type string
+
+	// URI is where a challenge response can be posted to.
+	URI string
+
+	// Token is a random value that uniquely identifies the challenge.
+	Token string
+
+	// Status identifies the status of this challenge.
+	Status string
+
+	// Error indicates the reason for an authorization failure
+	// when this challenge was used.
+	// The type of a non-nil value is *Error.
+	Error error
+}
+
 // wireChallenge is ACME JSON challenge representation.
 type wireChallenge struct {
 	URI    string `json:"uri"`