githaven/vendor/github.com/mholt/acmez/acme/order.go

248 lines
9.4 KiB
Go
Raw Normal View History

// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package acme
import (
"context"
"encoding/base64"
"errors"
"fmt"
"time"
)
// Order is an object that "represents a client's request for a certificate
// and is used to track the progress of that order through to issuance.
// Thus, the object contains information about the requested
// certificate, the authorizations that the server requires the client
// to complete, and any certificates that have resulted from this order."
// §7.1.3
type Order struct {
// status (required, string): The status of this order. Possible
// values are "pending", "ready", "processing", "valid", and
// "invalid". See Section 7.1.6.
Status string `json:"status"`
// expires (optional, string): The timestamp after which the server
// will consider this order invalid, encoded in the format specified
// in [RFC3339]. This field is REQUIRED for objects with "pending"
// or "valid" in the status field.
Expires time.Time `json:"expires,omitempty"`
// identifiers (required, array of object): An array of identifier
// objects that the order pertains to.
Identifiers []Identifier `json:"identifiers"`
// notBefore (optional, string): The requested value of the notBefore
// field in the certificate, in the date format defined in [RFC3339].
NotBefore *time.Time `json:"notBefore,omitempty"`
// notAfter (optional, string): The requested value of the notAfter
// field in the certificate, in the date format defined in [RFC3339].
NotAfter *time.Time `json:"notAfter,omitempty"`
// error (optional, object): The error that occurred while processing
// the order, if any. This field is structured as a problem document
// [RFC7807].
Error *Problem `json:"error,omitempty"`
// authorizations (required, array of string): For pending orders, the
// authorizations that the client needs to complete before the
// requested certificate can be issued (see Section 7.5), including
// unexpired authorizations that the client has completed in the past
// for identifiers specified in the order. The authorizations
// required are dictated by server policy; there may not be a 1:1
// relationship between the order identifiers and the authorizations
// required. For final orders (in the "valid" or "invalid" state),
// the authorizations that were completed. Each entry is a URL from
// which an authorization can be fetched with a POST-as-GET request.
Authorizations []string `json:"authorizations"`
// finalize (required, string): A URL that a CSR must be POSTed to once
// all of the order's authorizations are satisfied to finalize the
// order. The result of a successful finalization will be the
// population of the certificate URL for the order.
Finalize string `json:"finalize"`
// certificate (optional, string): A URL for the certificate that has
// been issued in response to this order.
Certificate string `json:"certificate"`
// Similar to new-account, the server returns a 201 response with
// the URL to the order object in the Location header.
//
// We transfer the value from the header to this field for
// storage and recall purposes.
Location string `json:"-"`
}
// Identifier is used in order and authorization (authz) objects.
type Identifier struct {
// type (required, string): The type of identifier. This document
// defines the "dns" identifier type. See the registry defined in
// Section 9.7.7 for any others.
Type string `json:"type"`
// value (required, string): The identifier itself.
Value string `json:"value"`
}
// NewOrder creates a new order with the server.
//
// "The client begins the certificate issuance process by sending a POST
// request to the server's newOrder resource." §7.4
func (c *Client) NewOrder(ctx context.Context, account Account, order Order) (Order, error) {
if err := c.provision(ctx); err != nil {
return order, err
}
resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.NewOrder, order, &order)
if err != nil {
return order, err
}
order.Location = resp.Header.Get("Location")
return order, nil
}
// FinalizeOrder finalizes the order with the server and polls under the server has
// updated the order status. The CSR must be in ASN.1 DER-encoded format. If this
// succeeds, the certificate is ready to download once this returns.
//
// "Once the client believes it has fulfilled the server's requirements,
// it should send a POST request to the order resource's finalize URL." §7.4
func (c *Client) FinalizeOrder(ctx context.Context, account Account, order Order, csrASN1DER []byte) (Order, error) {
if err := c.provision(ctx); err != nil {
return order, err
}
body := struct {
// csr (required, string): A CSR encoding the parameters for the
// certificate being requested [RFC2986]. The CSR is sent in the
// base64url-encoded version of the DER format. (Note: Because this
// field uses base64url, and does not include headers, it is
// different from PEM.) §7.4
CSR string `json:"csr"`
}{
CSR: base64.RawURLEncoding.EncodeToString(csrASN1DER),
}
resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Finalize, body, &order)
if err != nil {
// "A request to finalize an order will result in error if the order is
// not in the 'ready' state. In such cases, the server MUST return a
// 403 (Forbidden) error with a problem document of type
// 'orderNotReady'. The client should then send a POST-as-GET request
// to the order resource to obtain its current state. The status of the
// order will indicate what action the client should take (see below)."
// §7.4
var problem Problem
if errors.As(err, &problem) {
if problem.Type != ProblemTypeOrderNotReady {
return order, err
}
} else {
return order, err
}
}
// unlike with accounts and authorizations, the spec isn't clear on whether
// the server MUST set this on finalizing the order, but their example shows a
// Location header, so I guess if it's set in the response, we should keep it
if newLocation := resp.Header.Get("Location"); newLocation != "" {
order.Location = newLocation
}
if finished, err := orderIsFinished(order); finished {
return order, err
}
// TODO: "The elements of the "authorizations" and "identifiers" arrays are
// immutable once set. If a client observes a change
// in the contents of either array, then it SHOULD consider the order
// invalid."
maxDuration := c.pollTimeout()
start := time.Now()
for time.Since(start) < maxDuration {
// querying an order is expensive on the server-side, so we
// shouldn't do it too frequently; honor server preference
interval, err := retryAfter(resp, c.pollInterval())
if err != nil {
return order, err
}
select {
case <-time.After(interval):
case <-ctx.Done():
return order, ctx.Err()
}
resp, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order)
if err != nil {
return order, fmt.Errorf("polling order status: %w", err)
}
// (same reasoning as above)
if newLocation := resp.Header.Get("Location"); newLocation != "" {
order.Location = newLocation
}
if finished, err := orderIsFinished(order); finished {
return order, err
}
}
return order, fmt.Errorf("order took too long")
}
// orderIsFinished returns true if the order processing is complete,
// regardless of success or failure. If this function returns true,
// polling an order status should stop. If there is an error with the
// order, an error will be returned. This function should be called
// only after a request to finalize an order. See §7.4.
func orderIsFinished(order Order) (bool, error) {
switch order.Status {
case StatusInvalid:
// "invalid": The certificate will not be issued. Consider this
// order process abandoned.
return true, fmt.Errorf("final order is invalid: %w", order.Error)
case StatusPending:
// "pending": The server does not believe that the client has
// fulfilled the requirements. Check the "authorizations" array for
// entries that are still pending.
return true, fmt.Errorf("order pending, authorizations remaining: %v", order.Authorizations)
case StatusReady:
// "ready": The server agrees that the requirements have been
// fulfilled, and is awaiting finalization. Submit a finalization
// request.
// (we did just submit a finalization request, so this is an error)
return true, fmt.Errorf("unexpected state: %s - order already finalized", order.Status)
case StatusProcessing:
// "processing": The certificate is being issued. Send a GET request
// after the time given in the "Retry-After" header field of the
// response, if any.
return false, nil
case StatusValid:
// "valid": The server has issued the certificate and provisioned its
// URL to the "certificate" field of the order. Download the
// certificate.
return true, nil
default:
return true, fmt.Errorf("unrecognized order status: %s", order.Status)
}
}