7e290869e7
Includes69ecbb4d6d
(forward-port of8b5121be2f
), which fixes CVE-2020-7919: - Panic in crypto/x509 certificate parsing and golang.org/x/crypto/cryptobyte On 32-bit architectures, a malformed input to crypto/x509 or the ASN.1 parsing functions of golang.org/x/crypto/cryptobyte can lead to a panic. The malformed certificate can be delivered via a crypto/tls connection to a client, or to a server that accepts client certificates. net/http clients can be made to crash by an HTTPS server, while net/http servers that accept client certificates will recover the panic and are unaffected. Thanks to Project Wycheproof for providing the test cases that led to the discovery of this issue. The issue is CVE-2020-7919 and Go issue golang.org/issue/36837. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
393 lines
11 KiB
Go
393 lines
11 KiB
Go
// 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"
|
|
}
|