-
-
Notifications
You must be signed in to change notification settings - Fork 32
/
order.go
281 lines (245 loc) · 10.6 KB
/
order.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// 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"
"go.uber.org/zap"
)
// 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,omitempty"`
// 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"`
// replaces (string, optional): A string uniquely identifying a
// previously-issued certificate which this order is intended to replace.
// This unique identifier is constructed in the same way as the path
// component for GET requests described above. Clients SHOULD include
// this field in New Order requests if there is a clear predecessor
// certificate, as is the case for most certificate renewals.
//
// EXPERIMENTAL: Draft ACME extension ARI: draft-ietf-acme-ari-03
Replaces string `json:"replaces,omitempty"`
// 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:"-"`
}
func (o Order) identifierValues() []string {
var list []string
for _, id := range o.Identifiers {
list = append(list, id.Value)
}
return list
}
// 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
}
if c.Logger != nil {
c.Logger.Debug("creating order",
zap.String("account", account.Location),
zap.Strings("identifiers", order.identifierValues()))
}
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
}
// GetOrder retrieves an order from the server. The Order's Location field must be populated.
func (c *Client) GetOrder(ctx context.Context, account Account, order Order) (Order, error) {
if err := c.provision(ctx); err != nil {
return order, err
}
_, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order)
return order, err
}
// 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)
}
}