order.go raw

   1  package api
   2  
   3  import (
   4  	"encoding/base64"
   5  	"errors"
   6  	"fmt"
   7  	"slices"
   8  	"time"
   9  
  10  	"github.com/go-acme/lego/v4/acme"
  11  )
  12  
  13  // OrderOptions used to create an order (optional).
  14  type OrderOptions struct {
  15  	NotBefore time.Time
  16  	NotAfter  time.Time
  17  
  18  	// A string uniquely identifying the profile
  19  	// which will be used to affect issuance of the certificate requested by this Order.
  20  	// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
  21  	Profile string
  22  
  23  	// A string uniquely identifying a previously-issued certificate which this
  24  	// order is intended to replace.
  25  	// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
  26  	ReplacesCertID string
  27  }
  28  
  29  type OrderService service
  30  
  31  // New Creates a new order.
  32  func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
  33  	return o.NewWithOptions(domains, nil)
  34  }
  35  
  36  // NewWithOptions Creates a new order.
  37  func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
  38  	orderReq := acme.Order{Identifiers: createIdentifiers(domains)}
  39  
  40  	if opts != nil {
  41  		if !opts.NotAfter.IsZero() {
  42  			orderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)
  43  		}
  44  
  45  		if !opts.NotBefore.IsZero() {
  46  			orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
  47  		}
  48  
  49  		if o.core.GetDirectory().RenewalInfo != "" {
  50  			orderReq.Replaces = opts.ReplacesCertID
  51  		}
  52  
  53  		if opts.Profile != "" {
  54  			orderReq.Profile = opts.Profile
  55  		}
  56  	}
  57  
  58  	var order acme.Order
  59  
  60  	resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
  61  	if err != nil {
  62  		are := &acme.AlreadyReplacedError{}
  63  		if !errors.As(err, &are) {
  64  			return acme.ExtendedOrder{}, err
  65  		}
  66  
  67  		// If the Server rejects the request because the identified certificate has already been marked as replaced,
  68  		// it MUST return an HTTP 409 (Conflict) with a problem document of type "alreadyReplaced" (see Section 7.4).
  69  		// https://www.rfc-editor.org/rfc/rfc9773.html#section-5
  70  		orderReq.Replaces = ""
  71  
  72  		resp, err = o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
  73  		if err != nil {
  74  			return acme.ExtendedOrder{}, err
  75  		}
  76  	}
  77  
  78  	// The server MUST return an error if it cannot fulfill the request as specified,
  79  	// and it MUST NOT issue a certificate with contents other than those requested.
  80  	// If the server requires the request to be modified in a certain way,
  81  	// it should indicate the required changes using an appropriate error type and description.
  82  	// https://www.rfc-editor.org/rfc/rfc8555#section-7.4
  83  	//
  84  	// Some ACME servers don't return an error,
  85  	// and/or change the order identifiers in the response,
  86  	// so we need to ensure that the identifiers are the same as requested.
  87  	// Deduplication by the server is allowed.
  88  	if compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 {
  89  		// Sorts identifiers to avoid error message ambiguities about the order of the identifiers.
  90  		slices.SortStableFunc(orderReq.Identifiers, compareIdentifier)
  91  		slices.SortStableFunc(order.Identifiers, compareIdentifier)
  92  
  93  		return acme.ExtendedOrder{},
  94  			fmt.Errorf("order identifiers have been modified by the ACME server (RFC8555 ยง7.4): %+v != %+v",
  95  				orderReq.Identifiers, order.Identifiers)
  96  	}
  97  
  98  	return acme.ExtendedOrder{
  99  		Order:    order,
 100  		Location: resp.Header.Get("Location"),
 101  	}, nil
 102  }
 103  
 104  // Get Gets an order.
 105  func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
 106  	if orderURL == "" {
 107  		return acme.ExtendedOrder{}, errors.New("order[get]: empty URL")
 108  	}
 109  
 110  	var order acme.Order
 111  
 112  	_, err := o.core.postAsGet(orderURL, &order)
 113  	if err != nil {
 114  		return acme.ExtendedOrder{}, err
 115  	}
 116  
 117  	return acme.ExtendedOrder{Order: order}, nil
 118  }
 119  
 120  // UpdateForCSR Updates an order for a CSR.
 121  func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedOrder, error) {
 122  	csrMsg := acme.CSRMessage{
 123  		Csr: base64.RawURLEncoding.EncodeToString(csr),
 124  	}
 125  
 126  	var order acme.Order
 127  
 128  	_, err := o.core.post(orderURL, csrMsg, &order)
 129  	if err != nil {
 130  		return acme.ExtendedOrder{}, err
 131  	}
 132  
 133  	if order.Status == acme.StatusInvalid {
 134  		return acme.ExtendedOrder{}, fmt.Errorf("invalid order: %w", order.Err())
 135  	}
 136  
 137  	return acme.ExtendedOrder{Order: order}, nil
 138  }
 139