forked from Shiloh/githaven
248 lines
9.4 KiB
Go
248 lines
9.4 KiB
Go
|
// 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)
|
||
|
}
|
||
|
}
|