123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- // Copyright 2019 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- package acme
- import (
- "context"
- "crypto"
- "encoding/base64"
- "encoding/json"
- "encoding/pem"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "time"
- )
- // DeactivateReg permanently disables an existing account associated with c.Key.
- // A deactivated account can no longer request certificate issuance or access
- // resources related to the account, such as orders or authorizations.
- //
- // It only works with CAs implementing RFC 8555.
- func (c *Client) DeactivateReg(ctx context.Context) error {
- url := string(c.accountKID(ctx))
- if url == "" {
- return ErrNoAccount
- }
- req := json.RawMessage(`{"status": "deactivated"}`)
- res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
- if err != nil {
- return err
- }
- res.Body.Close()
- return nil
- }
- // registerRFC is quivalent to c.Register but for CAs implementing RFC 8555.
- // It expects c.Discover to have already been called.
- // TODO: Implement externalAccountBinding.
- func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
- c.cacheMu.Lock() // guard c.kid access
- defer c.cacheMu.Unlock()
- req := struct {
- TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
- Contact []string `json:"contact,omitempty"`
- }{
- Contact: acct.Contact,
- }
- if c.dir.Terms != "" {
- req.TermsAgreed = prompt(c.dir.Terms)
- }
- res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(
- http.StatusOK, // account with this key already registered
- http.StatusCreated, // new account created
- ))
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- a, err := responseAccount(res)
- if err != nil {
- return nil, err
- }
- // Cache Account URL even if we return an error to the caller.
- // It is by all means a valid and usable "kid" value for future requests.
- c.kid = keyID(a.URI)
- if res.StatusCode == http.StatusOK {
- return nil, ErrAccountAlreadyExists
- }
- return a, nil
- }
- // updateGegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
- // It expects c.Discover to have already been called.
- func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) {
- url := string(c.accountKID(ctx))
- if url == "" {
- return nil, ErrNoAccount
- }
- req := struct {
- Contact []string `json:"contact,omitempty"`
- }{
- Contact: a.Contact,
- }
- res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- return responseAccount(res)
- }
- // getGegRFC is equivalent to c.GetReg but for CAs implementing RFC 8555.
- // It expects c.Discover to have already been called.
- func (c *Client) getRegRFC(ctx context.Context) (*Account, error) {
- req := json.RawMessage(`{"onlyReturnExisting": true}`)
- res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(http.StatusOK))
- if e, ok := err.(*Error); ok && e.ProblemType == "urn:ietf:params:acme:error:accountDoesNotExist" {
- return nil, ErrNoAccount
- }
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- return responseAccount(res)
- }
- func responseAccount(res *http.Response) (*Account, error) {
- var v struct {
- Status string
- Contact []string
- Orders string
- }
- if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
- return nil, fmt.Errorf("acme: invalid account response: %v", err)
- }
- return &Account{
- URI: res.Header.Get("Location"),
- Status: v.Status,
- Contact: v.Contact,
- OrdersURL: v.Orders,
- }, nil
- }
- // AuthorizeOrder initiates the order-based application for certificate issuance,
- // as opposed to pre-authorization in Authorize.
- // It is only supported by CAs implementing RFC 8555.
- //
- // The caller then needs to fetch each authorization with GetAuthorization,
- // identify those with StatusPending status and fulfill a challenge using Accept.
- // Once all authorizations are satisfied, the caller will typically want to poll
- // order status using WaitOrder until it's in StatusReady state.
- // To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert.
- func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
- dir, err := c.Discover(ctx)
- if err != nil {
- return nil, err
- }
- req := struct {
- Identifiers []wireAuthzID `json:"identifiers"`
- NotBefore string `json:"notBefore,omitempty"`
- NotAfter string `json:"notAfter,omitempty"`
- }{}
- for _, v := range id {
- req.Identifiers = append(req.Identifiers, wireAuthzID{
- Type: v.Type,
- Value: v.Value,
- })
- }
- for _, o := range opt {
- switch o := o.(type) {
- case orderNotBeforeOpt:
- req.NotBefore = time.Time(o).Format(time.RFC3339)
- case orderNotAfterOpt:
- req.NotAfter = time.Time(o).Format(time.RFC3339)
- default:
- // Package's fault if we let this happen.
- panic(fmt.Sprintf("unsupported order option type %T", o))
- }
- }
- res, err := c.post(ctx, nil, dir.OrderURL, req, wantStatus(http.StatusCreated))
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- return responseOrder(res)
- }
- // GetOrder retrives an order identified by the given URL.
- // For orders created with AuthorizeOrder, the url value is Order.URI.
- //
- // If a caller needs to poll an order until its status is final,
- // see the WaitOrder method.
- func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
- if _, err := c.Discover(ctx); err != nil {
- return nil, err
- }
- res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- return responseOrder(res)
- }
- // WaitOrder polls an order from the given URL until it is in one of the final states,
- // StatusReady, StatusValid or StatusInvalid, the CA responded with a non-retryable error
- // or the context is done.
- //
- // It returns a non-nil Order only if its Status is StatusReady or StatusValid.
- // In all other cases WaitOrder returns an error.
- // If the Status is StatusInvalid, the returned error is of type *OrderError.
- func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
- if _, err := c.Discover(ctx); err != nil {
- return nil, err
- }
- for {
- res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
- if err != nil {
- return nil, err
- }
- o, err := responseOrder(res)
- res.Body.Close()
- switch {
- case err != nil:
- // Skip and retry.
- case o.Status == StatusInvalid:
- return nil, &OrderError{OrderURL: o.URI, Status: o.Status}
- case o.Status == StatusReady || o.Status == StatusValid:
- return o, nil
- }
- d := retryAfter(res.Header.Get("Retry-After"))
- if d == 0 {
- // Default retry-after.
- // Same reasoning as in WaitAuthorization.
- d = time.Second
- }
- t := time.NewTimer(d)
- select {
- case <-ctx.Done():
- t.Stop()
- return nil, ctx.Err()
- case <-t.C:
- // Retry.
- }
- }
- }
- func responseOrder(res *http.Response) (*Order, error) {
- var v struct {
- Status string
- Expires time.Time
- Identifiers []wireAuthzID
- NotBefore time.Time
- NotAfter time.Time
- Error *wireError
- Authorizations []string
- Finalize string
- Certificate string
- }
- if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
- return nil, fmt.Errorf("acme: error reading order: %v", err)
- }
- o := &Order{
- URI: res.Header.Get("Location"),
- Status: v.Status,
- Expires: v.Expires,
- NotBefore: v.NotBefore,
- NotAfter: v.NotAfter,
- AuthzURLs: v.Authorizations,
- FinalizeURL: v.Finalize,
- CertURL: v.Certificate,
- }
- for _, id := range v.Identifiers {
- o.Identifiers = append(o.Identifiers, AuthzID{Type: id.Type, Value: id.Value})
- }
- if v.Error != nil {
- o.Error = v.Error.error(nil /* headers */)
- }
- return o, nil
- }
- // CreateOrderCert submits the CSR (Certificate Signing Request) to a CA at the specified URL.
- // The URL is the FinalizeURL field of an Order created with AuthorizeOrder.
- //
- // If the bundle argument is true, the returned value also contain the CA (issuer)
- // certificate chain. Otherwise, only a leaf certificate is returned.
- // The returned URL can be used to re-fetch the certificate using FetchCert.
- //
- // This method is only supported by CAs implementing RFC 8555. See CreateCert for pre-RFC CAs.
- //
- // CreateOrderCert returns an error if the CA's response is unreasonably large.
- // Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
- func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
- if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
- return nil, "", err
- }
- // RFC describes this as "finalize order" request.
- req := struct {
- CSR string `json:"csr"`
- }{
- CSR: base64.RawURLEncoding.EncodeToString(csr),
- }
- res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
- if err != nil {
- return nil, "", err
- }
- defer res.Body.Close()
- o, err := responseOrder(res)
- if err != nil {
- return nil, "", err
- }
- // Wait for CA to issue the cert if they haven't.
- if o.Status != StatusValid {
- o, err = c.WaitOrder(ctx, o.URI)
- }
- if err != nil {
- return nil, "", err
- }
- // The only acceptable status post finalize and WaitOrder is "valid".
- if o.Status != StatusValid {
- return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status}
- }
- crt, err := c.fetchCertRFC(ctx, o.CertURL, bundle)
- return crt, o.CertURL, err
- }
- // fetchCertRFC downloads issued certificate from the given URL.
- // It expects the CA to respond with PEM-encoded certificate chain.
- //
- // The URL argument is the CertURL field of Order.
- func (c *Client) fetchCertRFC(ctx context.Context, url string, bundle bool) ([][]byte, error) {
- res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- // Get all the bytes up to a sane maximum.
- // Account very roughly for base64 overhead.
- const max = maxCertChainSize + maxCertChainSize/33
- b, err := ioutil.ReadAll(io.LimitReader(res.Body, max+1))
- if err != nil {
- return nil, fmt.Errorf("acme: fetch cert response stream: %v", err)
- }
- if len(b) > max {
- return nil, errors.New("acme: certificate chain is too big")
- }
- // Decode PEM chain.
- var chain [][]byte
- for {
- var p *pem.Block
- p, b = pem.Decode(b)
- if p == nil {
- break
- }
- if p.Type != "CERTIFICATE" {
- return nil, fmt.Errorf("acme: invalid PEM cert type %q", p.Type)
- }
- chain = append(chain, p.Bytes)
- if !bundle {
- return chain, nil
- }
- if len(chain) > maxChainLen {
- return nil, errors.New("acme: certificate chain is too long")
- }
- }
- if len(chain) == 0 {
- return nil, errors.New("acme: certificate chain is empty")
- }
- return chain, nil
- }
- // sends a cert revocation request in either JWK form when key is non-nil or KID form otherwise.
- func (c *Client) revokeCertRFC(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
- req := &struct {
- Cert string `json:"certificate"`
- Reason int `json:"reason"`
- }{
- Cert: base64.RawURLEncoding.EncodeToString(cert),
- Reason: int(reason),
- }
- res, err := c.post(ctx, key, c.dir.RevokeURL, req, wantStatus(http.StatusOK))
- if err != nil {
- if isAlreadyRevoked(err) {
- // Assume it is not an error to revoke an already revoked cert.
- return nil
- }
- return err
- }
- defer res.Body.Close()
- return nil
- }
- func isAlreadyRevoked(err error) bool {
- e, ok := err.(*Error)
- return ok && e.ProblemType == "urn:ietf:params:acme:error:alreadyRevoked"
- }
|