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
}
|