| // 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 equivalent to c.Register but for CAs implementing RFC 8555. |
| // It expects c.Discover to have already been called. |
| 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"` |
| ExternalAccountBinding *jsonWebSignature `json:"externalAccountBinding,omitempty"` |
| }{ |
| Contact: acct.Contact, |
| } |
| if c.dir.Terms != "" { |
| req.TermsAgreed = prompt(c.dir.Terms) |
| } |
| |
| // set 'externalAccountBinding' field if requested |
| if acct.ExternalAccountBinding != nil { |
| eabJWS, err := c.encodeExternalAccountBinding(acct.ExternalAccountBinding) |
| if err != nil { |
| return nil, fmt.Errorf("acme: failed to encode external account binding: %v", err) |
| } |
| req.ExternalAccountBinding = eabJWS |
| } |
| |
| 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 |
| } |
| |
| // encodeExternalAccountBinding will encode an external account binding stanza |
| // as described in https://tools.ietf.org/html/rfc8555#section-7.3.4. |
| func (c *Client) encodeExternalAccountBinding(eab *ExternalAccountBinding) (*jsonWebSignature, error) { |
| jwk, err := jwkEncode(c.Key.Public()) |
| if err != nil { |
| return nil, err |
| } |
| return jwsWithMAC(eab.Key, eab.KID, c.dir.RegURL, []byte(jwk)) |
| } |
| |
| // updateRegRFC 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" |
| } |
| |
| // ListCertAlternates retrieves any alternate certificate chain URLs for the |
| // given certificate chain URL. These alternate URLs can be passed to FetchCert |
| // in order to retrieve the alternate certificate chains. |
| // |
| // If there are no alternate issuer certificate chains, a nil slice will be |
| // returned. |
| func (c *Client) ListCertAlternates(ctx context.Context, url string) ([]string, error) { |
| if _, err := c.Discover(ctx); err != nil { // required by c.accountKID |
| return nil, err |
| } |
| |
| res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK)) |
| if err != nil { |
| return nil, err |
| } |
| defer res.Body.Close() |
| |
| // We don't need the body but we need to discard it so we don't end up |
| // preventing keep-alive |
| if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { |
| return nil, fmt.Errorf("acme: cert alternates response stream: %v", err) |
| } |
| alts := linkHeader(res.Header, "alternate") |
| return alts, nil |
| } |