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