232 lines
7.6 KiB
Go
232 lines
7.6 KiB
Go
package acme
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type OrderExtension struct {
|
|
Profile string
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// NewOrderExtension takes a struct providing any extensions onto the order
|
|
func (c Client) NewOrderExtension(account Account, identifiers []Identifier, ext OrderExtension) (Order, error) {
|
|
return c.ReplacementOrderExtension(account, nil, identifiers, ext)
|
|
}
|
|
|
|
// 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) {
|
|
return c.ReplacementOrderExtension(account, oldCert, identifiers, OrderExtension{})
|
|
}
|
|
|
|
// ReplacementOrderExtension takes a struct providing any extensions onto the order
|
|
func (c Client) ReplacementOrderExtension(account Account, oldCert *x509.Certificate, identifiers []Identifier, ext OrderExtension) (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
|
|
}
|
|
|
|
// optional fields are listed as 'omitempty' so the json encoder doesn't
|
|
// include those keys if their values are not provided.
|
|
newOrderReq := struct {
|
|
Identifiers []Identifier `json:"identifiers"`
|
|
Replaces string `json:"replaces,omitempty"`
|
|
Profile string `json:"Profile,omitempty"`
|
|
}{
|
|
Identifiers: identifiers,
|
|
}
|
|
|
|
newOrderResp := Order{}
|
|
|
|
if ext.Profile != "" {
|
|
_, ok := c.Directory().Meta.Profiles[ext.Profile]
|
|
if !ok {
|
|
return Order{}, fmt.Errorf("requested Profile not advertised by directory: %v", ext.Profile)
|
|
}
|
|
newOrderReq.Profile = ext.Profile
|
|
}
|
|
|
|
// 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
|
|
}
|