-
Notifications
You must be signed in to change notification settings - Fork 21
/
order.go
208 lines (174 loc) · 6.67 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
package acme
import (
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
)
// NewOrder initiates a new order for a new certificate. This method does not use ACME Renewal Info.
func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) {
return c.ReplacementOrder(account, nil, identifiers)
}
// NewOrderDomains takes a list of domain dns identifiers for a new certificate. Essentially a helper function.
func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) {
var identifiers []Identifier
for _, d := range domains {
identifiers = append(identifiers, Identifier{Type: "dns", Value: d})
}
return c.ReplacementOrder(account, nil, identifiers)
}
// ReplacementOrder takes an existing *x509.Certificate and initiates a new
// order for a new certificate, but with the order being marked as a
// replacement. Replacement orders which are valid replacements are (currently)
// exempt from Let's Encrypt NewOrder rate limits, but may not be exempt from
// other ACME CAs ACME Renewal Info implementations. At least one identifier
// must match the list of identifiers from the parent order to be considered as
// a valid replacement order.
// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, identifiers []Identifier) (Order, error) {
// If an old cert being replaced is present and the acme directory doesn't list a RenewalInfo endpoint,
// throw an error. This endpoint being present indicates support for ARI.
if oldCert != nil && c.dir.RenewalInfo == "" {
return Order{}, ErrRenewalInfoNotSupported
}
// 'replaces' is specifically listed as 'omitempty' so the json encoder doesn't include this key
// if the ari oldCert is nil
newOrderReq := struct {
Identifiers []Identifier `json:"identifiers"`
Replaces string `json:"replaces,omitempty"`
}{
Identifiers: identifiers,
}
newOrderResp := Order{}
// If present, add the ari cert ID from the original/old certificate
if oldCert != nil {
replacesCertID, err := GenerateARICertID(oldCert)
if err != nil {
return Order{}, fmt.Errorf("acme: error generating replacement certificate id: %v", err)
}
newOrderReq.Replaces = replacesCertID
newOrderResp.Replaces = replacesCertID // server does not appear to set this currently?
}
// Submit the order
resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated)
if err != nil {
return newOrderResp, err
}
defer resp.Body.Close()
newOrderResp.URL = resp.Header.Get("Location")
return newOrderResp, nil
}
// FetchOrder fetches an existing order given an order url.
func (c Client) FetchOrder(account Account, orderURL string) (Order, error) {
orderResp := Order{
URL: orderURL, // boulder response doesn't seem to contain location header for this request
}
_, err := c.post(orderURL, account.URL, account.PrivateKey, "", &orderResp, http.StatusOK)
return orderResp, err
}
// Helper function to determine whether an order is "finished" by its status.
func checkFinalizedOrderStatus(order Order) (bool, error) {
switch order.Status {
case "invalid":
// "invalid": The certificate will not be issued. Consider this
// order process abandoned.
if order.Error.Type != "" {
return true, order.Error
}
return true, errors.New("acme: finalized order is invalid, no error provided")
case "pending":
// "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, errors.New("acme: authorizations not fulfilled")
case "ready":
// "ready": The server agrees that the requirements have been
// fulfilled, and is awaiting finalization. Submit a finalization
// request.
return true, errors.New("acme: unexpected 'ready' state")
case "processing":
// "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 "valid":
// "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("acme: unknown order status: %s", order.Status)
}
}
// FinalizeOrder indicates to the acme server that the client considers an order complete and "finalizes" it.
// If the server believes the authorizations have been filled successfully, a certificate should then be available.
// This function assumes that the order status is "ready".
func (c Client) FinalizeOrder(account Account, order Order, csr *x509.CertificateRequest) (Order, error) {
finaliseReq := struct {
Csr string `json:"csr"`
}{
Csr: base64.RawURLEncoding.EncodeToString(csr.Raw),
}
resp, err := c.post(order.Finalize, account.URL, account.PrivateKey, finaliseReq, &order, http.StatusOK)
if err != nil {
return order, err
}
order.URL = resp.Header.Get("Location")
updateOrder := func(resp *http.Response) (bool, error) {
if finished, err := checkFinalizedOrderStatus(order); finished {
return true, err
}
retryAfter, err := parseRetryAfter(resp.Header.Get("Retry-After"))
if err != nil {
return false, fmt.Errorf("acme: error parsing retry-after header: %v", err)
}
order.RetryAfter = retryAfter
return false, nil
}
if finished, err := updateOrder(resp); finished || err != nil {
return order, err
}
fetchOrder := func() (bool, error) {
resp, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK)
if err != nil {
return false, nil
}
return updateOrder(resp)
}
if !c.IgnoreRetryAfter && !order.RetryAfter.IsZero() {
_, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return order, errors.New("acme: finalized order timeout")
}
diff := time.Until(order.RetryAfter)
_, pollTimeout := c.getPollingDurations()
if diff > pollTimeout {
return order, fmt.Errorf("acme: Retry-After (%v) longer than poll timeout (%v)", diff, c.PollTimeout)
}
if diff > 0 {
time.Sleep(diff)
}
if finished, err := fetchOrder(); finished || err != nil {
return order, err
}
}
}
if !c.IgnoreRetryAfter {
pollInterval, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return order, errors.New("acme: finalized order timeout")
}
time.Sleep(pollInterval)
if finished, err := fetchOrder(); finished || err != nil {
return order, err
}
}
}
return order, err
}