* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
		
			
				
	
	
		
			248 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			248 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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)
 | |
| 	}
 | |
| }
 |