whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,818 @@
// Copyright 2015 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 provides an implementation of the
// Automatic Certificate Management Environment (ACME) spec,
// most famously used by Let's Encrypt.
//
// The initial implementation of this package was based on an early version
// of the spec. The current implementation supports only the modern
// RFC 8555 but some of the old API surface remains for compatibility.
// While code using the old API will still compile, it will return an error.
// Note the deprecation comments to update your code.
//
// See https://tools.ietf.org/html/rfc8555 for the spec.
//
// Most common scenarios will want to use autocert subdirectory instead,
// which provides automatic access to certificates from Let's Encrypt
// and any other ACME-based CA.
package acme
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net/http"
"strings"
"sync"
"time"
)
const (
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
LetsEncryptURL = "https://acme-v02.api.letsencrypt.org/directory"
// ALPNProto is the ALPN protocol name used by a CA server when validating
// tls-alpn-01 challenges.
//
// Package users must ensure their servers can negotiate the ACME ALPN in
// order for tls-alpn-01 challenge verifications to succeed.
// See the crypto/tls package's Config.NextProtos field.
ALPNProto = "acme-tls/1"
)
// idPeACMEIdentifier is the OID for the ACME extension for the TLS-ALPN challenge.
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1
var idPeACMEIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
const (
maxChainLen = 5 // max depth and breadth of a certificate chain
maxCertSize = 1 << 20 // max size of a certificate, in DER bytes
// Used for decoding certs from application/pem-certificate-chain response,
// the default when in RFC mode.
maxCertChainSize = maxCertSize * maxChainLen
// Max number of collected nonces kept in memory.
// Expect usual peak of 1 or 2.
maxNonces = 100
)
// Client is an ACME client.
//
// The only required field is Key. An example of creating a client with a new key
// is as follows:
//
// key, err := rsa.GenerateKey(rand.Reader, 2048)
// if err != nil {
// log.Fatal(err)
// }
// client := &Client{Key: key}
type Client struct {
// Key is the account key used to register with a CA and sign requests.
// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
//
// The following algorithms are supported:
// RS256, ES256, ES384 and ES512.
// See RFC 7518 for more details about the algorithms.
Key crypto.Signer
// HTTPClient optionally specifies an HTTP client to use
// instead of http.DefaultClient.
HTTPClient *http.Client
// DirectoryURL points to the CA directory endpoint.
// If empty, LetsEncryptURL is used.
// Mutating this value after a successful call of Client's Discover method
// will have no effect.
DirectoryURL string
// RetryBackoff computes the duration after which the nth retry of a failed request
// should occur. The value of n for the first call on failure is 1.
// The values of r and resp are the request and response of the last failed attempt.
// If the returned value is negative or zero, no more retries are done and an error
// is returned to the caller of the original method.
//
// Requests which result in a 4xx client error are not retried,
// except for 400 Bad Request due to "bad nonce" errors and 429 Too Many Requests.
//
// If RetryBackoff is nil, a truncated exponential backoff algorithm
// with the ceiling of 10 seconds is used, where each subsequent retry n
// is done after either ("Retry-After" + jitter) or (2^n seconds + jitter),
// preferring the former if "Retry-After" header is found in the resp.
// The jitter is a random value up to 1 second.
RetryBackoff func(n int, r *http.Request, resp *http.Response) time.Duration
// UserAgent is prepended to the User-Agent header sent to the ACME server,
// which by default is this package's name and version.
//
// Reusable libraries and tools in particular should set this value to be
// identifiable by the server, in case they are causing issues.
UserAgent string
cacheMu sync.Mutex
dir *Directory // cached result of Client's Discover method
// KID is the key identifier provided by the CA. If not provided it will be
// retrieved from the CA by making a call to the registration endpoint.
KID KeyID
noncesMu sync.Mutex
nonces map[string]struct{} // nonces collected from previous responses
}
// accountKID returns a key ID associated with c.Key, the account identity
// provided by the CA during RFC based registration.
// It assumes c.Discover has already been called.
//
// accountKID requires at most one network roundtrip.
// It caches only successful result.
//
// When in pre-RFC mode or when c.getRegRFC responds with an error, accountKID
// returns noKeyID.
func (c *Client) accountKID(ctx context.Context) KeyID {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
if c.KID != noKeyID {
return c.KID
}
a, err := c.getRegRFC(ctx)
if err != nil {
return noKeyID
}
c.KID = KeyID(a.URI)
return c.KID
}
var errPreRFC = errors.New("acme: server does not support the RFC 8555 version of ACME")
// Discover performs ACME server discovery using c.DirectoryURL.
//
// It caches successful result. So, subsequent calls will not result in
// a network round-trip. This also means mutating c.DirectoryURL after successful call
// of this method will have no effect.
func (c *Client) Discover(ctx context.Context) (Directory, error) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
if c.dir != nil {
return *c.dir, nil
}
res, err := c.get(ctx, c.directoryURL(), wantStatus(http.StatusOK))
if err != nil {
return Directory{}, err
}
defer res.Body.Close()
c.addNonce(res.Header)
var v struct {
Reg string `json:"newAccount"`
Authz string `json:"newAuthz"`
Order string `json:"newOrder"`
Revoke string `json:"revokeCert"`
Nonce string `json:"newNonce"`
KeyChange string `json:"keyChange"`
Meta struct {
Terms string `json:"termsOfService"`
Website string `json:"website"`
CAA []string `json:"caaIdentities"`
ExternalAcct bool `json:"externalAccountRequired"`
}
}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return Directory{}, err
}
if v.Order == "" {
return Directory{}, errPreRFC
}
c.dir = &Directory{
RegURL: v.Reg,
AuthzURL: v.Authz,
OrderURL: v.Order,
RevokeURL: v.Revoke,
NonceURL: v.Nonce,
KeyChangeURL: v.KeyChange,
Terms: v.Meta.Terms,
Website: v.Meta.Website,
CAA: v.Meta.CAA,
ExternalAccountRequired: v.Meta.ExternalAcct,
}
return *c.dir, nil
}
func (c *Client) directoryURL() string {
if c.DirectoryURL != "" {
return c.DirectoryURL
}
return LetsEncryptURL
}
// CreateCert was part of the old version of ACME. It is incompatible with RFC 8555.
//
// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateOrderCert.
func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
return nil, "", errPreRFC
}
// FetchCert retrieves already issued certificate from the given url, in DER format.
// It retries the request until the certificate is successfully retrieved,
// context is cancelled by the caller or an error response is received.
//
// If the bundle argument is true, the returned value also contains the CA (issuer)
// certificate chain.
//
// FetchCert returns an error if the CA's response or chain was unreasonably large.
// Callers are encouraged to parse the returned value to ensure the certificate is valid
// and has expected features.
func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.fetchCertRFC(ctx, url, bundle)
}
// RevokeCert revokes a previously issued certificate cert, provided in DER format.
//
// The key argument, used to sign the request, must be authorized
// to revoke the certificate. It's up to the CA to decide which keys are authorized.
// For instance, the key pair of the certificate may be authorized.
// If the key is nil, c.Key is used instead.
func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
if _, err := c.Discover(ctx); err != nil {
return err
}
return c.revokeCertRFC(ctx, key, cert, reason)
}
// AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service
// during account registration. See Register method of Client for more details.
func AcceptTOS(tosURL string) bool { return true }
// Register creates a new account with the CA using c.Key.
// It returns the registered account. The account acct is not modified.
//
// The registration may require the caller to agree to the CA's Terms of Service (TOS).
// If so, and the account has not indicated the acceptance of the terms (see Account for details),
// Register calls prompt with a TOS URL provided by the CA. Prompt should report
// whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS.
//
// When interfacing with an RFC-compliant CA, non-RFC 8555 fields of acct are ignored
// and prompt is called if Directory's Terms field is non-zero.
// Also see Error's Instance field for when a CA requires already registered accounts to agree
// to an updated Terms of Service.
func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
if c.Key == nil {
return nil, errors.New("acme: client.Key must be set to Register")
}
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.registerRFC(ctx, acct, prompt)
}
// GetReg retrieves an existing account associated with c.Key.
//
// The url argument is a legacy artifact of the pre-RFC 8555 API
// and is ignored.
func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.getRegRFC(ctx)
}
// UpdateReg updates an existing registration.
// It returns an updated account copy. The provided account is not modified.
//
// The account's URI is ignored and the account URL associated with
// c.Key is used instead.
func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.updateRegRFC(ctx, acct)
}
// AccountKeyRollover attempts to transition a client's account key to a new key.
// On success client's Key is updated which is not concurrency safe.
// On failure an error will be returned.
// The new key is already registered with the ACME provider if the following is true:
// - error is of type acme.Error
// - StatusCode should be 409 (Conflict)
// - Location header will have the KID of the associated account
//
// More about account key rollover can be found at
// https://tools.ietf.org/html/rfc8555#section-7.3.5.
func (c *Client) AccountKeyRollover(ctx context.Context, newKey crypto.Signer) error {
return c.accountKeyRollover(ctx, newKey)
}
// Authorize performs the initial step in the pre-authorization flow,
// as opposed to order-based flow.
// The caller will then need to choose from and perform a set of returned
// challenges using c.Accept in order to successfully complete authorization.
//
// Once complete, the caller can use AuthorizeOrder which the CA
// should provision with the already satisfied authorization.
// For pre-RFC CAs, the caller can proceed directly to requesting a certificate
// using CreateCert method.
//
// If an authorization has been previously granted, the CA may return
// a valid authorization which has its Status field set to StatusValid.
//
// More about pre-authorization can be found at
// https://tools.ietf.org/html/rfc8555#section-7.4.1.
func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) {
return c.authorize(ctx, "dns", domain)
}
// AuthorizeIP is the same as Authorize but requests IP address authorization.
// Clients which successfully obtain such authorization may request to issue
// a certificate for IP addresses.
//
// See the ACME spec extension for more details about IP address identifiers:
// https://tools.ietf.org/html/draft-ietf-acme-ip.
func (c *Client) AuthorizeIP(ctx context.Context, ipaddr string) (*Authorization, error) {
return c.authorize(ctx, "ip", ipaddr)
}
func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
type authzID struct {
Type string `json:"type"`
Value string `json:"value"`
}
req := struct {
Resource string `json:"resource"`
Identifier authzID `json:"identifier"`
}{
Resource: "new-authz",
Identifier: authzID{Type: typ, Value: val},
}
res, err := c.post(ctx, nil, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
if err != nil {
return nil, err
}
defer res.Body.Close()
var v wireAuthz
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
if v.Status != StatusPending && v.Status != StatusValid {
return nil, fmt.Errorf("acme: unexpected status: %s", v.Status)
}
return v.authorization(res.Header.Get("Location")), nil
}
// GetAuthorization retrieves an authorization identified by the given URL.
//
// If a caller needs to poll an authorization until its status is final,
// see the WaitAuthorization method.
func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, 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()
var v wireAuthz
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.authorization(url), nil
}
// RevokeAuthorization relinquishes an existing authorization identified
// by the given URL.
// The url argument is an Authorization.URI value.
//
// If successful, the caller will be required to obtain a new authorization
// using the Authorize or AuthorizeOrder methods before being able to request
// a new certificate for the domain associated with the authorization.
//
// It does not revoke existing certificates.
func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
if _, err := c.Discover(ctx); err != nil {
return err
}
req := struct {
Resource string `json:"resource"`
Status string `json:"status"`
Delete bool `json:"delete"`
}{
Resource: "authz",
Status: "deactivated",
Delete: true,
}
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
if err != nil {
return err
}
defer res.Body.Close()
return nil
}
// WaitAuthorization polls an authorization at the given URL
// until it is in one of the final states, StatusValid or StatusInvalid,
// the ACME CA responded with a 4xx error code, or the context is done.
//
// It returns a non-nil Authorization only if its Status is StatusValid.
// In all other cases WaitAuthorization returns an error.
// If the Status is StatusInvalid, the returned error is of type *AuthorizationError.
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
for {
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
if err != nil {
return nil, err
}
var raw wireAuthz
err = json.NewDecoder(res.Body).Decode(&raw)
res.Body.Close()
switch {
case err != nil:
// Skip and retry.
case raw.Status == StatusValid:
return raw.authorization(url), nil
case raw.Status == StatusInvalid:
return nil, raw.error(url)
}
// Exponential backoff is implemented in c.get above.
// This is just to prevent continuously hitting the CA
// while waiting for a final authorization status.
d := retryAfter(res.Header.Get("Retry-After"))
if d == 0 {
// Given that the fastest challenges TLS-SNI and HTTP-01
// require a CA to make at least 1 network round trip
// and most likely persist a challenge state,
// this default delay seems reasonable.
d = time.Second
}
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return nil, ctx.Err()
case <-t.C:
// Retry.
}
}
}
// GetChallenge retrieves the current status of an challenge.
//
// A client typically polls a challenge status using this method.
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
if err != nil {
return nil, err
}
defer res.Body.Close()
v := wireChallenge{URI: url}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.challenge(), nil
}
// Accept informs the server that the client accepts one of its challenges
// previously obtained with c.Authorize.
//
// The server will then perform the validation asynchronously.
func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
res, err := c.post(ctx, nil, chal.URI, json.RawMessage("{}"), wantStatus(
http.StatusOK, // according to the spec
http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md)
))
if err != nil {
return nil, err
}
defer res.Body.Close()
var v wireChallenge
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.challenge(), nil
}
// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response.
// A TXT record containing the returned value must be provisioned under
// "_acme-challenge" name of the domain being validated.
//
// The token argument is a Challenge.Token value.
func (c *Client) DNS01ChallengeRecord(token string) (string, error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(ka))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
// HTTP01ChallengeResponse returns the response for an http-01 challenge.
// Servers should respond with the value to HTTP requests at the URL path
// provided by HTTP01ChallengePath to validate the challenge and prove control
// over a domain name.
//
// The token argument is a Challenge.Token value.
func (c *Client) HTTP01ChallengeResponse(token string) (string, error) {
return keyAuth(c.Key.Public(), token)
}
// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge
// should be provided by the servers.
// The response value can be obtained with HTTP01ChallengeResponse.
//
// The token argument is a Challenge.Token value.
func (c *Client) HTTP01ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response.
//
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec.
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, "", err
}
b := sha256.Sum256([]byte(ka))
h := hex.EncodeToString(b[:])
name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
cert, err = tlsChallengeCert([]string{name}, opt)
if err != nil {
return tls.Certificate{}, "", err
}
return cert, name, nil
}
// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response.
//
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec.
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
b := sha256.Sum256([]byte(token))
h := hex.EncodeToString(b[:])
sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, "", err
}
b = sha256.Sum256([]byte(ka))
h = hex.EncodeToString(b[:])
sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
cert, err = tlsChallengeCert([]string{sanA, sanB}, opt)
if err != nil {
return tls.Certificate{}, "", err
}
return cert, sanA, nil
}
// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
// Servers can present the certificate to validate the challenge and prove control
// over a domain name. For more details on TLS-ALPN-01 see
// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3
//
// The token argument is a Challenge.Token value.
// If a WithKey option is provided, its private part signs the returned cert,
// and the public part is used to specify the signee.
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
//
// The returned certificate is valid for the next 24 hours and must be presented only when
// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol
// has been specified.
func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, err
}
shasum := sha256.Sum256([]byte(ka))
extValue, err := asn1.Marshal(shasum[:])
if err != nil {
return tls.Certificate{}, err
}
acmeExtension := pkix.Extension{
Id: idPeACMEIdentifier,
Critical: true,
Value: extValue,
}
tmpl := defaultTLSChallengeCertTemplate()
var newOpt []CertOption
for _, o := range opt {
switch o := o.(type) {
case *certOptTemplate:
t := *(*x509.Certificate)(o) // shallow copy is ok
tmpl = &t
default:
newOpt = append(newOpt, o)
}
}
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
newOpt = append(newOpt, WithTemplate(tmpl))
return tlsChallengeCert([]string{domain}, newOpt)
}
// popNonce returns a nonce value previously stored with c.addNonce
// or fetches a fresh one from c.dir.NonceURL.
// If NonceURL is empty, it first tries c.directoryURL() and, failing that,
// the provided url.
func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
if len(c.nonces) == 0 {
if c.dir != nil && c.dir.NonceURL != "" {
return c.fetchNonce(ctx, c.dir.NonceURL)
}
dirURL := c.directoryURL()
v, err := c.fetchNonce(ctx, dirURL)
if err != nil && url != dirURL {
v, err = c.fetchNonce(ctx, url)
}
return v, err
}
var nonce string
for nonce = range c.nonces {
delete(c.nonces, nonce)
break
}
return nonce, nil
}
// clearNonces clears any stored nonces
func (c *Client) clearNonces() {
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
c.nonces = make(map[string]struct{})
}
// addNonce stores a nonce value found in h (if any) for future use.
func (c *Client) addNonce(h http.Header) {
v := nonceFromHeader(h)
if v == "" {
return
}
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
if len(c.nonces) >= maxNonces {
return
}
if c.nonces == nil {
c.nonces = make(map[string]struct{})
}
c.nonces[v] = struct{}{}
}
func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) {
r, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return "", err
}
resp, err := c.doNoRetry(ctx, r)
if err != nil {
return "", err
}
defer resp.Body.Close()
nonce := nonceFromHeader(resp.Header)
if nonce == "" {
if resp.StatusCode > 299 {
return "", responseError(resp)
}
return "", errors.New("acme: nonce not found")
}
return nonce, nil
}
func nonceFromHeader(h http.Header) string {
return h.Get("Replay-Nonce")
}
// linkHeader returns URI-Reference values of all Link headers
// with relation-type rel.
// See https://tools.ietf.org/html/rfc5988#section-5 for details.
func linkHeader(h http.Header, rel string) []string {
var links []string
for _, v := range h["Link"] {
parts := strings.Split(v, ";")
for _, p := range parts {
p = strings.TrimSpace(p)
if !strings.HasPrefix(p, "rel=") {
continue
}
if v := strings.Trim(p[4:], `"`); v == rel {
links = append(links, strings.Trim(parts[0], "<>"))
}
}
}
return links
}
// keyAuth generates a key authorization string for a given token.
func keyAuth(pub crypto.PublicKey, token string) (string, error) {
th, err := JWKThumbprint(pub)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", token, th), nil
}
// defaultTLSChallengeCertTemplate is a template used to create challenge certs for TLS challenges.
func defaultTLSChallengeCertTemplate() *x509.Certificate {
return &x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
// with the given SANs and auto-generated public/private key pair.
// The Subject Common Name is set to the first SAN to aid debugging.
// To create a cert with a custom key pair, specify WithKey option.
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
var key crypto.Signer
tmpl := defaultTLSChallengeCertTemplate()
for _, o := range opt {
switch o := o.(type) {
case *certOptKey:
if key != nil {
return tls.Certificate{}, errors.New("acme: duplicate key option")
}
key = o.key
case *certOptTemplate:
t := *(*x509.Certificate)(o) // shallow copy is ok
tmpl = &t
default:
// package's fault, if we let this happen:
panic(fmt.Sprintf("unsupported option type %T", o))
}
}
if key == nil {
var err error
if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil {
return tls.Certificate{}, err
}
}
tmpl.DNSNames = san
if len(san) > 0 {
tmpl.Subject.CommonName = san[0]
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: key,
}, nil
}
// encodePEM returns b encoded as PEM with block of type typ.
func encodePEM(typ string, b []byte) []byte {
pb := &pem.Block{Type: typ, Bytes: b}
return pem.EncodeToMemory(pb)
}
// timeNow is time.Now, except in tests which can mess with it.
var timeNow = time.Now
@@ -0,0 +1,855 @@
// Copyright 2015 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 (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"sort"
"strings"
"testing"
"time"
)
// newTestClient creates a client with a non-nil Directory so that it skips
// the discovery which is otherwise done on the first call of almost every
// exported method.
func newTestClient() *Client {
return &Client{
Key: testKeyEC,
dir: &Directory{}, // skip discovery
}
}
// Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided
// interface.
func decodeJWSRequest(t *testing.T, v interface{}, r io.Reader) {
// Decode request
var req struct{ Payload string }
if err := json.NewDecoder(r).Decode(&req); err != nil {
t.Fatal(err)
}
payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
if err != nil {
t.Fatal(err)
}
err = json.Unmarshal(payload, v)
if err != nil {
t.Fatal(err)
}
}
type jwsHead struct {
Alg string
Nonce string
URL string `json:"url"`
KID string `json:"kid"`
JWK map[string]string `json:"jwk"`
}
func decodeJWSHead(r io.Reader) (*jwsHead, error) {
var req struct{ Protected string }
if err := json.NewDecoder(r).Decode(&req); err != nil {
return nil, err
}
b, err := base64.RawURLEncoding.DecodeString(req.Protected)
if err != nil {
return nil, err
}
var head jwsHead
if err := json.Unmarshal(b, &head); err != nil {
return nil, err
}
return &head, nil
}
func TestRegisterWithoutKey(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.Header().Set("Replay-Nonce", "test-nonce")
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, `{}`)
}))
defer ts.Close()
// First verify that using a complete client results in success.
c := Client{
Key: testKeyEC,
DirectoryURL: ts.URL,
dir: &Directory{RegURL: ts.URL},
}
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err != nil {
t.Fatalf("c.Register() = %v; want success with a complete test client", err)
}
c.Key = nil
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err == nil {
t.Error("c.Register() from client without key succeeded, wanted error")
}
}
func TestAuthorize(t *testing.T) {
tt := []struct{ typ, value string }{
{"dns", "example.com"},
{"ip", "1.2.3.4"},
}
for _, test := range tt {
t.Run(test.typ, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.Header().Set("Replay-Nonce", "test-nonce")
return
}
if r.Method != "POST" {
t.Errorf("r.Method = %q; want POST", r.Method)
}
var j struct {
Resource string
Identifier struct {
Type string
Value string
}
}
decodeJWSRequest(t, &j, r.Body)
// Test request
if j.Resource != "new-authz" {
t.Errorf("j.Resource = %q; want new-authz", j.Resource)
}
if j.Identifier.Type != test.typ {
t.Errorf("j.Identifier.Type = %q; want %q", j.Identifier.Type, test.typ)
}
if j.Identifier.Value != test.value {
t.Errorf("j.Identifier.Value = %q; want %q", j.Identifier.Value, test.value)
}
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{
"identifier": {"type":%q,"value":%q},
"status":"pending",
"challenges":[
{
"type":"http-01",
"status":"pending",
"uri":"https://ca.tld/acme/challenge/publickey/id1",
"token":"token1"
},
{
"type":"tls-sni-01",
"status":"pending",
"uri":"https://ca.tld/acme/challenge/publickey/id2",
"token":"token2"
}
],
"combinations":[[0],[1]]
}`, test.typ, test.value)
}))
defer ts.Close()
var (
auth *Authorization
err error
)
cl := Client{
Key: testKeyEC,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
switch test.typ {
case "dns":
auth, err = cl.Authorize(context.Background(), test.value)
case "ip":
auth, err = cl.AuthorizeIP(context.Background(), test.value)
default:
t.Fatalf("unknown identifier type: %q", test.typ)
}
if err != nil {
t.Fatal(err)
}
if auth.URI != "https://ca.tld/acme/auth/1" {
t.Errorf("URI = %q; want https://ca.tld/acme/auth/1", auth.URI)
}
if auth.Status != "pending" {
t.Errorf("Status = %q; want pending", auth.Status)
}
if auth.Identifier.Type != test.typ {
t.Errorf("Identifier.Type = %q; want %q", auth.Identifier.Type, test.typ)
}
if auth.Identifier.Value != test.value {
t.Errorf("Identifier.Value = %q; want %q", auth.Identifier.Value, test.value)
}
if n := len(auth.Challenges); n != 2 {
t.Fatalf("len(auth.Challenges) = %d; want 2", n)
}
c := auth.Challenges[0]
if c.Type != "http-01" {
t.Errorf("c.Type = %q; want http-01", c.Type)
}
if c.URI != "https://ca.tld/acme/challenge/publickey/id1" {
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI)
}
if c.Token != "token1" {
t.Errorf("c.Token = %q; want token1", c.Token)
}
c = auth.Challenges[1]
if c.Type != "tls-sni-01" {
t.Errorf("c.Type = %q; want tls-sni-01", c.Type)
}
if c.URI != "https://ca.tld/acme/challenge/publickey/id2" {
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI)
}
if c.Token != "token2" {
t.Errorf("c.Token = %q; want token2", c.Token)
}
combs := [][]int{{0}, {1}}
if !reflect.DeepEqual(auth.Combinations, combs) {
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
}
})
}
}
func TestAuthorizeValid(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.Header().Set("Replay-Nonce", "nonce")
return
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"status":"valid"}`))
}))
defer ts.Close()
client := Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
_, err := client.Authorize(context.Background(), "example.com")
if err != nil {
t.Errorf("err = %v", err)
}
}
func TestWaitAuthorization(t *testing.T) {
t.Run("wait loop", func(t *testing.T) {
var count int
authz, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
count++
w.Header().Set("Retry-After", "0")
if count > 1 {
fmt.Fprintf(w, `{"status":"valid"}`)
return
}
fmt.Fprintf(w, `{"status":"pending"}`)
})
if err != nil {
t.Fatalf("non-nil error: %v", err)
}
if authz == nil {
t.Fatal("authz is nil")
}
})
t.Run("invalid status", func(t *testing.T) {
_, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"status":"invalid"}`)
})
if _, ok := err.(*AuthorizationError); !ok {
t.Errorf("err is %v (%T); want non-nil *AuthorizationError", err, err)
}
})
t.Run("invalid status with error returns the authorization error", func(t *testing.T) {
_, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{
"type": "dns-01",
"status": "invalid",
"error": {
"type": "urn:ietf:params:acme:error:caa",
"detail": "CAA record for <domain> prevents issuance",
"status": 403
},
"url": "https://acme-v02.api.letsencrypt.org/acme/chall-v3/xxx/xxx",
"token": "xxx",
"validationRecord": [
{
"hostname": "<domain>"
}
]
}`)
})
want := &AuthorizationError{
Errors: []error{
(&wireError{
Status: 403,
Type: "urn:ietf:params:acme:error:caa",
Detail: "CAA record for <domain> prevents issuance",
}).error(nil),
},
}
_, ok := err.(*AuthorizationError)
if !ok {
t.Errorf("err is %T; want non-nil *AuthorizationError", err)
}
if err.Error() != want.Error() {
t.Errorf("err is %v; want %v", err, want)
}
})
t.Run("non-retriable error", func(t *testing.T) {
const code = http.StatusBadRequest
_, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
})
res, ok := err.(*Error)
if !ok {
t.Fatalf("err is %v (%T); want a non-nil *Error", err, err)
}
if res.StatusCode != code {
t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, code)
}
})
for _, code := range []int{http.StatusTooManyRequests, http.StatusInternalServerError} {
t.Run(fmt.Sprintf("retriable %d error", code), func(t *testing.T) {
var count int
authz, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
count++
w.Header().Set("Retry-After", "0")
if count > 1 {
fmt.Fprintf(w, `{"status":"valid"}`)
return
}
w.WriteHeader(code)
})
if err != nil {
t.Fatalf("non-nil error: %v", err)
}
if authz == nil {
t.Fatal("authz is nil")
}
})
}
t.Run("context cancel", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := runWaitAuthorization(ctx, t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Retry-After", "60")
fmt.Fprintf(w, `{"status":"pending"}`)
time.AfterFunc(1*time.Millisecond, cancel)
})
if err == nil {
t.Error("err is nil")
}
})
}
func runWaitAuthorization(ctx context.Context, t *testing.T, h http.HandlerFunc) (*Authorization, error) {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", fmt.Sprintf("bad-test-nonce-%v", time.Now().UnixNano()))
h(w, r)
}))
defer ts.Close()
client := &Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{},
KID: "some-key-id", // set to avoid lookup attempt
}
return client.WaitAuthorization(ctx, ts.URL)
}
func TestRevokeAuthorization(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.Header().Set("Replay-Nonce", "nonce")
return
}
switch r.URL.Path {
case "/1":
var req struct {
Resource string
Status string
Delete bool
}
decodeJWSRequest(t, &req, r.Body)
if req.Resource != "authz" {
t.Errorf("req.Resource = %q; want authz", req.Resource)
}
if req.Status != "deactivated" {
t.Errorf("req.Status = %q; want deactivated", req.Status)
}
if !req.Delete {
t.Errorf("req.Delete is false")
}
case "/2":
w.WriteHeader(http.StatusBadRequest)
}
}))
defer ts.Close()
client := &Client{
Key: testKey,
DirectoryURL: ts.URL, // don't dial outside of localhost
dir: &Directory{}, // don't do discovery
}
ctx := context.Background()
if err := client.RevokeAuthorization(ctx, ts.URL+"/1"); err != nil {
t.Errorf("err = %v", err)
}
if client.RevokeAuthorization(ctx, ts.URL+"/2") == nil {
t.Error("nil error")
}
}
func TestFetchCertCancel(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
w.Header().Set("Retry-After", "0")
w.WriteHeader(http.StatusBadRequest)
}))
defer ts.Close()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
var err error
go func() {
cl := newTestClient()
_, err = cl.FetchCert(ctx, ts.URL, false)
close(done)
}()
cancel()
<-done
if err != context.Canceled {
t.Errorf("err = %v; want %v", err, context.Canceled)
}
}
func TestFetchCertDepth(t *testing.T) {
var count byte
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
if count > maxChainLen+1 {
t.Errorf("count = %d; want at most %d", count, maxChainLen+1)
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set("Link", fmt.Sprintf("<%s>;rel=up", ts.URL))
w.Write([]byte{count})
}))
defer ts.Close()
cl := newTestClient()
_, err := cl.FetchCert(context.Background(), ts.URL, true)
if err == nil {
t.Errorf("err is nil")
}
}
func TestFetchCertBreadth(t *testing.T) {
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for i := 0; i < maxChainLen+1; i++ {
w.Header().Add("Link", fmt.Sprintf("<%s>;rel=up", ts.URL))
}
w.Write([]byte{1})
}))
defer ts.Close()
cl := newTestClient()
_, err := cl.FetchCert(context.Background(), ts.URL, true)
if err == nil {
t.Errorf("err is nil")
}
}
func TestFetchCertSize(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b := bytes.Repeat([]byte{1}, maxCertSize+1)
w.Write(b)
}))
defer ts.Close()
cl := newTestClient()
_, err := cl.FetchCert(context.Background(), ts.URL, false)
if err == nil {
t.Errorf("err is nil")
}
}
func TestNonce_add(t *testing.T) {
var c Client
c.addNonce(http.Header{"Replay-Nonce": {"nonce"}})
c.addNonce(http.Header{"Replay-Nonce": {}})
c.addNonce(http.Header{"Replay-Nonce": {"nonce"}})
nonces := map[string]struct{}{"nonce": {}}
if !reflect.DeepEqual(c.nonces, nonces) {
t.Errorf("c.nonces = %q; want %q", c.nonces, nonces)
}
}
func TestNonce_addMax(t *testing.T) {
c := &Client{nonces: make(map[string]struct{})}
for i := 0; i < maxNonces; i++ {
c.nonces[fmt.Sprintf("%d", i)] = struct{}{}
}
c.addNonce(http.Header{"Replay-Nonce": {"nonce"}})
if n := len(c.nonces); n != maxNonces {
t.Errorf("len(c.nonces) = %d; want %d", n, maxNonces)
}
}
func TestNonce_fetch(t *testing.T) {
tests := []struct {
code int
nonce string
}{
{http.StatusOK, "nonce1"},
{http.StatusBadRequest, "nonce2"},
{http.StatusOK, ""},
}
var i int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "HEAD" {
t.Errorf("%d: r.Method = %q; want HEAD", i, r.Method)
}
w.Header().Set("Replay-Nonce", tests[i].nonce)
w.WriteHeader(tests[i].code)
}))
defer ts.Close()
for ; i < len(tests); i++ {
test := tests[i]
c := newTestClient()
n, err := c.fetchNonce(context.Background(), ts.URL)
if n != test.nonce {
t.Errorf("%d: n=%q; want %q", i, n, test.nonce)
}
switch {
case err == nil && test.nonce == "":
t.Errorf("%d: n=%q, err=%v; want non-nil error", i, n, err)
case err != nil && test.nonce != "":
t.Errorf("%d: n=%q, err=%v; want %q", i, n, err, test.nonce)
}
}
}
func TestNonce_fetchError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTooManyRequests)
}))
defer ts.Close()
c := newTestClient()
_, err := c.fetchNonce(context.Background(), ts.URL)
e, ok := err.(*Error)
if !ok {
t.Fatalf("err is %T; want *Error", err)
}
if e.StatusCode != http.StatusTooManyRequests {
t.Errorf("e.StatusCode = %d; want %d", e.StatusCode, http.StatusTooManyRequests)
}
}
func TestNonce_popWhenEmpty(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "HEAD" {
t.Errorf("r.Method = %q; want HEAD", r.Method)
}
switch r.URL.Path {
case "/dir-with-nonce":
w.Header().Set("Replay-Nonce", "dirnonce")
case "/new-nonce":
w.Header().Set("Replay-Nonce", "newnonce")
case "/dir-no-nonce", "/empty":
// No nonce in the header.
default:
t.Errorf("Unknown URL: %s", r.URL)
}
}))
defer ts.Close()
ctx := context.Background()
tt := []struct {
dirURL, popURL, nonce string
wantOK bool
}{
{ts.URL + "/dir-with-nonce", ts.URL + "/new-nonce", "dirnonce", true},
{ts.URL + "/dir-no-nonce", ts.URL + "/new-nonce", "newnonce", true},
{ts.URL + "/dir-no-nonce", ts.URL + "/empty", "", false},
}
for _, test := range tt {
t.Run(fmt.Sprintf("nonce:%s wantOK:%v", test.nonce, test.wantOK), func(t *testing.T) {
c := Client{DirectoryURL: test.dirURL}
v, err := c.popNonce(ctx, test.popURL)
if !test.wantOK {
if err == nil {
t.Fatalf("c.popNonce(%q) returned nil error", test.popURL)
}
return
}
if err != nil {
t.Fatalf("c.popNonce(%q): %v", test.popURL, err)
}
if v != test.nonce {
t.Errorf("c.popNonce(%q) = %q; want %q", test.popURL, v, test.nonce)
}
})
}
}
func TestLinkHeader(t *testing.T) {
h := http.Header{"Link": {
`<https://example.com/acme/new-authz>;rel="next"`,
`<https://example.com/acme/recover-reg>; rel=recover`,
`<https://example.com/acme/terms>; foo=bar; rel="terms-of-service"`,
`<dup>;rel="next"`,
}}
tests := []struct {
rel string
out []string
}{
{"next", []string{"https://example.com/acme/new-authz", "dup"}},
{"recover", []string{"https://example.com/acme/recover-reg"}},
{"terms-of-service", []string{"https://example.com/acme/terms"}},
{"empty", nil},
}
for i, test := range tests {
if v := linkHeader(h, test.rel); !reflect.DeepEqual(v, test.out) {
t.Errorf("%d: linkHeader(%q): %v; want %v", i, test.rel, v, test.out)
}
}
}
func TestTLSSNI01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n <token.testKeyECThumbprint> | shasum -a 256
san = "dbbd5eefe7b4d06eb9d1d9f5acb4c7cd.a27d320e4b30332f0b6cb441734ad7b0.acme.invalid"
)
tlscert, name, err := newTestClient().TLSSNI01ChallengeCert(token)
if err != nil {
t.Fatal(err)
}
if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
if len(cert.DNSNames) != 1 || cert.DNSNames[0] != san {
t.Fatalf("cert.DNSNames = %v; want %q", cert.DNSNames, san)
}
if cert.DNSNames[0] != name {
t.Errorf("cert.DNSNames[0] != name: %q vs %q", cert.DNSNames[0], name)
}
if cn := cert.Subject.CommonName; cn != san {
t.Errorf("cert.Subject.CommonName = %q; want %q", cn, san)
}
}
func TestTLSSNI02ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA | shasum -a 256
sanA = "7ea0aaa69214e71e02cebb18bb867736.09b730209baabf60e43d4999979ff139.token.acme.invalid"
// echo -n <token.testKeyECThumbprint> | shasum -a 256
sanB = "dbbd5eefe7b4d06eb9d1d9f5acb4c7cd.a27d320e4b30332f0b6cb441734ad7b0.ka.acme.invalid"
)
tlscert, name, err := newTestClient().TLSSNI02ChallengeCert(token)
if err != nil {
t.Fatal(err)
}
if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
names := []string{sanA, sanB}
if !reflect.DeepEqual(cert.DNSNames, names) {
t.Fatalf("cert.DNSNames = %v;\nwant %v", cert.DNSNames, names)
}
sort.Strings(cert.DNSNames)
i := sort.SearchStrings(cert.DNSNames, name)
if i >= len(cert.DNSNames) || cert.DNSNames[i] != name {
t.Errorf("%v doesn't have %q", cert.DNSNames, name)
}
if cn := cert.Subject.CommonName; cn != sanA {
t.Errorf("CommonName = %q; want %q", cn, sanA)
}
}
func TestTLSALPN01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
keyAuth = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA." + testKeyECThumbprint
// echo -n <token.testKeyECThumbprint> | shasum -a 256
h = "0420dbbd5eefe7b4d06eb9d1d9f5acb4c7cda27d320e4b30332f0b6cb441734ad7b0"
domain = "example.com"
)
extValue, err := hex.DecodeString(h)
if err != nil {
t.Fatal(err)
}
tlscert, err := newTestClient().TLSALPN01ChallengeCert(token, domain)
if err != nil {
t.Fatal(err)
}
if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
names := []string{domain}
if !reflect.DeepEqual(cert.DNSNames, names) {
t.Fatalf("cert.DNSNames = %v;\nwant %v", cert.DNSNames, names)
}
if cn := cert.Subject.CommonName; cn != domain {
t.Errorf("CommonName = %q; want %q", cn, domain)
}
acmeExts := []pkix.Extension{}
for _, ext := range cert.Extensions {
if idPeACMEIdentifier.Equal(ext.Id) {
acmeExts = append(acmeExts, ext)
}
}
if len(acmeExts) != 1 {
t.Errorf("acmeExts = %v; want exactly one", acmeExts)
}
if !acmeExts[0].Critical {
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
}
if bytes.Compare(acmeExts[0].Value, extValue) != 0 {
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
}
}
func TestTLSChallengeCertOpt(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{Organization: []string{"Test"}},
DNSNames: []string{"should-be-overwritten"},
}
opts := []CertOption{WithKey(key), WithTemplate(tmpl)}
client := newTestClient()
cert1, _, err := client.TLSSNI01ChallengeCert("token", opts...)
if err != nil {
t.Fatal(err)
}
cert2, _, err := client.TLSSNI02ChallengeCert("token", opts...)
if err != nil {
t.Fatal(err)
}
for i, tlscert := range []tls.Certificate{cert1, cert2} {
// verify generated cert private key
tlskey, ok := tlscert.PrivateKey.(*rsa.PrivateKey)
if !ok {
t.Errorf("%d: tlscert.PrivateKey is %T; want *rsa.PrivateKey", i, tlscert.PrivateKey)
continue
}
if tlskey.D.Cmp(key.D) != 0 {
t.Errorf("%d: tlskey.D = %v; want %v", i, tlskey.D, key.D)
}
// verify generated cert public key
x509Cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
tlspub, ok := x509Cert.PublicKey.(*rsa.PublicKey)
if !ok {
t.Errorf("%d: x509Cert.PublicKey is %T; want *rsa.PublicKey", i, x509Cert.PublicKey)
continue
}
if tlspub.N.Cmp(key.N) != 0 {
t.Errorf("%d: tlspub.N = %v; want %v", i, tlspub.N, key.N)
}
// verify template option
sn := big.NewInt(2)
if x509Cert.SerialNumber.Cmp(sn) != 0 {
t.Errorf("%d: SerialNumber = %v; want %v", i, x509Cert.SerialNumber, sn)
}
org := []string{"Test"}
if !reflect.DeepEqual(x509Cert.Subject.Organization, org) {
t.Errorf("%d: Subject.Organization = %+v; want %+v", i, x509Cert.Subject.Organization, org)
}
for _, v := range x509Cert.DNSNames {
if !strings.HasSuffix(v, ".acme.invalid") {
t.Errorf("%d: invalid DNSNames element: %q", i, v)
}
}
}
}
func TestHTTP01Challenge(t *testing.T) {
const (
token = "xxx"
// thumbprint is precomputed for testKeyEC in jws_test.go
value = token + "." + testKeyECThumbprint
urlpath = "/.well-known/acme-challenge/" + token
)
client := newTestClient()
val, err := client.HTTP01ChallengeResponse(token)
if err != nil {
t.Fatal(err)
}
if val != value {
t.Errorf("val = %q; want %q", val, value)
}
if path := client.HTTP01ChallengePath(token); path != urlpath {
t.Errorf("path = %q; want %q", path, urlpath)
}
}
func TestDNS01ChallengeRecord(t *testing.T) {
// echo -n xxx.<testKeyECThumbprint> | \
// openssl dgst -binary -sha256 | \
// base64 | tr -d '=' | tr '/+' '_-'
const value = "8DERMexQ5VcdJ_prpPiA0mVdp7imgbCgjsG4SqqNMIo"
val, err := newTestClient().DNS01ChallengeRecord("xxx")
if err != nil {
t.Fatal(err)
}
if val != value {
t.Errorf("val = %q; want %q", val, value)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,996 @@
// Copyright 2016 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 autocert
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert/internal/acmetest"
)
var (
exampleDomain = "example.org"
exampleCertKey = certKey{domain: exampleDomain}
exampleCertKeyRSA = certKey{domain: exampleDomain, isRSA: true}
)
type memCache struct {
t *testing.T
mu sync.Mutex
keyData map[string][]byte
}
func (m *memCache) Get(ctx context.Context, key string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.keyData[key]
if !ok {
return nil, ErrCacheMiss
}
return v, nil
}
// filenameSafe returns whether all characters in s are printable ASCII
// and safe to use in a filename on most filesystems.
func filenameSafe(s string) bool {
for _, c := range s {
if c < 0x20 || c > 0x7E {
return false
}
switch c {
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
return false
}
}
return true
}
func (m *memCache) Put(ctx context.Context, key string, data []byte) error {
if !filenameSafe(key) {
m.t.Errorf("invalid characters in cache key %q", key)
}
m.mu.Lock()
defer m.mu.Unlock()
m.keyData[key] = data
return nil
}
func (m *memCache) Delete(ctx context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.keyData, key)
return nil
}
func newMemCache(t *testing.T) *memCache {
return &memCache{
t: t,
keyData: make(map[string][]byte),
}
}
func (m *memCache) numCerts() int {
m.mu.Lock()
defer m.mu.Unlock()
res := 0
for key := range m.keyData {
if strings.HasSuffix(key, "+token") ||
strings.HasSuffix(key, "+key") ||
strings.HasSuffix(key, "+http-01") {
continue
}
res++
}
return res
}
func dummyCert(pub interface{}, san ...string) ([]byte, error) {
return dateDummyCert(pub, time.Now(), time.Now().Add(90*24*time.Hour), san...)
}
func dateDummyCert(pub interface{}, start, end time.Time, san ...string) ([]byte, error) {
// use EC key to run faster on 386
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
t := &x509.Certificate{
SerialNumber: randomSerial(),
NotBefore: start,
NotAfter: end,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment,
DNSNames: san,
}
if pub == nil {
pub = &key.PublicKey
}
return x509.CreateCertificate(rand.Reader, t, t, pub, key)
}
func randomSerial() *big.Int {
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 32))
if err != nil {
panic(err)
}
return serial
}
type algorithmSupport int
const (
algRSA algorithmSupport = iota
algECDSA
)
func clientHelloInfo(sni string, alg algorithmSupport) *tls.ClientHelloInfo {
hello := &tls.ClientHelloInfo{
ServerName: sni,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
}
if alg == algECDSA {
hello.CipherSuites = append(hello.CipherSuites, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305)
}
return hello
}
func testManager(t *testing.T) *Manager {
man := &Manager{
Prompt: AcceptTOS,
Cache: newMemCache(t),
}
t.Cleanup(man.stopRenew)
return man
}
func TestGetCertificate(t *testing.T) {
tests := []struct {
name string
hello *tls.ClientHelloInfo
domain string
expectError string
prepare func(t *testing.T, man *Manager, s *acmetest.CAServer)
verify func(t *testing.T, man *Manager, leaf *x509.Certificate)
disableALPN bool
disableHTTP bool
}{
{
name: "ALPN",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
disableHTTP: true,
},
{
name: "HTTP",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
disableALPN: true,
},
{
name: "nilPrompt",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
man.Prompt = nil
},
expectError: "Manager.Prompt not set",
},
{
name: "trailingDot",
hello: clientHelloInfo("example.org.", algECDSA),
domain: "example.org",
},
{
name: "unicodeIDN",
hello: clientHelloInfo("éé.com", algECDSA),
domain: "xn--9caa.com",
},
{
name: "unicodeIDN/mixedCase",
hello: clientHelloInfo("éÉ.com", algECDSA),
domain: "xn--9caa.com",
},
{
name: "upperCase",
hello: clientHelloInfo("EXAMPLE.ORG", algECDSA),
domain: "example.org",
},
{
name: "goodCache",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make a valid cert and cache it.
c := s.Start().LeafCert(exampleDomain, "ECDSA",
// Use a time before the Let's Encrypt revocation cutoff to also test
// that non-Let's Encrypt certificates are not renewed.
time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC),
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
)
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
// Break the server to check that the cache is used.
disableALPN: true, disableHTTP: true,
},
{
name: "expiredCache",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make an expired cert and cache it.
c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now().Add(-10*time.Minute), time.Now().Add(-5*time.Minute))
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
},
{
name: "forceRSA",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
man.ForceRSA = true
},
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
t.Errorf("leaf.PublicKey is %T; want *ecdsa.PublicKey", leaf.PublicKey)
}
},
},
{
name: "goodLetsEncrypt",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make a valid certificate issued after the TLS-ALPN-01
// revocation window and cache it.
s.IssuerName(pkix.Name{Country: []string{"US"},
Organization: []string{"Let's Encrypt"}, CommonName: "R3"})
c := s.Start().LeafCert(exampleDomain, "ECDSA",
time.Date(2022, time.January, 26, 12, 0, 0, 0, time.UTC),
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
)
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
// Break the server to check that the cache is used.
disableALPN: true, disableHTTP: true,
},
{
name: "revokedLetsEncrypt",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make a certificate issued during the TLS-ALPN-01
// revocation window and cache it.
s.IssuerName(pkix.Name{Country: []string{"US"},
Organization: []string{"Let's Encrypt"}, CommonName: "R3"})
c := s.Start().LeafCert(exampleDomain, "ECDSA",
time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC),
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
)
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
if leaf.NotBefore.Before(time.Now().Add(-10 * time.Minute)) {
t.Error("certificate was not reissued")
}
},
},
{
// TestGetCertificate/tokenCache tests the fallback of token
// certificate fetches to cache when Manager.certTokens misses.
name: "tokenCacheALPN",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make a separate manager with a shared cache, simulating
// separate nodes that serve requests for the same domain.
man2 := testManager(t)
man2.Cache = man.Cache
// Redirect the verification request to man2, although the
// client request will hit man, testing that they can complete a
// verification by communicating through the cache.
s.ResolveGetCertificate("example.org", man2.GetCertificate)
},
// Drop the default verification paths.
disableALPN: true,
},
{
name: "tokenCacheHTTP",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
man2 := testManager(t)
man2.Cache = man.Cache
s.ResolveHandler("example.org", man2.HTTPHandler(nil))
},
disableHTTP: true,
},
{
name: "ecdsa",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
t.Error("an ECDSA client was served a non-ECDSA certificate")
}
},
},
{
name: "rsa",
hello: clientHelloInfo("example.org", algRSA),
domain: "example.org",
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
if _, ok := leaf.PublicKey.(*rsa.PublicKey); !ok {
t.Error("an RSA client was served a non-RSA certificate")
}
},
},
{
name: "wrongCacheKeyType",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
// Make an RSA cert and cache it without suffix.
c := s.Start().LeafCert(exampleDomain, "RSA", time.Now(), time.Now().Add(90*24*time.Hour))
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
// The RSA cached cert should be silently ignored and replaced.
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
t.Error("an ECDSA client was served a non-ECDSA certificate")
}
if numCerts := man.Cache.(*memCache).numCerts(); numCerts != 1 {
t.Errorf("found %d certificates in cache; want %d", numCerts, 1)
}
},
},
{
name: "almostExpiredCache",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
man.RenewBefore = 24 * time.Hour
// Cache an almost expired cert.
c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now(), time.Now().Add(10*time.Minute))
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
},
},
{
name: "provideExternalAuth",
hello: clientHelloInfo("example.org", algECDSA),
domain: "example.org",
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
s.ExternalAccountRequired()
man.ExternalAccountBinding = &acme.ExternalAccountBinding{
KID: "test-key",
Key: make([]byte, 32),
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
man := testManager(t)
s := acmetest.NewCAServer(t)
if !tt.disableALPN {
s.ResolveGetCertificate(tt.domain, man.GetCertificate)
}
if !tt.disableHTTP {
s.ResolveHandler(tt.domain, man.HTTPHandler(nil))
}
if tt.prepare != nil {
tt.prepare(t, man, s)
}
s.Start()
man.Client = &acme.Client{DirectoryURL: s.URL()}
tlscert, err := man.GetCertificate(tt.hello)
if tt.expectError != "" {
if err == nil {
t.Fatal("expected error, got certificate")
}
if !strings.Contains(err.Error(), tt.expectError) {
t.Errorf("got %q, expected %q", err, tt.expectError)
}
return
}
if err != nil {
t.Fatalf("man.GetCertificate: %v", err)
}
leaf, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
opts := x509.VerifyOptions{
DNSName: tt.domain,
Intermediates: x509.NewCertPool(),
Roots: s.Roots(),
}
for _, cert := range tlscert.Certificate[1:] {
c, err := x509.ParseCertificate(cert)
if err != nil {
t.Fatal(err)
}
opts.Intermediates.AddCert(c)
}
if _, err := leaf.Verify(opts); err != nil {
t.Error(err)
}
if san := leaf.DNSNames[0]; san != tt.domain {
t.Errorf("got SAN %q, expected %q", san, tt.domain)
}
if tt.verify != nil {
tt.verify(t, man, leaf)
}
})
}
}
func TestGetCertificate_failedAttempt(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}))
defer ts.Close()
d := createCertRetryAfter
f := testDidRemoveState
defer func() {
createCertRetryAfter = d
testDidRemoveState = f
}()
createCertRetryAfter = 0
done := make(chan struct{})
testDidRemoveState = func(ck certKey) {
if ck != exampleCertKey {
t.Errorf("testDidRemoveState: domain = %v; want %v", ck, exampleCertKey)
}
close(done)
}
man := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{
DirectoryURL: ts.URL,
},
}
defer man.stopRenew()
hello := clientHelloInfo(exampleDomain, algECDSA)
if _, err := man.GetCertificate(hello); err == nil {
t.Error("GetCertificate: err is nil")
}
<-done
man.stateMu.Lock()
defer man.stateMu.Unlock()
if v, exist := man.state[exampleCertKey]; exist {
t.Errorf("state exists for %v: %+v", exampleCertKey, v)
}
}
func TestRevokeFailedAuthz(t *testing.T) {
ca := acmetest.NewCAServer(t)
// Make the authz unfulfillable on the client side, so it will be left
// pending at the end of the verification attempt.
ca.ChallengeTypes("fake-01", "fake-02")
ca.Start()
m := testManager(t)
m.Client = &acme.Client{DirectoryURL: ca.URL()}
_, err := m.GetCertificate(clientHelloInfo("example.org", algECDSA))
if err == nil {
t.Fatal("expected GetCertificate to fail")
}
logTicker := time.NewTicker(3 * time.Second)
defer logTicker.Stop()
for {
authz, err := m.Client.GetAuthorization(context.Background(), ca.URL()+"/authz/0")
if err != nil {
t.Fatal(err)
}
if authz.Status == acme.StatusDeactivated {
return
}
select {
case <-logTicker.C:
t.Logf("still waiting on revocations")
default:
}
time.Sleep(50 * time.Millisecond)
}
}
func TestHTTPHandlerDefaultFallback(t *testing.T) {
tt := []struct {
method, url string
wantCode int
wantLocation string
}{
{"GET", "http://example.org", 302, "https://example.org/"},
{"GET", "http://example.org/foo", 302, "https://example.org/foo"},
{"GET", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"},
{"GET", "http://example.org/?a=b", 302, "https://example.org/?a=b"},
{"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"},
{"GET", "http://example.org:80/foo?a=b", 302, "https://example.org:443/foo?a=b"},
{"GET", "http://example.org:80/foo%20bar", 302, "https://example.org:443/foo%20bar"},
{"GET", "http://[2602:d1:xxxx::c60a]:1234", 302, "https://[2602:d1:xxxx::c60a]:443/"},
{"GET", "http://[2602:d1:xxxx::c60a]", 302, "https://[2602:d1:xxxx::c60a]/"},
{"GET", "http://[2602:d1:xxxx::c60a]/foo?a=b", 302, "https://[2602:d1:xxxx::c60a]/foo?a=b"},
{"HEAD", "http://example.org", 302, "https://example.org/"},
{"HEAD", "http://example.org/foo", 302, "https://example.org/foo"},
{"HEAD", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"},
{"HEAD", "http://example.org/?a=b", 302, "https://example.org/?a=b"},
{"HEAD", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"},
{"POST", "http://example.org", 400, ""},
{"PUT", "http://example.org", 400, ""},
{"GET", "http://example.org/.well-known/acme-challenge/x", 404, ""},
}
var m Manager
h := m.HTTPHandler(nil)
for i, test := range tt {
r := httptest.NewRequest(test.method, test.url, nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != test.wantCode {
t.Errorf("%d: w.Code = %d; want %d", i, w.Code, test.wantCode)
t.Errorf("%d: body: %s", i, w.Body.Bytes())
}
if v := w.Header().Get("Location"); v != test.wantLocation {
t.Errorf("%d: Location = %q; want %q", i, v, test.wantLocation)
}
}
}
func TestAccountKeyCache(t *testing.T) {
m := Manager{Cache: newMemCache(t)}
ctx := context.Background()
k1, err := m.accountKey(ctx)
if err != nil {
t.Fatal(err)
}
k2, err := m.accountKey(ctx)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(k1, k2) {
t.Errorf("account keys don't match: k1 = %#v; k2 = %#v", k1, k2)
}
}
func TestCache(t *testing.T) {
ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
cert, err := dummyCert(ecdsaKey.Public(), exampleDomain)
if err != nil {
t.Fatal(err)
}
ecdsaCert := &tls.Certificate{
Certificate: [][]byte{cert},
PrivateKey: ecdsaKey,
}
rsaKey, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
cert, err = dummyCert(rsaKey.Public(), exampleDomain)
if err != nil {
t.Fatal(err)
}
rsaCert := &tls.Certificate{
Certificate: [][]byte{cert},
PrivateKey: rsaKey,
}
man := &Manager{Cache: newMemCache(t)}
defer man.stopRenew()
ctx := context.Background()
if err := man.cachePut(ctx, exampleCertKey, ecdsaCert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
if err := man.cachePut(ctx, exampleCertKeyRSA, rsaCert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
res, err := man.cacheGet(ctx, exampleCertKey)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
if res == nil || !bytes.Equal(res.Certificate[0], ecdsaCert.Certificate[0]) {
t.Errorf("man.cacheGet = %+v; want %+v", res, ecdsaCert)
}
res, err = man.cacheGet(ctx, exampleCertKeyRSA)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
if res == nil || !bytes.Equal(res.Certificate[0], rsaCert.Certificate[0]) {
t.Errorf("man.cacheGet = %+v; want %+v", res, rsaCert)
}
}
func TestHostWhitelist(t *testing.T) {
policy := HostWhitelist("example.com", "EXAMPLE.ORG", "*.example.net", "éÉ.com")
tt := []struct {
host string
allow bool
}{
{"example.com", true},
{"example.org", true},
{"xn--9caa.com", true}, // éé.com
{"one.example.com", false},
{"two.example.org", false},
{"three.example.net", false},
{"dummy", false},
}
for i, test := range tt {
err := policy(nil, test.host)
if err != nil && test.allow {
t.Errorf("%d: policy(%q): %v; want nil", i, test.host, err)
}
if err == nil && !test.allow {
t.Errorf("%d: policy(%q): nil; want an error", i, test.host)
}
}
}
func TestValidCert(t *testing.T) {
key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
key2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
key3, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
cert1, err := dummyCert(key1.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
cert2, err := dummyCert(key2.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
cert3, err := dummyCert(key3.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
now := time.Now()
early, err := dateDummyCert(key1.Public(), now.Add(time.Hour), now.Add(2*time.Hour), "example.org")
if err != nil {
t.Fatal(err)
}
expired, err := dateDummyCert(key1.Public(), now.Add(-2*time.Hour), now.Add(-time.Hour), "example.org")
if err != nil {
t.Fatal(err)
}
tt := []struct {
ck certKey
key crypto.Signer
cert [][]byte
ok bool
}{
{certKey{domain: "example.org"}, key1, [][]byte{cert1}, true},
{certKey{domain: "example.org", isRSA: true}, key3, [][]byte{cert3}, true},
{certKey{domain: "example.org"}, key1, [][]byte{cert1, cert2, cert3}, true},
{certKey{domain: "example.org"}, key1, [][]byte{cert1, {1}}, false},
{certKey{domain: "example.org"}, key1, [][]byte{{1}}, false},
{certKey{domain: "example.org"}, key1, [][]byte{cert2}, false},
{certKey{domain: "example.org"}, key2, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key1, [][]byte{cert3}, false},
{certKey{domain: "example.org"}, key3, [][]byte{cert1}, false},
{certKey{domain: "example.net"}, key1, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key1, [][]byte{early}, false},
{certKey{domain: "example.org"}, key1, [][]byte{expired}, false},
{certKey{domain: "example.org", isRSA: true}, key1, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key3, [][]byte{cert3}, false},
}
for i, test := range tt {
leaf, err := validCert(test.ck, test.cert, test.key, now)
if err != nil && test.ok {
t.Errorf("%d: err = %v", i, err)
}
if err == nil && !test.ok {
t.Errorf("%d: err is nil", i)
}
if err == nil && test.ok && leaf == nil {
t.Errorf("%d: leaf is nil", i)
}
}
}
type cacheGetFunc func(ctx context.Context, key string) ([]byte, error)
func (f cacheGetFunc) Get(ctx context.Context, key string) ([]byte, error) {
return f(ctx, key)
}
func (f cacheGetFunc) Put(ctx context.Context, key string, data []byte) error {
return fmt.Errorf("unsupported Put of %q = %q", key, data)
}
func (f cacheGetFunc) Delete(ctx context.Context, key string) error {
return fmt.Errorf("unsupported Delete of %q", key)
}
func TestManagerGetCertificateBogusSNI(t *testing.T) {
m := Manager{
Prompt: AcceptTOS,
Cache: cacheGetFunc(func(ctx context.Context, key string) ([]byte, error) {
return nil, fmt.Errorf("cache.Get of %s", key)
}),
}
tests := []struct {
name string
wantErr string
}{
{"foo.com", "cache.Get of foo.com"},
{"foo.com.", "cache.Get of foo.com"},
{`a\b.com`, "acme/autocert: server name contains invalid character"},
{`a/b.com`, "acme/autocert: server name contains invalid character"},
{"", "acme/autocert: missing server name"},
{"foo", "acme/autocert: server name component count invalid"},
{".foo", "acme/autocert: server name component count invalid"},
{"foo.", "acme/autocert: server name component count invalid"},
{"fo.o", "cache.Get of fo.o"},
}
for _, tt := range tests {
_, err := m.GetCertificate(clientHelloInfo(tt.name, algECDSA))
got := fmt.Sprint(err)
if got != tt.wantErr {
t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr)
}
}
}
func TestCertRequest(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
// An extension from RFC7633. Any will do.
ext := pkix.Extension{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1},
Value: []byte("dummy"),
}
b, err := certRequest(key, "example.org", []pkix.Extension{ext})
if err != nil {
t.Fatalf("certRequest: %v", err)
}
r, err := x509.ParseCertificateRequest(b)
if err != nil {
t.Fatalf("ParseCertificateRequest: %v", err)
}
var found bool
for _, v := range r.Extensions {
if v.Id.Equal(ext.Id) {
found = true
break
}
}
if !found {
t.Errorf("want %v in Extensions: %v", ext, r.Extensions)
}
}
func TestSupportsECDSA(t *testing.T) {
tests := []struct {
CipherSuites []uint16
SignatureSchemes []tls.SignatureScheme
SupportedCurves []tls.CurveID
ecdsaOk bool
}{
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}, nil, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, nil, nil, true},
// SignatureSchemes limits, not extends, CipherSuites
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256,
}, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, nil, true},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, []tls.CurveID{
tls.CurveP521,
}, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, []tls.CurveID{
tls.CurveP256,
tls.CurveP521,
}, true},
}
for i, tt := range tests {
result := supportsECDSA(&tls.ClientHelloInfo{
CipherSuites: tt.CipherSuites,
SignatureSchemes: tt.SignatureSchemes,
SupportedCurves: tt.SupportedCurves,
})
if result != tt.ecdsaOk {
t.Errorf("%d: supportsECDSA = %v; want %v", i, result, tt.ecdsaOk)
}
}
}
func TestEndToEndALPN(t *testing.T) {
const domain = "example.org"
// ACME CA server
ca := acmetest.NewCAServer(t).Start()
// User HTTPS server.
m := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{DirectoryURL: ca.URL()},
}
us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
us.TLS = &tls.Config{
NextProtos: []string{"http/1.1", acme.ALPNProto},
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := m.GetCertificate(hello)
if err != nil {
t.Errorf("m.GetCertificate: %v", err)
}
return cert, err
},
}
us.StartTLS()
defer us.Close()
// In TLS-ALPN challenge verification, CA connects to the domain:443 in question.
// Because the domain won't resolve in tests, we need to tell the CA
// where to dial to instead.
ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://"))
// A client visiting user's HTTPS server.
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: ca.Roots(),
ServerName: domain,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(us.URL)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if v := string(b); v != "OK" {
t.Errorf("user server response: %q; want 'OK'", v)
}
}
func TestEndToEndHTTP(t *testing.T) {
const domain = "example.org"
// ACME CA server.
ca := acmetest.NewCAServer(t).ChallengeTypes("http-01").Start()
// User HTTP server for the ACME challenge.
m := testManager(t)
m.Client = &acme.Client{DirectoryURL: ca.URL()}
s := httptest.NewServer(m.HTTPHandler(nil))
defer s.Close()
// User HTTPS server.
ss := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
ss.TLS = &tls.Config{
NextProtos: []string{"http/1.1", acme.ALPNProto},
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := m.GetCertificate(hello)
if err != nil {
t.Errorf("m.GetCertificate: %v", err)
}
return cert, err
},
}
ss.StartTLS()
defer ss.Close()
// Redirect the CA requests to the HTTP server.
ca.Resolve(domain, strings.TrimPrefix(s.URL, "http://"))
// A client visiting user's HTTPS server.
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: ca.Roots(),
ServerName: domain,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ss.URL)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if v := string(b); v != "OK" {
t.Errorf("user server response: %q; want 'OK'", v)
}
}
@@ -0,0 +1,135 @@
// Copyright 2016 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 autocert
import (
"context"
"errors"
"os"
"path/filepath"
)
// ErrCacheMiss is returned when a certificate is not found in cache.
var ErrCacheMiss = errors.New("acme/autocert: certificate cache miss")
// Cache is used by Manager to store and retrieve previously obtained certificates
// and other account data as opaque blobs.
//
// Cache implementations should not rely on the key naming pattern. Keys can
// include any printable ASCII characters, except the following: \/:*?"<>|
type Cache interface {
// Get returns a certificate data for the specified key.
// If there's no such key, Get returns ErrCacheMiss.
Get(ctx context.Context, key string) ([]byte, error)
// Put stores the data in the cache under the specified key.
// Underlying implementations may use any data storage format,
// as long as the reverse operation, Get, results in the original data.
Put(ctx context.Context, key string, data []byte) error
// Delete removes a certificate data from the cache under the specified key.
// If there's no such key in the cache, Delete returns nil.
Delete(ctx context.Context, key string) error
}
// DirCache implements Cache using a directory on the local filesystem.
// If the directory does not exist, it will be created with 0700 permissions.
type DirCache string
// Get reads a certificate data from the specified file name.
func (d DirCache) Get(ctx context.Context, name string) ([]byte, error) {
name = filepath.Join(string(d), filepath.Clean("/"+name))
var (
data []byte
err error
done = make(chan struct{})
)
go func() {
data, err = os.ReadFile(name)
close(done)
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
}
if os.IsNotExist(err) {
return nil, ErrCacheMiss
}
return data, err
}
// Put writes the certificate data to the specified file name.
// The file will be created with 0600 permissions.
func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
if err := os.MkdirAll(string(d), 0700); err != nil {
return err
}
done := make(chan struct{})
var err error
go func() {
defer close(done)
var tmp string
if tmp, err = d.writeTempFile(name, data); err != nil {
return
}
defer os.Remove(tmp)
select {
case <-ctx.Done():
// Don't overwrite the file if the context was canceled.
default:
newName := filepath.Join(string(d), filepath.Clean("/"+name))
err = os.Rename(tmp, newName)
}
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
}
return err
}
// Delete removes the specified file name.
func (d DirCache) Delete(ctx context.Context, name string) error {
name = filepath.Join(string(d), filepath.Clean("/"+name))
var (
err error
done = make(chan struct{})
)
go func() {
err = os.Remove(name)
close(done)
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
}
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// writeTempFile writes b to a temporary file, closes the file and returns its path.
func (d DirCache) writeTempFile(prefix string, b []byte) (name string, reterr error) {
// TempFile uses 0600 permissions
f, err := os.CreateTemp(string(d), prefix)
if err != nil {
return "", err
}
defer func() {
if reterr != nil {
os.Remove(f.Name())
}
}()
if _, err := f.Write(b); err != nil {
f.Close()
return "", err
}
return f.Name(), f.Close()
}
@@ -0,0 +1,66 @@
// Copyright 2016 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 autocert
import (
"context"
"os"
"path/filepath"
"reflect"
"testing"
)
// make sure DirCache satisfies Cache interface
var _ Cache = DirCache("/")
func TestDirCache(t *testing.T) {
dir, err := os.MkdirTemp("", "autocert")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir = filepath.Join(dir, "certs") // a nonexistent dir
cache := DirCache(dir)
ctx := context.Background()
// test cache miss
if _, err := cache.Get(ctx, "nonexistent"); err != ErrCacheMiss {
t.Errorf("get: %v; want ErrCacheMiss", err)
}
// test put/get
b1 := []byte{1}
if err := cache.Put(ctx, "dummy", b1); err != nil {
t.Fatalf("put: %v", err)
}
b2, err := cache.Get(ctx, "dummy")
if err != nil {
t.Fatalf("get: %v", err)
}
if !reflect.DeepEqual(b1, b2) {
t.Errorf("b1 = %v; want %v", b1, b2)
}
name := filepath.Join(dir, "dummy")
if _, err := os.Stat(name); err != nil {
t.Error(err)
}
// test put deletes temp file
tmp, err := filepath.Glob(name + "?*")
if err != nil {
t.Error(err)
}
if tmp != nil {
t.Errorf("temp file exists: %s", tmp)
}
// test delete
if err := cache.Delete(ctx, "dummy"); err != nil {
t.Fatalf("delete: %v", err)
}
if _, err := cache.Get(ctx, "dummy"); err != ErrCacheMiss {
t.Errorf("get: %v; want ErrCacheMiss", err)
}
}
@@ -0,0 +1,35 @@
// Copyright 2017 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 autocert_test
import (
"fmt"
"log"
"net/http"
"golang.org/x/crypto/acme/autocert"
)
func ExampleNewListener() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, TLS user! Your config: %+v", r.TLS)
})
log.Fatal(http.Serve(autocert.NewListener("example.com"), mux))
}
func ExampleManager() {
m := &autocert.Manager{
Cache: autocert.DirCache("secret-dir"),
Prompt: autocert.AcceptTOS,
Email: "example@example.org",
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
s := &http.Server{
Addr: ":https",
TLSConfig: m.TLSConfig(),
}
s.ListenAndServeTLS("", "")
}
@@ -0,0 +1,796 @@
// Copyright 2018 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 acmetest provides types for testing acme and autocert packages.
//
// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well.
package acmetest
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/http/httptest"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/acme"
)
// CAServer is a simple test server which implements ACME spec bits needed for testing.
type CAServer struct {
rootKey crypto.Signer
rootCert []byte // DER encoding
rootTemplate *x509.Certificate
t *testing.T
server *httptest.Server
issuer pkix.Name
challengeTypes []string
url string
roots *x509.CertPool
eabRequired bool
mu sync.Mutex
certCount int // number of issued certs
acctRegistered bool // set once an account has been registered
domainAddr map[string]string // domain name to addr:port resolution
domainGetCert map[string]getCertificateFunc // domain name to GetCertificate function
domainHandler map[string]http.Handler // domain name to Handle function
validAuthz map[string]*authorization // valid authz, keyed by domain name
authorizations []*authorization // all authz, index is used as ID
orders []*order // index is used as order ID
errors []error // encountered client errors
}
type getCertificateFunc func(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
// NewCAServer creates a new ACME test server. The returned CAServer issues
// certs signed with the CA roots available in the Roots field.
func NewCAServer(t *testing.T) *CAServer {
ca := &CAServer{t: t,
challengeTypes: []string{"fake-01", "tls-alpn-01", "http-01"},
domainAddr: make(map[string]string),
domainGetCert: make(map[string]getCertificateFunc),
domainHandler: make(map[string]http.Handler),
validAuthz: make(map[string]*authorization),
}
ca.server = httptest.NewUnstartedServer(http.HandlerFunc(ca.handle))
r, err := rand.Int(rand.Reader, big.NewInt(1000000))
if err != nil {
panic(fmt.Sprintf("rand.Int: %v", err))
}
ca.issuer = pkix.Name{
Organization: []string{"Test Acme Co"},
CommonName: "Root CA " + r.String(),
}
return ca
}
func (ca *CAServer) generateRoot() {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: ca.issuer,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
}
cert, err := x509.ParseCertificate(der)
if err != nil {
panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
}
ca.roots = x509.NewCertPool()
ca.roots.AddCert(cert)
ca.rootKey = key
ca.rootCert = der
ca.rootTemplate = tmpl
}
// IssuerName sets the name of the issuing CA.
func (ca *CAServer) IssuerName(name pkix.Name) *CAServer {
if ca.url != "" {
panic("IssuerName must be called before Start")
}
ca.issuer = name
return ca
}
// ChallengeTypes sets the supported challenge types.
func (ca *CAServer) ChallengeTypes(types ...string) *CAServer {
if ca.url != "" {
panic("ChallengeTypes must be called before Start")
}
ca.challengeTypes = types
return ca
}
// URL returns the server address, after Start has been called.
func (ca *CAServer) URL() string {
if ca.url == "" {
panic("URL called before Start")
}
return ca.url
}
// Roots returns a pool cointaining the CA root.
func (ca *CAServer) Roots() *x509.CertPool {
if ca.url == "" {
panic("Roots called before Start")
}
return ca.roots
}
// ExternalAccountRequired makes an EAB JWS required for account registration.
func (ca *CAServer) ExternalAccountRequired() *CAServer {
if ca.url != "" {
panic("ExternalAccountRequired must be called before Start")
}
ca.eabRequired = true
return ca
}
// Start starts serving requests. The server address becomes available in the
// URL field.
func (ca *CAServer) Start() *CAServer {
if ca.url == "" {
ca.generateRoot()
ca.server.Start()
ca.t.Cleanup(ca.server.Close)
ca.url = ca.server.URL
}
return ca
}
func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
return ca.server.URL + fmt.Sprintf(format, arg...)
}
func (ca *CAServer) addr(domain string) (string, bool) {
ca.mu.Lock()
defer ca.mu.Unlock()
addr, ok := ca.domainAddr[domain]
return addr, ok
}
func (ca *CAServer) getCert(domain string) (getCertificateFunc, bool) {
ca.mu.Lock()
defer ca.mu.Unlock()
f, ok := ca.domainGetCert[domain]
return f, ok
}
func (ca *CAServer) getHandler(domain string) (http.Handler, bool) {
ca.mu.Lock()
defer ca.mu.Unlock()
h, ok := ca.domainHandler[domain]
return h, ok
}
func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) {
s := fmt.Sprintf(format, a...)
ca.t.Errorf(format, a...)
http.Error(w, s, code)
}
// Resolve adds a domain to address resolution for the ca to dial to
// when validating challenges for the domain authorization.
func (ca *CAServer) Resolve(domain, addr string) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.domainAddr[domain] = addr
}
// ResolveGetCertificate redirects TLS connections for domain to f when
// validating challenges for the domain authorization.
func (ca *CAServer) ResolveGetCertificate(domain string, f getCertificateFunc) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.domainGetCert[domain] = f
}
// ResolveHandler redirects HTTP requests for domain to f when
// validating challenges for the domain authorization.
func (ca *CAServer) ResolveHandler(domain string, h http.Handler) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.domainHandler[domain] = h
}
type discovery struct {
NewNonce string `json:"newNonce"`
NewAccount string `json:"newAccount"`
NewOrder string `json:"newOrder"`
NewAuthz string `json:"newAuthz"`
Meta discoveryMeta `json:"meta,omitempty"`
}
type discoveryMeta struct {
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
}
type challenge struct {
URI string `json:"uri"`
Type string `json:"type"`
Token string `json:"token"`
}
type authorization struct {
Status string `json:"status"`
Challenges []challenge `json:"challenges"`
domain string
id int
}
type order struct {
Status string `json:"status"`
AuthzURLs []string `json:"authorizations"`
FinalizeURL string `json:"finalize"` // CSR submit URL
CertURL string `json:"certificate"` // already issued cert
leaf []byte // issued cert in DER format
}
func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
ca.t.Logf("%s %s", r.Method, r.URL)
w.Header().Set("Replay-Nonce", "nonce")
// TODO: Verify nonce header for all POST requests.
switch {
default:
ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path)
// Discovery request.
case r.URL.Path == "/":
resp := &discovery{
NewNonce: ca.serverURL("/new-nonce"),
NewAccount: ca.serverURL("/new-account"),
NewOrder: ca.serverURL("/new-order"),
Meta: discoveryMeta{
ExternalAccountRequired: ca.eabRequired,
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(fmt.Sprintf("discovery response: %v", err))
}
// Nonce requests.
case r.URL.Path == "/new-nonce":
// Nonce values are always set. Nothing else to do.
return
// Client key registration request.
case r.URL.Path == "/new-account":
ca.mu.Lock()
defer ca.mu.Unlock()
if ca.acctRegistered {
ca.httpErrorf(w, http.StatusServiceUnavailable, "multiple accounts are not implemented")
return
}
ca.acctRegistered = true
var req struct {
ExternalAccountBinding json.RawMessage
}
if err := decodePayload(&req, r.Body); err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
if ca.eabRequired && len(req.ExternalAccountBinding) == 0 {
ca.httpErrorf(w, http.StatusBadRequest, "registration failed: no JWS for EAB")
return
}
// TODO: Check the user account key against a ca.accountKeys?
w.Header().Set("Location", ca.serverURL("/accounts/1"))
w.WriteHeader(http.StatusCreated)
w.Write([]byte("{}"))
// New order request.
case r.URL.Path == "/new-order":
var req struct {
Identifiers []struct{ Value string }
}
if err := decodePayload(&req, r.Body); err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
ca.mu.Lock()
defer ca.mu.Unlock()
o := &order{Status: acme.StatusPending}
for _, id := range req.Identifiers {
z := ca.authz(id.Value)
o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%d", z.id))
}
orderID := len(ca.orders)
ca.orders = append(ca.orders, o)
w.Header().Set("Location", ca.serverURL("/orders/%d", orderID))
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(o); err != nil {
panic(err)
}
// Existing order status requests.
case strings.HasPrefix(r.URL.Path, "/orders/"):
ca.mu.Lock()
defer ca.mu.Unlock()
o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/"))
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
if err := json.NewEncoder(w).Encode(o); err != nil {
panic(err)
}
// Accept challenge requests.
case strings.HasPrefix(r.URL.Path, "/challenge/"):
parts := strings.Split(r.URL.Path, "/")
typ, id := parts[len(parts)-2], parts[len(parts)-1]
ca.mu.Lock()
supported := false
for _, suppTyp := range ca.challengeTypes {
if suppTyp == typ {
supported = true
}
}
a, err := ca.storedAuthz(id)
ca.mu.Unlock()
if !supported {
ca.httpErrorf(w, http.StatusBadRequest, "unsupported challenge: %v", typ)
return
}
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: %v", err)
return
}
ca.validateChallenge(a, typ)
w.Write([]byte("{}"))
// Get authorization status requests.
case strings.HasPrefix(r.URL.Path, "/authz/"):
var req struct{ Status string }
decodePayload(&req, r.Body)
deactivate := req.Status == "deactivated"
ca.mu.Lock()
defer ca.mu.Unlock()
authz, err := ca.storedAuthz(strings.TrimPrefix(r.URL.Path, "/authz/"))
if err != nil {
ca.httpErrorf(w, http.StatusNotFound, "%v", err)
return
}
if deactivate {
// Note we don't invalidate authorized orders as we should.
authz.Status = "deactivated"
ca.t.Logf("authz %d is now %s", authz.id, authz.Status)
ca.updatePendingOrders()
}
if err := json.NewEncoder(w).Encode(authz); err != nil {
panic(fmt.Sprintf("encoding authz %d: %v", authz.id, err))
}
// Certificate issuance request.
case strings.HasPrefix(r.URL.Path, "/new-cert/"):
ca.mu.Lock()
defer ca.mu.Unlock()
orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/")
o, err := ca.storedOrder(orderID)
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
if o.Status != acme.StatusReady {
ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
return
}
// Validate CSR request.
var req struct {
CSR string `json:"csr"`
}
decodePayload(&req, r.Body)
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
// Issue the certificate.
der, err := ca.leafCert(csr)
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err)
return
}
o.leaf = der
o.CertURL = ca.serverURL("/issued-cert/%s", orderID)
o.Status = acme.StatusValid
if err := json.NewEncoder(w).Encode(o); err != nil {
panic(err)
}
// Already issued cert download requests.
case strings.HasPrefix(r.URL.Path, "/issued-cert/"):
ca.mu.Lock()
defer ca.mu.Unlock()
o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/"))
if err != nil {
ca.httpErrorf(w, http.StatusBadRequest, err.Error())
return
}
if o.Status != acme.StatusValid {
ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
return
}
w.Header().Set("Content-Type", "application/pem-certificate-chain")
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf})
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert})
}
}
// storedOrder retrieves a previously created order at index i.
// It requires ca.mu to be locked.
func (ca *CAServer) storedOrder(i string) (*order, error) {
idx, err := strconv.Atoi(i)
if err != nil {
return nil, fmt.Errorf("storedOrder: %v", err)
}
if idx < 0 {
return nil, fmt.Errorf("storedOrder: invalid order index %d", idx)
}
if idx > len(ca.orders)-1 {
return nil, fmt.Errorf("storedOrder: no such order %d", idx)
}
ca.updatePendingOrders()
return ca.orders[idx], nil
}
// storedAuthz retrieves a previously created authz at index i.
// It requires ca.mu to be locked.
func (ca *CAServer) storedAuthz(i string) (*authorization, error) {
idx, err := strconv.Atoi(i)
if err != nil {
return nil, fmt.Errorf("storedAuthz: %v", err)
}
if idx < 0 {
return nil, fmt.Errorf("storedAuthz: invalid authz index %d", idx)
}
if idx > len(ca.authorizations)-1 {
return nil, fmt.Errorf("storedAuthz: no such authz %d", idx)
}
return ca.authorizations[idx], nil
}
// authz returns an existing valid authorization for the identifier or creates a
// new one. It requires ca.mu to be locked.
func (ca *CAServer) authz(identifier string) *authorization {
authz, ok := ca.validAuthz[identifier]
if !ok {
authzId := len(ca.authorizations)
authz = &authorization{
id: authzId,
domain: identifier,
Status: acme.StatusPending,
}
for _, typ := range ca.challengeTypes {
authz.Challenges = append(authz.Challenges, challenge{
Type: typ,
URI: ca.serverURL("/challenge/%s/%d", typ, authzId),
Token: challengeToken(authz.domain, typ, authzId),
})
}
ca.authorizations = append(ca.authorizations, authz)
}
return authz
}
// leafCert issues a new certificate.
// It requires ca.mu to be locked.
func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
ca.certCount++ // next leaf cert serial number
leaf := &x509.Certificate{
SerialNumber: big.NewInt(int64(ca.certCount)),
Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(90 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: csr.DNSNames,
BasicConstraintsValid: true,
}
if len(csr.DNSNames) == 0 {
leaf.DNSNames = []string{csr.Subject.CommonName}
}
return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
}
// LeafCert issues a leaf certificate.
func (ca *CAServer) LeafCert(name, keyType string, notBefore, notAfter time.Time) *tls.Certificate {
if ca.url == "" {
panic("LeafCert called before Start")
}
ca.mu.Lock()
defer ca.mu.Unlock()
var pk crypto.Signer
switch keyType {
case "RSA":
var err error
pk, err = rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
ca.t.Fatal(err)
}
case "ECDSA":
var err error
pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
ca.t.Fatal(err)
}
default:
panic("LeafCert: unknown key type")
}
ca.certCount++ // next leaf cert serial number
leaf := &x509.Certificate{
SerialNumber: big.NewInt(int64(ca.certCount)),
Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{name},
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, pk.Public(), ca.rootKey)
if err != nil {
ca.t.Fatal(err)
}
return &tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: pk,
}
}
func (ca *CAServer) validateChallenge(authz *authorization, typ string) {
var err error
switch typ {
case "tls-alpn-01":
err = ca.verifyALPNChallenge(authz)
case "http-01":
err = ca.verifyHTTPChallenge(authz)
default:
panic(fmt.Sprintf("validation of %q is not implemented", typ))
}
ca.mu.Lock()
defer ca.mu.Unlock()
if err != nil {
authz.Status = "invalid"
} else {
authz.Status = "valid"
ca.validAuthz[authz.domain] = authz
}
ca.t.Logf("validated %q for %q, err: %v", typ, authz.domain, err)
ca.t.Logf("authz %d is now %s", authz.id, authz.Status)
ca.updatePendingOrders()
}
func (ca *CAServer) updatePendingOrders() {
// Update all pending orders.
// An order becomes "ready" if all authorizations are "valid".
// An order becomes "invalid" if any authorization is "invalid".
// Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6
for i, o := range ca.orders {
if o.Status != acme.StatusPending {
continue
}
countValid, countInvalid := ca.validateAuthzURLs(o.AuthzURLs, i)
if countInvalid > 0 {
o.Status = acme.StatusInvalid
ca.t.Logf("order %d is now invalid", i)
continue
}
if countValid == len(o.AuthzURLs) {
o.Status = acme.StatusReady
o.FinalizeURL = ca.serverURL("/new-cert/%d", i)
ca.t.Logf("order %d is now ready", i)
}
}
}
func (ca *CAServer) validateAuthzURLs(urls []string, orderNum int) (countValid, countInvalid int) {
for _, zurl := range urls {
z, err := ca.storedAuthz(path.Base(zurl))
if err != nil {
ca.t.Logf("no authz %q for order %d", zurl, orderNum)
continue
}
if z.Status == acme.StatusInvalid {
countInvalid++
}
if z.Status == acme.StatusValid {
countValid++
}
}
return countValid, countInvalid
}
func (ca *CAServer) verifyALPNChallenge(a *authorization) error {
const acmeALPNProto = "acme-tls/1"
addr, haveAddr := ca.addr(a.domain)
getCert, haveGetCert := ca.getCert(a.domain)
if !haveAddr && !haveGetCert {
return fmt.Errorf("no resolution information for %q", a.domain)
}
if haveAddr && haveGetCert {
return fmt.Errorf("overlapping resolution information for %q", a.domain)
}
var crt *x509.Certificate
switch {
case haveAddr:
conn, err := tls.Dial("tcp", addr, &tls.Config{
ServerName: a.domain,
InsecureSkipVerify: true,
NextProtos: []string{acmeALPNProto},
MinVersion: tls.VersionTLS12,
})
if err != nil {
return err
}
if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
}
if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
}
crt = conn.ConnectionState().PeerCertificates[0]
case haveGetCert:
hello := &tls.ClientHelloInfo{
ServerName: a.domain,
// TODO: support selecting ECDSA.
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
SupportedProtos: []string{acme.ALPNProto},
SupportedVersions: []uint16{tls.VersionTLS12},
}
c, err := getCert(hello)
if err != nil {
return err
}
crt, err = x509.ParseCertificate(c.Certificate[0])
if err != nil {
return err
}
}
if err := crt.VerifyHostname(a.domain); err != nil {
return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err)
}
// See RFC 8737, Section 6.1.
oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
for _, x := range crt.Extensions {
if x.Id.Equal(oid) {
// TODO: check the token.
return nil
}
}
return fmt.Errorf("verifyTokenCert: no id-pe-acmeIdentifier extension found")
}
func (ca *CAServer) verifyHTTPChallenge(a *authorization) error {
addr, haveAddr := ca.addr(a.domain)
handler, haveHandler := ca.getHandler(a.domain)
if !haveAddr && !haveHandler {
return fmt.Errorf("no resolution information for %q", a.domain)
}
if haveAddr && haveHandler {
return fmt.Errorf("overlapping resolution information for %q", a.domain)
}
token := challengeToken(a.domain, "http-01", a.id)
path := "/.well-known/acme-challenge/" + token
var body string
switch {
case haveAddr:
t := &http.Transport{
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, addr)
},
}
req, err := http.NewRequest("GET", "http://"+a.domain+path, nil)
if err != nil {
return err
}
res, err := t.RoundTrip(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("http token: w.Code = %d; want %d", res.StatusCode, http.StatusOK)
}
b, err := io.ReadAll(res.Body)
if err != nil {
return err
}
body = string(b)
case haveHandler:
r := httptest.NewRequest("GET", path, nil)
r.Host = a.domain
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
return fmt.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK)
}
body = w.Body.String()
}
if !strings.HasPrefix(body, token) {
return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body)
}
return nil
}
func decodePayload(v interface{}, r io.Reader) error {
var req struct{ Payload string }
if err := json.NewDecoder(r).Decode(&req); err != nil {
return err
}
payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
if err != nil {
return err
}
return json.Unmarshal(payload, v)
}
func challengeToken(domain, challType string, authzID int) string {
return fmt.Sprintf("token-%s-%s-%d", domain, challType, authzID)
}
func unique(a []string) []string {
seen := make(map[string]bool)
var res []string
for _, s := range a {
if s != "" && !seen[s] {
seen[s] = true
res = append(res, s)
}
}
return res
}
@@ -0,0 +1,155 @@
// Copyright 2017 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 autocert
import (
"crypto/tls"
"log"
"net"
"os"
"path/filepath"
"runtime"
"time"
)
// NewListener returns a net.Listener that listens on the standard TLS
// port (443) on all interfaces and returns *tls.Conn connections with
// LetsEncrypt certificates for the provided domain or domains.
//
// It enables one-line HTTPS servers:
//
// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler))
//
// NewListener is a convenience function for a common configuration.
// More complex or custom configurations can use the autocert.Manager
// type instead.
//
// Use of this function implies acceptance of the LetsEncrypt Terms of
// Service. If domains is not empty, the provided domains are passed
// to HostWhitelist. If domains is empty, the listener will do
// LetsEncrypt challenges for any requested domain, which is not
// recommended.
//
// Certificates are cached in a "golang-autocert" directory under an
// operating system-specific cache or temp directory. This may not
// be suitable for servers spanning multiple machines.
//
// The returned listener uses a *tls.Config that enables HTTP/2, and
// should only be used with servers that support HTTP/2.
//
// The returned Listener also enables TCP keep-alives on the accepted
// connections. The returned *tls.Conn are returned before their TLS
// handshake has completed.
func NewListener(domains ...string) net.Listener {
m := &Manager{
Prompt: AcceptTOS,
}
if len(domains) > 0 {
m.HostPolicy = HostWhitelist(domains...)
}
dir := cacheDir()
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("warning: autocert.NewListener not using a cache: %v", err)
} else {
m.Cache = DirCache(dir)
}
return m.Listener()
}
// Listener listens on the standard TLS port (443) on all interfaces
// and returns a net.Listener returning *tls.Conn connections.
//
// The returned listener uses a *tls.Config that enables HTTP/2, and
// should only be used with servers that support HTTP/2.
//
// The returned Listener also enables TCP keep-alives on the accepted
// connections. The returned *tls.Conn are returned before their TLS
// handshake has completed.
//
// Unlike NewListener, it is the caller's responsibility to initialize
// the Manager m's Prompt, Cache, HostPolicy, and other desired options.
func (m *Manager) Listener() net.Listener {
ln := &listener{
conf: m.TLSConfig(),
}
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
return ln
}
type listener struct {
conf *tls.Config
tcpListener net.Listener
tcpListenErr error
}
func (ln *listener) Accept() (net.Conn, error) {
if ln.tcpListenErr != nil {
return nil, ln.tcpListenErr
}
conn, err := ln.tcpListener.Accept()
if err != nil {
return nil, err
}
tcpConn := conn.(*net.TCPConn)
// Because Listener is a convenience function, help out with
// this too. This is not possible for the caller to set once
// we return a *tcp.Conn wrapping an inaccessible net.Conn.
// If callers don't want this, they can do things the manual
// way and tweak as needed. But this is what net/http does
// itself, so copy that. If net/http changes, we can change
// here too.
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(3 * time.Minute)
return tls.Server(tcpConn, ln.conf), nil
}
func (ln *listener) Addr() net.Addr {
if ln.tcpListener != nil {
return ln.tcpListener.Addr()
}
// net.Listen failed. Return something non-nil in case callers
// call Addr before Accept:
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443}
}
func (ln *listener) Close() error {
if ln.tcpListenErr != nil {
return ln.tcpListenErr
}
return ln.tcpListener.Close()
}
func homeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
if h := os.Getenv("HOME"); h != "" {
return h
}
return "/"
}
func cacheDir() string {
const base = "golang-autocert"
switch runtime.GOOS {
case "darwin":
return filepath.Join(homeDir(), "Library", "Caches", base)
case "windows":
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
if v := os.Getenv(ev); v != "" {
return filepath.Join(v, base)
}
}
// Worst case:
return filepath.Join(homeDir(), base)
}
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return filepath.Join(xdg, base)
}
return filepath.Join(homeDir(), ".cache", base)
}
@@ -0,0 +1,156 @@
// Copyright 2016 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 autocert
import (
"context"
"crypto"
"sync"
"time"
)
// renewJitter is the maximum deviation from Manager.RenewBefore.
const renewJitter = time.Hour
// domainRenewal tracks the state used by the periodic timers
// renewing a single domain's cert.
type domainRenewal struct {
m *Manager
ck certKey
key crypto.Signer
timerMu sync.Mutex
timer *time.Timer
timerClose chan struct{} // if non-nil, renew closes this channel (and nils out the timer fields) instead of running
}
// start starts a cert renewal timer at the time
// defined by the certificate expiration time exp.
//
// If the timer is already started, calling start is a noop.
func (dr *domainRenewal) start(exp time.Time) {
dr.timerMu.Lock()
defer dr.timerMu.Unlock()
if dr.timer != nil {
return
}
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
}
// stop stops the cert renewal timer and waits for any in-flight calls to renew
// to complete. If the timer is already stopped, calling stop is a noop.
func (dr *domainRenewal) stop() {
dr.timerMu.Lock()
defer dr.timerMu.Unlock()
for {
if dr.timer == nil {
return
}
if dr.timer.Stop() {
dr.timer = nil
return
} else {
// dr.timer fired, and we acquired dr.timerMu before the renew callback did.
// (We know this because otherwise the renew callback would have reset dr.timer!)
timerClose := make(chan struct{})
dr.timerClose = timerClose
dr.timerMu.Unlock()
<-timerClose
dr.timerMu.Lock()
}
}
}
// renew is called periodically by a timer.
// The first renew call is kicked off by dr.start.
func (dr *domainRenewal) renew() {
dr.timerMu.Lock()
defer dr.timerMu.Unlock()
if dr.timerClose != nil {
close(dr.timerClose)
dr.timer, dr.timerClose = nil, nil
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
// TODO: rotate dr.key at some point?
next, err := dr.do(ctx)
if err != nil {
next = renewJitter / 2
next += time.Duration(pseudoRand.int63n(int64(next)))
}
testDidRenewLoop(next, err)
dr.timer = time.AfterFunc(next, dr.renew)
}
// updateState locks and replaces the relevant Manager.state item with the given
// state. It additionally updates dr.key with the given state's key.
func (dr *domainRenewal) updateState(state *certState) {
dr.m.stateMu.Lock()
defer dr.m.stateMu.Unlock()
dr.key = state.key
dr.m.state[dr.ck] = state
}
// do is similar to Manager.createCert but it doesn't lock a Manager.state item.
// Instead, it requests a new certificate independently and, upon success,
// replaces dr.m.state item with a new one and updates cache for the given domain.
//
// It may lock and update the Manager.state if the expiration date of the currently
// cached cert is far enough in the future.
//
// The returned value is a time interval after which the renewal should occur again.
func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
// a race is likely unavoidable in a distributed environment
// but we try nonetheless
if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil {
next := dr.next(tlscert.Leaf.NotAfter)
if next > dr.m.renewBefore()+renewJitter {
signer, ok := tlscert.PrivateKey.(crypto.Signer)
if ok {
state := &certState{
key: signer,
cert: tlscert.Certificate,
leaf: tlscert.Leaf,
}
dr.updateState(state)
return next, nil
}
}
}
der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.ck)
if err != nil {
return 0, err
}
state := &certState{
key: dr.key,
cert: der,
leaf: leaf,
}
tlscert, err := state.tlscert()
if err != nil {
return 0, err
}
if err := dr.m.cachePut(ctx, dr.ck, tlscert); err != nil {
return 0, err
}
dr.updateState(state)
return dr.next(leaf.NotAfter), nil
}
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
d := expiry.Sub(dr.m.now()) - dr.m.renewBefore()
// add a bit of randomness to renew deadline
n := pseudoRand.int63n(int64(renewJitter))
d -= time.Duration(n)
if d < 0 {
return 0
}
return d
}
var testDidRenewLoop = func(next time.Duration, err error) {}
@@ -0,0 +1,269 @@
// Copyright 2016 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 autocert
import (
"context"
"crypto"
"crypto/ecdsa"
"testing"
"time"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert/internal/acmetest"
)
func TestRenewalNext(t *testing.T) {
now := time.Now()
man := &Manager{
RenewBefore: 7 * 24 * time.Hour,
nowFunc: func() time.Time { return now },
}
defer man.stopRenew()
tt := []struct {
expiry time.Time
min, max time.Duration
}{
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour},
{now.Add(time.Hour), 0, 1},
{now, 0, 1},
{now.Add(-time.Hour), 0, 1},
}
dr := &domainRenewal{m: man}
for i, test := range tt {
next := dr.next(test.expiry)
if next < test.min || test.max < next {
t.Errorf("%d: next = %v; want between %v and %v", i, next, test.min, test.max)
}
}
}
func TestRenewFromCache(t *testing.T) {
man := testManager(t)
man.RenewBefore = 24 * time.Hour
ca := acmetest.NewCAServer(t).Start()
ca.ResolveGetCertificate(exampleDomain, man.GetCertificate)
man.Client = &acme.Client{
DirectoryURL: ca.URL(),
}
// cache an almost expired cert
now := time.Now()
c := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Minute))
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
t.Fatal(err)
}
// verify the renewal happened
defer func() {
// Stop the timers that read and execute testDidRenewLoop before restoring it.
// Otherwise the timer callback may race with the deferred write.
man.stopRenew()
testDidRenewLoop = func(next time.Duration, err error) {}
}()
renewed := make(chan bool, 1)
testDidRenewLoop = func(next time.Duration, err error) {
defer func() {
select {
case renewed <- true:
default:
// The renewal timer uses a random backoff. If the first renewal fails for
// some reason, we could end up with multiple calls here before the test
// stops the timer.
}
}()
if err != nil {
t.Errorf("testDidRenewLoop: %v", err)
}
// Next should be about 90 days:
// CaServer creates 90days expiry + account for man.RenewBefore.
// Previous expiration was within 1 min.
future := 88 * 24 * time.Hour
if next < future {
t.Errorf("testDidRenewLoop: next = %v; want >= %v", next, future)
}
// ensure the new cert is cached
after := time.Now().Add(future)
tlscert, err := man.cacheGet(context.Background(), exampleCertKey)
if err != nil {
t.Errorf("man.cacheGet: %v", err)
return
}
if !tlscert.Leaf.NotAfter.After(after) {
t.Errorf("cache leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
}
// verify the old cert is also replaced in memory
man.stateMu.Lock()
defer man.stateMu.Unlock()
s := man.state[exampleCertKey]
if s == nil {
t.Errorf("m.state[%q] is nil", exampleCertKey)
return
}
tlscert, err = s.tlscert()
if err != nil {
t.Errorf("s.tlscert: %v", err)
return
}
if !tlscert.Leaf.NotAfter.After(after) {
t.Errorf("state leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
}
}
// trigger renew
hello := clientHelloInfo(exampleDomain, algECDSA)
if _, err := man.GetCertificate(hello); err != nil {
t.Fatal(err)
}
<-renewed
}
func TestRenewFromCacheAlreadyRenewed(t *testing.T) {
ca := acmetest.NewCAServer(t).Start()
man := testManager(t)
man.RenewBefore = 24 * time.Hour
man.Client = &acme.Client{
DirectoryURL: "invalid",
}
// cache a recently renewed cert with a different private key
now := time.Now()
newCert := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Hour*24*90))
if err := man.cachePut(context.Background(), exampleCertKey, newCert); err != nil {
t.Fatal(err)
}
newLeaf, err := validCert(exampleCertKey, newCert.Certificate, newCert.PrivateKey.(crypto.Signer), now)
if err != nil {
t.Fatal(err)
}
// set internal state to an almost expired cert
oldCert := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Minute))
if err != nil {
t.Fatal(err)
}
oldLeaf, err := validCert(exampleCertKey, oldCert.Certificate, oldCert.PrivateKey.(crypto.Signer), now)
if err != nil {
t.Fatal(err)
}
man.stateMu.Lock()
if man.state == nil {
man.state = make(map[certKey]*certState)
}
s := &certState{
key: oldCert.PrivateKey.(crypto.Signer),
cert: oldCert.Certificate,
leaf: oldLeaf,
}
man.state[exampleCertKey] = s
man.stateMu.Unlock()
// verify the renewal accepted the newer cached cert
defer func() {
// Stop the timers that read and execute testDidRenewLoop before restoring it.
// Otherwise the timer callback may race with the deferred write.
man.stopRenew()
testDidRenewLoop = func(next time.Duration, err error) {}
}()
renewed := make(chan bool, 1)
testDidRenewLoop = func(next time.Duration, err error) {
defer func() {
select {
case renewed <- true:
default:
// The renewal timer uses a random backoff. If the first renewal fails for
// some reason, we could end up with multiple calls here before the test
// stops the timer.
}
}()
if err != nil {
t.Errorf("testDidRenewLoop: %v", err)
}
// Next should be about 90 days
// Previous expiration was within 1 min.
future := 88 * 24 * time.Hour
if next < future {
t.Errorf("testDidRenewLoop: next = %v; want >= %v", next, future)
}
// ensure the cached cert was not modified
tlscert, err := man.cacheGet(context.Background(), exampleCertKey)
if err != nil {
t.Errorf("man.cacheGet: %v", err)
return
}
if !tlscert.Leaf.NotAfter.Equal(newLeaf.NotAfter) {
t.Errorf("cache leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter)
}
// verify the old cert is also replaced in memory
man.stateMu.Lock()
defer man.stateMu.Unlock()
s := man.state[exampleCertKey]
if s == nil {
t.Errorf("m.state[%q] is nil", exampleCertKey)
return
}
stateKey := s.key.Public().(*ecdsa.PublicKey)
if !stateKey.Equal(newLeaf.PublicKey) {
t.Error("state key was not updated from cache")
return
}
tlscert, err = s.tlscert()
if err != nil {
t.Errorf("s.tlscert: %v", err)
return
}
if !tlscert.Leaf.NotAfter.Equal(newLeaf.NotAfter) {
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter)
}
}
// assert the expiring cert is returned from state
hello := clientHelloInfo(exampleDomain, algECDSA)
tlscert, err := man.GetCertificate(hello)
if err != nil {
t.Fatal(err)
}
if !oldLeaf.NotAfter.Equal(tlscert.Leaf.NotAfter) {
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, oldLeaf.NotAfter)
}
// trigger renew
man.startRenew(exampleCertKey, s.key, s.leaf.NotAfter)
<-renewed
func() {
man.renewalMu.Lock()
defer man.renewalMu.Unlock()
// verify the private key is replaced in the renewal state
r := man.renewal[exampleCertKey]
if r == nil {
t.Errorf("m.renewal[%q] is nil", exampleCertKey)
return
}
renewalKey := r.key.Public().(*ecdsa.PublicKey)
if !renewalKey.Equal(newLeaf.PublicKey) {
t.Error("renewal private key was not updated from cache")
}
}()
// assert the new cert is returned from state after renew
hello = clientHelloInfo(exampleDomain, algECDSA)
tlscert, err = man.GetCertificate(hello)
if err != nil {
t.Fatal(err)
}
if !newLeaf.NotAfter.Equal(tlscert.Leaf.NotAfter) {
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter)
}
}
@@ -0,0 +1,325 @@
// Copyright 2018 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 (
"bytes"
"context"
"crypto"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strconv"
"strings"
"time"
)
// retryTimer encapsulates common logic for retrying unsuccessful requests.
// It is not safe for concurrent use.
type retryTimer struct {
// backoffFn provides backoff delay sequence for retries.
// See Client.RetryBackoff doc comment.
backoffFn func(n int, r *http.Request, res *http.Response) time.Duration
// n is the current retry attempt.
n int
}
func (t *retryTimer) inc() {
t.n++
}
// backoff pauses the current goroutine as described in Client.RetryBackoff.
func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error {
d := t.backoffFn(t.n, r, res)
if d <= 0 {
return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n)
}
wakeup := time.NewTimer(d)
defer wakeup.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeup.C:
return nil
}
}
func (c *Client) retryTimer() *retryTimer {
f := c.RetryBackoff
if f == nil {
f = defaultBackoff
}
return &retryTimer{backoffFn: f}
}
// defaultBackoff provides default Client.RetryBackoff implementation
// using a truncated exponential backoff algorithm,
// as described in Client.RetryBackoff.
//
// The n argument is always bounded between 1 and 30.
// The returned value is always greater than 0.
func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
const max = 10 * time.Second
var jitter time.Duration
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
// Set the minimum to 1ms to avoid a case where
// an invalid Retry-After value is parsed into 0 below,
// resulting in the 0 returned value which would unintentionally
// stop the retries.
jitter = (1 + time.Duration(x.Int64())) * time.Millisecond
}
if v, ok := res.Header["Retry-After"]; ok {
return retryAfter(v[0]) + jitter
}
if n < 1 {
n = 1
}
if n > 30 {
n = 30
}
d := time.Duration(1<<uint(n-1))*time.Second + jitter
if d > max {
return max
}
return d
}
// retryAfter parses a Retry-After HTTP header value,
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
// It returns zero value if v cannot be parsed.
func retryAfter(v string) time.Duration {
if i, err := strconv.Atoi(v); err == nil {
return time.Duration(i) * time.Second
}
t, err := http.ParseTime(v)
if err != nil {
return 0
}
return t.Sub(timeNow())
}
// resOkay is a function that reports whether the provided response is okay.
// It is expected to keep the response body unread.
type resOkay func(*http.Response) bool
// wantStatus returns a function which reports whether the code
// matches the status code of a response.
func wantStatus(codes ...int) resOkay {
return func(res *http.Response) bool {
for _, code := range codes {
if code == res.StatusCode {
return true
}
}
return false
}
}
// get issues an unsigned GET request to the specified URL.
// It returns a non-error value only when ok reports true.
//
// get retries unsuccessful attempts according to c.RetryBackoff
// until the context is done or a non-retriable error is received.
func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
retry := c.retryTimer()
for {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := c.doNoRetry(ctx, req)
switch {
case err != nil:
return nil, err
case ok(res):
return res, nil
case isRetriable(res.StatusCode):
retry.inc()
resErr := responseError(res)
res.Body.Close()
// Ignore the error value from retry.backoff
// and return the one from last retry, as received from the CA.
if retry.backoff(ctx, req, res) != nil {
return nil, resErr
}
default:
defer res.Body.Close()
return nil, responseError(res)
}
}
}
// postAsGet is POST-as-GET, a replacement for GET in RFC 8555
// as described in https://tools.ietf.org/html/rfc8555#section-6.3.
// It makes a POST request in KID form with zero JWS payload.
// See nopayload doc comments in jws.go.
func (c *Client) postAsGet(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
return c.post(ctx, nil, url, noPayload, ok)
}
// post issues a signed POST request in JWS format using the provided key
// to the specified URL. If key is nil, c.Key is used instead.
// It returns a non-error value only when ok reports true.
//
// post retries unsuccessful attempts according to c.RetryBackoff
// until the context is done or a non-retriable error is received.
// It uses postNoRetry to make individual requests.
func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) {
retry := c.retryTimer()
for {
res, req, err := c.postNoRetry(ctx, key, url, body)
if err != nil {
return nil, err
}
if ok(res) {
return res, nil
}
resErr := responseError(res)
res.Body.Close()
switch {
// Check for bad nonce before isRetriable because it may have been returned
// with an unretriable response code such as 400 Bad Request.
case isBadNonce(resErr):
// Consider any previously stored nonce values to be invalid.
c.clearNonces()
case !isRetriable(res.StatusCode):
return nil, resErr
}
retry.inc()
// Ignore the error value from retry.backoff
// and return the one from last retry, as received from the CA.
if err := retry.backoff(ctx, req, res); err != nil {
return nil, resErr
}
}
}
// postNoRetry signs the body with the given key and POSTs it to the provided url.
// It is used by c.post to retry unsuccessful attempts.
// The body argument must be JSON-serializable.
//
// If key argument is nil, c.Key is used to sign the request.
// If key argument is nil and c.accountKID returns a non-zero keyID,
// the request is sent in KID form. Otherwise, JWK form is used.
//
// In practice, when interfacing with RFC-compliant CAs most requests are sent in KID form
// and JWK is used only when KID is unavailable: new account endpoint and certificate
// revocation requests authenticated by a cert key.
// See jwsEncodeJSON for other details.
func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
kid := noKeyID
if key == nil {
if c.Key == nil {
return nil, nil, errors.New("acme: Client.Key must be populated to make POST requests")
}
key = c.Key
kid = c.accountKID(ctx)
}
nonce, err := c.popNonce(ctx, url)
if err != nil {
return nil, nil, err
}
b, err := jwsEncodeJSON(body, key, kid, nonce, url)
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/jose+json")
res, err := c.doNoRetry(ctx, req)
if err != nil {
return nil, nil, err
}
c.addNonce(res.Header)
return res, req, nil
}
// doNoRetry issues a request req, replacing its context (if any) with ctx.
func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", c.userAgent())
res, err := c.httpClient().Do(req.WithContext(ctx))
if err != nil {
select {
case <-ctx.Done():
// Prefer the unadorned context error.
// (The acme package had tests assuming this, previously from ctxhttp's
// behavior, predating net/http supporting contexts natively)
// TODO(bradfitz): reconsider this in the future. But for now this
// requires no test updates.
return nil, ctx.Err()
default:
return nil, err
}
}
return res, nil
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
// packageVersion is the version of the module that contains this package, for
// sending as part of the User-Agent header. It's set in version_go112.go.
var packageVersion string
// userAgent returns the User-Agent header value. It includes the package name,
// the module version (if available), and the c.UserAgent value (if set).
func (c *Client) userAgent() string {
ua := "golang.org/x/crypto/acme"
if packageVersion != "" {
ua += "@" + packageVersion
}
if c.UserAgent != "" {
ua = c.UserAgent + " " + ua
}
return ua
}
// isBadNonce reports whether err is an ACME "badnonce" error.
func isBadNonce(err error) bool {
// According to the spec badNonce is urn:ietf:params:acme:error:badNonce.
// However, ACME servers in the wild return their versions of the error.
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4
// and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66.
ae, ok := err.(*Error)
return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce")
}
// isRetriable reports whether a request can be retried
// based on the response status code.
//
// Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code.
// Callers should parse the response and check with isBadNonce.
func isRetriable(code int) bool {
return code <= 399 || code >= 500 || code == http.StatusTooManyRequests
}
// responseError creates an error of Error type from resp.
func responseError(resp *http.Response) error {
// don't care if ReadAll returns an error:
// json.Unmarshal will fail in that case anyway
b, _ := io.ReadAll(resp.Body)
e := &wireError{Status: resp.StatusCode}
if err := json.Unmarshal(b, e); err != nil {
// this is not a regular error response:
// populate detail with anything we received,
// e.Status will already contain HTTP response code value
e.Detail = string(b)
if e.Detail == "" {
e.Detail = resp.Status
}
}
return e.error(resp.Header)
}
@@ -0,0 +1,255 @@
// Copyright 2018 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"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
)
func TestDefaultBackoff(t *testing.T) {
tt := []struct {
nretry int
retryAfter string // Retry-After header
out time.Duration // expected min; max = min + jitter
}{
{-1, "", time.Second}, // verify the lower bound is 1
{0, "", time.Second}, // verify the lower bound is 1
{100, "", 10 * time.Second}, // verify the ceiling
{1, "3600", time.Hour}, // verify the header value is used
{1, "", 1 * time.Second},
{2, "", 2 * time.Second},
{3, "", 4 * time.Second},
{4, "", 8 * time.Second},
}
for i, test := range tt {
r := httptest.NewRequest("GET", "/", nil)
resp := &http.Response{Header: http.Header{}}
if test.retryAfter != "" {
resp.Header.Set("Retry-After", test.retryAfter)
}
d := defaultBackoff(test.nretry, r, resp)
max := test.out + time.Second // + max jitter
if d < test.out || max < d {
t.Errorf("%d: defaultBackoff(%v) = %v; want between %v and %v", i, test.nretry, d, test.out, max)
}
}
}
func TestErrorResponse(t *testing.T) {
s := `{
"status": 400,
"type": "urn:acme:error:xxx",
"detail": "text"
}`
res := &http.Response{
StatusCode: 400,
Status: "400 Bad Request",
Body: io.NopCloser(strings.NewReader(s)),
Header: http.Header{"X-Foo": {"bar"}},
}
err := responseError(res)
v, ok := err.(*Error)
if !ok {
t.Fatalf("err = %+v (%T); want *Error type", err, err)
}
if v.StatusCode != 400 {
t.Errorf("v.StatusCode = %v; want 400", v.StatusCode)
}
if v.ProblemType != "urn:acme:error:xxx" {
t.Errorf("v.ProblemType = %q; want urn:acme:error:xxx", v.ProblemType)
}
if v.Detail != "text" {
t.Errorf("v.Detail = %q; want text", v.Detail)
}
if !reflect.DeepEqual(v.Header, res.Header) {
t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header)
}
}
func TestPostWithRetries(t *testing.T) {
var count int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count))
if r.Method == "HEAD" {
// We expect the client to do 2 head requests to fetch
// nonces, one to start and another after getting badNonce
return
}
head, err := decodeJWSHead(r.Body)
switch {
case err != nil:
t.Errorf("decodeJWSHead: %v", err)
case head.Nonce == "":
t.Error("head.Nonce is empty")
case head.Nonce == "nonce1":
// Return a badNonce error to force the call to retry.
w.Header().Set("Retry-After", "0")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
return
}
// Make client.Authorize happy; we're not testing its result.
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"status":"valid"}`))
}))
defer ts.Close()
client := &Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
// This call will fail with badNonce, causing a retry
if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
t.Errorf("client.Authorize 1: %v", err)
}
if count != 3 {
t.Errorf("total requests count: %d; want 3", count)
}
}
func TestRetryErrorType(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"type":"rateLimited"}`))
}))
defer ts.Close()
client := &Client{
Key: testKey,
RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
// Do no retries.
return 0
},
dir: &Directory{AuthzURL: ts.URL},
}
t.Run("post", func(t *testing.T) {
testRetryErrorType(t, func() error {
_, err := client.Authorize(context.Background(), "example.com")
return err
})
})
t.Run("get", func(t *testing.T) {
testRetryErrorType(t, func() error {
_, err := client.GetAuthorization(context.Background(), ts.URL)
return err
})
})
}
func testRetryErrorType(t *testing.T, callClient func() error) {
t.Helper()
err := callClient()
if err == nil {
t.Fatal("client.Authorize returned nil error")
}
acmeErr, ok := err.(*Error)
if !ok {
t.Fatalf("err is %v (%T); want *Error", err, err)
}
if acmeErr.StatusCode != http.StatusTooManyRequests {
t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
}
if acmeErr.ProblemType != "rateLimited" {
t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
}
}
func TestRetryBackoffArgs(t *testing.T) {
const resCode = http.StatusInternalServerError
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "test-nonce")
w.WriteHeader(resCode)
}))
defer ts.Close()
// Canceled in backoff.
ctx, cancel := context.WithCancel(context.Background())
var nretry int
backoff := func(n int, r *http.Request, res *http.Response) time.Duration {
nretry++
if n != nretry {
t.Errorf("n = %d; want %d", n, nretry)
}
if nretry == 3 {
cancel()
}
if r == nil {
t.Error("r is nil")
}
if res.StatusCode != resCode {
t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, resCode)
}
return time.Millisecond
}
client := &Client{
Key: testKey,
RetryBackoff: backoff,
dir: &Directory{AuthzURL: ts.URL},
}
if _, err := client.Authorize(ctx, "example.com"); err == nil {
t.Error("err is nil")
}
if nretry != 3 {
t.Errorf("nretry = %d; want 3", nretry)
}
}
func TestUserAgent(t *testing.T) {
for _, custom := range []string{"", "CUSTOM_UA"} {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Log(r.UserAgent())
if s := "golang.org/x/crypto/acme"; !strings.Contains(r.UserAgent(), s) {
t.Errorf("expected User-Agent to contain %q, got %q", s, r.UserAgent())
}
if !strings.Contains(r.UserAgent(), custom) {
t.Errorf("expected User-Agent to contain %q, got %q", custom, r.UserAgent())
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"newOrder": "sure"}`))
}))
defer ts.Close()
client := &Client{
Key: testKey,
DirectoryURL: ts.URL,
UserAgent: custom,
}
if _, err := client.Discover(context.Background()); err != nil {
t.Errorf("client.Discover: %v", err)
}
}
}
func TestAccountKidLoop(t *testing.T) {
// if Client.postNoRetry is called with a nil key argument
// then Client.Key must be set, otherwise we fall into an
// infinite loop (which also causes a deadlock).
client := &Client{dir: &Directory{OrderURL: ":)"}}
_, _, err := client.postNoRetry(context.Background(), nil, "", nil)
if err == nil {
t.Fatal("Client.postNoRetry didn't fail with a nil key")
}
expected := "acme: Client.Key must be populated to make POST requests"
if err.Error() != expected {
t.Fatalf("Unexpected error returned: wanted %q, got %q", expected, err.Error())
}
}
@@ -0,0 +1,433 @@
// 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.
// The acmeprober program runs against an actual ACME CA implementation.
// It spins up an HTTP server to fulfill authorization challenges
// or execute a DNS script to provision a response to dns-01 challenge.
//
// For http-01 and tls-alpn-01 challenge types this requires the ACME CA
// to be able to reach the HTTP server.
//
// A usage example:
//
// go run prober.go \
// -d https://acme-staging-v02.api.letsencrypt.org/directory \
// -f order \
// -t http-01 \
// -a :8080 \
// -domain some.example.org
//
// The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
// in order for the test to be able to fulfill http-01 challenge.
// To test tls-alpn-01 challenge, 443 port would need to be tunneled
// to 0.0.0.0:8080.
// When running with dns-01 challenge type, use -s argument instead of -a.
package main
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"
"golang.org/x/crypto/acme"
)
var (
// ACME CA directory URL.
// Let's Encrypt v2 prod: https://acme-v02.api.letsencrypt.org/directory
// Let's Encrypt v2 staging: https://acme-staging-v02.api.letsencrypt.org/directory
// See the following for more CAs implementing ACME protocol:
// https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment#CAs_&_PKIs_that_offer_ACME_certificates
directory = flag.String("d", "", "ACME directory URL.")
reginfo = flag.String("r", "", "ACME account registration info.")
flow = flag.String("f", "", `Flow to run: "order" or "preauthz" (RFC8555).`)
chaltyp = flag.String("t", "", "Challenge type: tls-alpn-01, http-01 or dns-01.")
addr = flag.String("a", "", "Local server address for tls-alpn-01 and http-01.")
dnsscript = flag.String("s", "", "Script to run for provisioning dns-01 challenges.")
domain = flag.String("domain", "", "Space separate domain identifiers.")
ipaddr = flag.String("ip", "", "Space separate IP address identifiers.")
)
func main() {
flag.Usage = func() {
fmt.Fprintln(flag.CommandLine.Output(), `
The prober program runs against an actual ACME CA implementation.
It spins up an HTTP server to fulfill authorization challenges
or execute a DNS script to provision a response to dns-01 challenge.
For http-01 and tls-alpn-01 challenge types this requires the ACME CA
to be able to reach the HTTP server.
A usage example:
go run prober.go \
-d https://acme-staging-v02.api.letsencrypt.org/directory \
-f order \
-t http-01 \
-a :8080 \
-domain some.example.org
The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
in order for the test to be able to fulfill http-01 challenge.
To test tls-alpn-01 challenge, 443 port would need to be tunneled
to 0.0.0.0:8080.
When running with dns-01 challenge type, use -s argument instead of -a.
`)
flag.PrintDefaults()
}
flag.Parse()
identifiers := acme.DomainIDs(strings.Fields(*domain)...)
identifiers = append(identifiers, acme.IPIDs(strings.Fields(*ipaddr)...)...)
if len(identifiers) == 0 {
log.Fatal("at least one domain or IP addr identifier is required")
}
// Duration of the whole run.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
// Create and register a new account.
akey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
cl := &acme.Client{Key: akey, DirectoryURL: *directory}
a := &acme.Account{Contact: strings.Fields(*reginfo)}
if _, err := cl.Register(ctx, a, acme.AcceptTOS); err != nil {
log.Fatalf("Register: %v", err)
}
// Run the desired flow test.
p := &prober{
client: cl,
chalType: *chaltyp,
localAddr: *addr,
dnsScript: *dnsscript,
}
switch *flow {
case "order":
p.runOrder(ctx, identifiers)
case "preauthz":
p.runPreauthz(ctx, identifiers)
default:
log.Fatalf("unknown flow: %q", *flow)
}
if len(p.errors) > 0 {
os.Exit(1)
}
}
type prober struct {
client *acme.Client
chalType string
localAddr string
dnsScript string
errors []error
}
func (p *prober) errorf(format string, a ...interface{}) {
err := fmt.Errorf(format, a...)
log.Print(err)
p.errors = append(p.errors, err)
}
func (p *prober) runOrder(ctx context.Context, identifiers []acme.AuthzID) {
// Create a new order and pick a challenge.
// Note that Let's Encrypt will reply with 400 error:malformed
// "NotBefore and NotAfter are not supported" when providing a NotAfter
// value like WithOrderNotAfter(time.Now().Add(24 * time.Hour)).
o, err := p.client.AuthorizeOrder(ctx, identifiers)
if err != nil {
log.Fatalf("AuthorizeOrder: %v", err)
}
var zurls []string
for _, u := range o.AuthzURLs {
z, err := p.client.GetAuthorization(ctx, u)
if err != nil {
log.Fatalf("GetAuthorization(%q): %v", u, err)
}
log.Printf("%+v", z)
if z.Status != acme.StatusPending {
log.Printf("authz status is %q; skipping", z.Status)
continue
}
if err := p.fulfill(ctx, z); err != nil {
log.Fatalf("fulfill(%s): %v", z.URI, err)
}
zurls = append(zurls, z.URI)
log.Printf("authorized for %+v", z.Identifier)
}
log.Print("all challenges are done")
if _, err := p.client.WaitOrder(ctx, o.URI); err != nil {
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
}
csr, certkey := newCSR(identifiers)
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
if err != nil {
log.Fatalf("CreateOrderCert: %v", err)
}
log.Printf("cert URL: %s", curl)
if err := checkCert(der, identifiers); err != nil {
p.errorf("invalid cert: %v", err)
}
// Deactivate all authorizations we satisfied earlier.
for _, v := range zurls {
if err := p.client.RevokeAuthorization(ctx, v); err != nil {
p.errorf("RevokAuthorization(%q): %v", v, err)
continue
}
}
// Deactivate the account. We don't need it for any further calls.
if err := p.client.DeactivateReg(ctx); err != nil {
p.errorf("DeactivateReg: %v", err)
}
// Try revoking the issued cert using its private key.
if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
p.errorf("RevokeCert: %v", err)
}
}
func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) {
dir, err := p.client.Discover(ctx)
if err != nil {
log.Fatalf("Discover: %v", err)
}
if dir.AuthzURL == "" {
log.Fatal("CA does not support pre-authorization")
}
var zurls []string
for _, id := range identifiers {
z, err := authorize(ctx, p.client, id)
if err != nil {
log.Fatalf("AuthorizeID(%+v): %v", z, err)
}
if z.Status == acme.StatusValid {
log.Printf("authz %s is valid; skipping", z.URI)
continue
}
if err := p.fulfill(ctx, z); err != nil {
log.Fatalf("fulfill(%s): %v", z.URI, err)
}
zurls = append(zurls, z.URI)
log.Printf("authorized for %+v", id)
}
// We should be all set now.
// Expect all authorizations to be satisfied.
log.Print("all challenges are done")
o, err := p.client.AuthorizeOrder(ctx, identifiers)
if err != nil {
log.Fatalf("AuthorizeOrder: %v", err)
}
waitCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
if _, err := p.client.WaitOrder(waitCtx, o.URI); err != nil {
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
}
csr, certkey := newCSR(identifiers)
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
if err != nil {
log.Fatalf("CreateOrderCert: %v", err)
}
log.Printf("cert URL: %s", curl)
if err := checkCert(der, identifiers); err != nil {
p.errorf("invalid cert: %v", err)
}
// Deactivate all authorizations we satisfied earlier.
for _, v := range zurls {
if err := p.client.RevokeAuthorization(ctx, v); err != nil {
p.errorf("RevokeAuthorization(%q): %v", v, err)
continue
}
}
// Deactivate the account. We don't need it for any further calls.
if err := p.client.DeactivateReg(ctx); err != nil {
p.errorf("DeactivateReg: %v", err)
}
// Try revoking the issued cert using its private key.
if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
p.errorf("RevokeCert: %v", err)
}
}
func (p *prober) fulfill(ctx context.Context, z *acme.Authorization) error {
var chal *acme.Challenge
for i, c := range z.Challenges {
log.Printf("challenge %d: %+v", i, c)
if c.Type == p.chalType {
log.Printf("picked %s for authz %s", c.URI, z.URI)
chal = c
}
}
if chal == nil {
return fmt.Errorf("challenge type %q wasn't offered for authz %s", p.chalType, z.URI)
}
switch chal.Type {
case "tls-alpn-01":
return p.runTLSALPN01(ctx, z, chal)
case "http-01":
return p.runHTTP01(ctx, z, chal)
case "dns-01":
return p.runDNS01(ctx, z, chal)
default:
return fmt.Errorf("unknown challenge type %q", chal.Type)
}
}
func (p *prober) runTLSALPN01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
tokenCert, err := p.client.TLSALPN01ChallengeCert(chal.Token, z.Identifier.Value)
if err != nil {
return fmt.Errorf("TLSALPN01ChallengeCert: %v", err)
}
s := &http.Server{
Addr: p.localAddr,
TLSConfig: &tls.Config{
NextProtos: []string{acme.ALPNProto},
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
log.Printf("hello: %+v", hello)
return &tokenCert, nil
},
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}),
}
go s.ListenAndServeTLS("", "")
defer s.Close()
if _, err := p.client.Accept(ctx, chal); err != nil {
return fmt.Errorf("Accept(%q): %v", chal.URI, err)
}
_, zerr := p.client.WaitAuthorization(ctx, z.URI)
return zerr
}
func (p *prober) runHTTP01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
body, err := p.client.HTTP01ChallengeResponse(chal.Token)
if err != nil {
return fmt.Errorf("HTTP01ChallengeResponse: %v", err)
}
s := &http.Server{
Addr: p.localAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
if r.URL.Path != p.client.HTTP01ChallengePath(chal.Token) {
w.WriteHeader(http.StatusNotFound)
return
}
w.Write([]byte(body))
}),
}
go s.ListenAndServe()
defer s.Close()
if _, err := p.client.Accept(ctx, chal); err != nil {
return fmt.Errorf("Accept(%q): %v", chal.URI, err)
}
_, zerr := p.client.WaitAuthorization(ctx, z.URI)
return zerr
}
func (p *prober) runDNS01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
token, err := p.client.DNS01ChallengeRecord(chal.Token)
if err != nil {
return fmt.Errorf("DNS01ChallengeRecord: %v", err)
}
name := fmt.Sprintf("_acme-challenge.%s", z.Identifier.Value)
cmd := exec.CommandContext(ctx, p.dnsScript, name, token)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s: %v", p.dnsScript, err)
}
if _, err := p.client.Accept(ctx, chal); err != nil {
return fmt.Errorf("Accept(%q): %v", chal.URI, err)
}
_, zerr := p.client.WaitAuthorization(ctx, z.URI)
return zerr
}
func authorize(ctx context.Context, client *acme.Client, id acme.AuthzID) (*acme.Authorization, error) {
if id.Type == "ip" {
return client.AuthorizeIP(ctx, id.Value)
}
return client.Authorize(ctx, id.Value)
}
func checkCert(derChain [][]byte, id []acme.AuthzID) error {
if len(derChain) == 0 {
return errors.New("cert chain is zero bytes")
}
for i, b := range derChain {
crt, err := x509.ParseCertificate(b)
if err != nil {
return fmt.Errorf("%d: ParseCertificate: %v", i, err)
}
log.Printf("%d: serial: 0x%s", i, crt.SerialNumber)
log.Printf("%d: subject: %s", i, crt.Subject)
log.Printf("%d: issuer: %s", i, crt.Issuer)
log.Printf("%d: expires in %.1f day(s)", i, time.Until(crt.NotAfter).Hours()/24)
if i > 0 { // not a leaf cert
continue
}
p := &pem.Block{Type: "CERTIFICATE", Bytes: b}
log.Printf("%d: leaf:\n%s", i, pem.EncodeToMemory(p))
for _, v := range id {
if err := crt.VerifyHostname(v.Value); err != nil {
return err
}
}
}
return nil
}
func newCSR(identifiers []acme.AuthzID) ([]byte, crypto.Signer) {
var csr x509.CertificateRequest
for _, id := range identifiers {
switch id.Type {
case "dns":
csr.DNSNames = append(csr.DNSNames, id.Value)
case "ip":
csr.IPAddresses = append(csr.IPAddresses, net.ParseIP(id.Value))
default:
panic(fmt.Sprintf("newCSR: unknown identifier type %q", id.Type))
}
}
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("newCSR: ecdsa.GenerateKey for a cert: %v", err))
}
b, err := x509.CreateCertificateRequest(rand.Reader, &csr, k)
if err != nil {
panic(fmt.Sprintf("newCSR: x509.CreateCertificateRequest: %v", err))
}
return b, k
}
@@ -0,0 +1,257 @@
// Copyright 2015 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 (
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
_ "crypto/sha512" // need for EC keys
"encoding/asn1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
)
// KeyID is the account key identity provided by a CA during registration.
type KeyID string
// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
// See jwsEncodeJSON for details.
const noKeyID = KeyID("")
// noPayload indicates jwsEncodeJSON will encode zero-length octet string
// in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
// authenticated GET requests via POSTing with an empty payload.
// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
const noPayload = ""
// noNonce indicates that the nonce should be omitted from the protected header.
// See jwsEncodeJSON for details.
const noNonce = ""
// jsonWebSignature can be easily serialized into a JWS following
// https://tools.ietf.org/html/rfc7515#section-3.2.
type jsonWebSignature struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}
// jwsEncodeJSON signs claimset using provided key and a nonce.
// The result is serialized in JSON format containing either kid or jwk
// fields based on the provided KeyID value.
//
// The claimset is marshalled using json.Marshal unless it is a string.
// In which case it is inserted directly into the message.
//
// If kid is non-empty, its quoted value is inserted in the protected header
// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
//
// If nonce is non-empty, its quoted value is inserted in the protected header.
//
// See https://tools.ietf.org/html/rfc7515#section-7.
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) {
if key == nil {
return nil, errors.New("nil key")
}
alg, sha := jwsHasher(key.Public())
if alg == "" || !sha.Available() {
return nil, ErrUnsupportedKey
}
headers := struct {
Alg string `json:"alg"`
KID string `json:"kid,omitempty"`
JWK json.RawMessage `json:"jwk,omitempty"`
Nonce string `json:"nonce,omitempty"`
URL string `json:"url"`
}{
Alg: alg,
Nonce: nonce,
URL: url,
}
switch kid {
case noKeyID:
jwk, err := jwkEncode(key.Public())
if err != nil {
return nil, err
}
headers.JWK = json.RawMessage(jwk)
default:
headers.KID = string(kid)
}
phJSON, err := json.Marshal(headers)
if err != nil {
return nil, err
}
phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON))
var payload string
if val, ok := claimset.(string); ok {
payload = val
} else {
cs, err := json.Marshal(claimset)
if err != nil {
return nil, err
}
payload = base64.RawURLEncoding.EncodeToString(cs)
}
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
if err != nil {
return nil, err
}
enc := jsonWebSignature{
Protected: phead,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(sig),
}
return json.Marshal(&enc)
}
// jwsWithMAC creates and signs a JWS using the given key and the HS256
// algorithm. kid and url are included in the protected header. rawPayload
// should not be base64-URL-encoded.
func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) {
if len(key) == 0 {
return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
}
header := struct {
Algorithm string `json:"alg"`
KID string `json:"kid"`
URL string `json:"url,omitempty"`
}{
// Only HMAC-SHA256 is supported.
Algorithm: "HS256",
KID: kid,
URL: url,
}
rawProtected, err := json.Marshal(header)
if err != nil {
return nil, err
}
protected := base64.RawURLEncoding.EncodeToString(rawProtected)
payload := base64.RawURLEncoding.EncodeToString(rawPayload)
h := hmac.New(sha256.New, key)
if _, err := h.Write([]byte(protected + "." + payload)); err != nil {
return nil, err
}
mac := h.Sum(nil)
return &jsonWebSignature{
Protected: protected,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(mac),
}, nil
}
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
// The result is also suitable for creating a JWK thumbprint.
// https://tools.ietf.org/html/rfc7517
func jwkEncode(pub crypto.PublicKey) (string, error) {
switch pub := pub.(type) {
case *rsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.3.1
n := pub.N
e := big.NewInt(int64(pub.E))
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
base64.RawURLEncoding.EncodeToString(e.Bytes()),
base64.RawURLEncoding.EncodeToString(n.Bytes()),
), nil
case *ecdsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.2.1
p := pub.Curve.Params()
n := p.BitSize / 8
if p.BitSize%8 != 0 {
n++
}
x := pub.X.Bytes()
if n > len(x) {
x = append(make([]byte, n-len(x)), x...)
}
y := pub.Y.Bytes()
if n > len(y) {
y = append(make([]byte, n-len(y)), y...)
}
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
p.Name,
base64.RawURLEncoding.EncodeToString(x),
base64.RawURLEncoding.EncodeToString(y),
), nil
}
return "", ErrUnsupportedKey
}
// jwsSign signs the digest using the given key.
// The hash is unused for ECDSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
switch pub := key.Public().(type) {
case *rsa.PublicKey:
return key.Sign(rand.Reader, digest, hash)
case *ecdsa.PublicKey:
sigASN1, err := key.Sign(rand.Reader, digest, hash)
if err != nil {
return nil, err
}
var rs struct{ R, S *big.Int }
if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil {
return nil, err
}
rb, sb := rs.R.Bytes(), rs.S.Bytes()
size := pub.Params().BitSize / 8
if size%8 > 0 {
size++
}
sig := make([]byte, size*2)
copy(sig[size-len(rb):], rb)
copy(sig[size*2-len(sb):], sb)
return sig, nil
}
return nil, ErrUnsupportedKey
}
// jwsHasher indicates suitable JWS algorithm name and a hash function
// to use for signing a digest with the provided key.
// It returns ("", 0) if the key is not supported.
func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
switch pub := pub.(type) {
case *rsa.PublicKey:
return "RS256", crypto.SHA256
case *ecdsa.PublicKey:
switch pub.Params().Name {
case "P-256":
return "ES256", crypto.SHA256
case "P-384":
return "ES384", crypto.SHA384
case "P-521":
return "ES512", crypto.SHA512
}
}
return "", 0
}
// JWKThumbprint creates a JWK thumbprint out of pub
// as specified in https://tools.ietf.org/html/rfc7638.
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
jwk, err := jwkEncode(pub)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(jwk))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
@@ -0,0 +1,550 @@
// Copyright 2015 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 (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"testing"
)
// The following shell command alias is used in the comments
// throughout this file:
// alias b64raw="base64 -w0 | tr -d '=' | tr '/+' '_-'"
const (
// Modulus in raw base64:
// 4xgZ3eRPkwoRvy7qeRUbmMDe0V-xH9eWLdu0iheeLlrmD2mqWXfP9IeSKApbn34
// g8TuAS9g5zhq8ELQ3kmjr-KV86GAMgI6VAcGlq3QrzpTCf_30Ab7-zawrfRaFON
// a1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosqEXeaIkVYBEhbh
// Nu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZfoyFyek380mHg
// JAumQ_I2fjj98_97mk3ihOY4AgVdCDj1z_GCoZkG5Rq7nbCGyosyKWyDX00Zs-n
// NqVhoLeIvXC4nnWdJMZ6rogxyQQ
testKeyPEM = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30
Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq
EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf
oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy
KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV
9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H
r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm
ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP
G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS
zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6
9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s
8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc
7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL
qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ
Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU
RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o
JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd
4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt
jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q
YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73
c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G
N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7
EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO
9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx
-----END RSA PRIVATE KEY-----
`
// This thumbprint is for the testKey defined above.
testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
// openssl ecparam -name secp256k1 -genkey -noout
testKeyECPEM = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49
AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5
QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ==
-----END EC PRIVATE KEY-----
`
// openssl ecparam -name secp384r1 -genkey -noout
testKeyEC384PEM = `
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD
Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj
JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke
WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg=
-----END EC PRIVATE KEY-----
`
// openssl ecparam -name secp521r1 -genkey -noout
testKeyEC512PEM = `
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z
KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx
7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD
FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd
GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
-----END EC PRIVATE KEY-----
`
// 1. openssl ec -in key.pem -noout -text
// 2. remove first byte, 04 (the header); the rest is X and Y
// 3. convert each with: echo <val> | xxd -r -p | b64raw
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt"
testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo"
testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY"
testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax"
// echo -n '{"crv":"P-256","kty":"EC","x":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
// openssl dgst -binary -sha256 | b64raw
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
)
var (
testKey *rsa.PrivateKey
testKeyEC *ecdsa.PrivateKey
testKeyEC384 *ecdsa.PrivateKey
testKeyEC512 *ecdsa.PrivateKey
)
func init() {
testKey = parseRSA(testKeyPEM, "testKeyPEM")
testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM")
testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM")
testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM")
}
func decodePEM(s, name string) []byte {
d, _ := pem.Decode([]byte(s))
if d == nil {
panic("no block found in " + name)
}
return d.Bytes
}
func parseRSA(s, name string) *rsa.PrivateKey {
b := decodePEM(s, name)
k, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
panic(fmt.Sprintf("%s: %v", name, err))
}
return k
}
func parseEC(s, name string) *ecdsa.PrivateKey {
b := decodePEM(s, name)
k, err := x509.ParseECPrivateKey(b)
if err != nil {
panic(fmt.Sprintf("%s: %v", name, err))
}
return k
}
func TestJWSEncodeJSON(t *testing.T) {
claims := struct{ Msg string }{"Hello JWS"}
// JWS signed with testKey and "nonce" as the nonce value
// JSON-serialized JWS fields are split for easier testing
const (
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
// {"Msg":"Hello JWS"}
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
// printf '<protected>.<payload>' | openssl dgst -binary -sha256 -sign testKey | b64raw
signature = "YFyl_xz1E7TR-3E1bIuASTr424EgCvBHjt25WUFC2VaDjXYV0Rj_" +
"Hd3dJ_2IRqBrXDZZ2n4ZeA_4mm3QFwmwyeDwe2sWElhb82lCZ8iX" +
"uFnjeOmSOjx-nWwPa5ibCXzLq13zZ-OBV1Z4oN_TuailQeRoSfA3" +
"nO8gG52mv1x2OMQ5MAFtt8jcngBLzts4AyhI6mBJ2w7Yaj3ZCriq" +
"DWA3GLFvvHdW1Ba9Z01wtGT2CuZI7DUk_6Qj1b3BkBGcoKur5C9i" +
"bUJtCkABwBMvBQNyD3MmXsrRFRTgvVlyU_yMaucYm7nmzEr_2PaQ" +
"50rFt_9qOfJ4sfbLtG1Wwae57BQx1g"
)
b, err := jwsEncodeJSON(claims, testKey, noKeyID, "nonce", "url")
if err != nil {
t.Fatal(err)
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Fatal(err)
}
if jws.Protected != protected {
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
}
if jws.Payload != payload {
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
}
if jws.Signature != signature {
t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, signature)
}
}
func TestJWSEncodeNoNonce(t *testing.T) {
kid := KeyID("https://example.org/account/1")
claims := "RawString"
const (
// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYWNjb3VudC8xIiwidXJsIjoidXJsIn0"
// "Raw String"
payload = "RawString"
)
b, err := jwsEncodeJSON(claims, testKeyEC, kid, "", "url")
if err != nil {
t.Fatal(err)
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Fatal(err)
}
if jws.Protected != protected {
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
}
if jws.Payload != payload {
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
}
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
if err != nil {
t.Fatalf("jws.Signature: %v", err)
}
r, s := big.NewInt(0), big.NewInt(0)
r.SetBytes(sig[:len(sig)/2])
s.SetBytes(sig[len(sig)/2:])
h := sha256.Sum256([]byte(protected + "." + payload))
if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
t.Error("invalid signature")
}
}
func TestJWSEncodeKID(t *testing.T) {
kid := KeyID("https://example.org/account/1")
claims := struct{ Msg string }{"Hello JWS"}
// JWS signed with testKeyEC
const (
// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5" +
"vcmcvYWNjb3VudC8xIiwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
// {"Msg":"Hello JWS"}
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
)
b, err := jwsEncodeJSON(claims, testKeyEC, kid, "nonce", "url")
if err != nil {
t.Fatal(err)
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Fatal(err)
}
if jws.Protected != protected {
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
}
if jws.Payload != payload {
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
}
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
if err != nil {
t.Fatalf("jws.Signature: %v", err)
}
r, s := big.NewInt(0), big.NewInt(0)
r.SetBytes(sig[:len(sig)/2])
s.SetBytes(sig[len(sig)/2:])
h := sha256.Sum256([]byte(protected + "." + payload))
if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
t.Error("invalid signature")
}
}
func TestJWSEncodeJSONEC(t *testing.T) {
tt := []struct {
key *ecdsa.PrivateKey
x, y string
alg, crv string
}{
{testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"},
{testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"},
{testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"},
}
for i, test := range tt {
claims := struct{ Msg string }{"Hello JWS"}
b, err := jwsEncodeJSON(claims, test.key, noKeyID, "nonce", "url")
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err = base64.RawURLEncoding.DecodeString(jws.Protected)
if err != nil {
t.Errorf("%d: jws.Protected: %v", i, err)
}
var head struct {
Alg string
Nonce string
URL string `json:"url"`
KID string `json:"kid"`
JWK struct {
Crv string
Kty string
X string
Y string
} `json:"jwk"`
}
if err := json.Unmarshal(b, &head); err != nil {
t.Errorf("%d: jws.Protected: %v", i, err)
}
if head.Alg != test.alg {
t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg)
}
if head.Nonce != "nonce" {
t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce)
}
if head.URL != "url" {
t.Errorf("%d: head.URL = %q; want 'url'", i, head.URL)
}
if head.KID != "" {
// We used noKeyID in jwsEncodeJSON: expect no kid value.
t.Errorf("%d: head.KID = %q; want empty", i, head.KID)
}
if head.JWK.Crv != test.crv {
t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv)
}
if head.JWK.Kty != "EC" {
t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty)
}
if head.JWK.X != test.x {
t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x)
}
if head.JWK.Y != test.y {
t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y)
}
}
}
type customTestSigner struct {
sig []byte
pub crypto.PublicKey
}
func (s *customTestSigner) Public() crypto.PublicKey { return s.pub }
func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
return s.sig, nil
}
func TestJWSEncodeJSONCustom(t *testing.T) {
claims := struct{ Msg string }{"hello"}
const (
// printf '{"Msg":"hello"}' | b64raw
payload = "eyJNc2ciOiJoZWxsbyJ9"
// printf 'testsig' | b64raw
testsig = "dGVzdHNpZw"
// the example P256 curve point from https://tools.ietf.org/html/rfc7515#appendix-A.3.1
// encoded as ASN.1…
es256stdsig = "MEUCIA7RIVN5Y2xIPC9/FVgH1AKjsigDOvl8fheBmsMWnqZlAiEA" +
"xQoH04w8cOXY8S2vCEpUgKZlkMXyk1Cajz9/ioOjVNU"
// …and RFC7518 (https://tools.ietf.org/html/rfc7518#section-3.4)
es256jwsig = "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw" +
"5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
// printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":<testKeyECPubY>,"y":<testKeyECPubY>},"nonce":"nonce","url":"url"}' | b64raw
es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0" +
"eSI6IkVDIiwieCI6IjVsaEV1ZzV4SzR4QkRaMm5BYmF4THRhTGl2" +
"ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFHa3Yw" +
"VGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6" +
"Im5vbmNlIiwidXJsIjoidXJsIn0"
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
)
tt := []struct {
alg, phead string
pub crypto.PublicKey
stdsig, jwsig string
}{
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
}
for _, tc := range tt {
tc := tc
t.Run(tc.alg, func(t *testing.T) {
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
if err != nil {
t.Errorf("couldn't decode test vector: %v", err)
}
signer := &customTestSigner{
sig: stdsig,
pub: tc.pub,
}
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
if err != nil {
t.Fatal(err)
}
var j jsonWebSignature
if err := json.Unmarshal(b, &j); err != nil {
t.Fatal(err)
}
if j.Protected != tc.phead {
t.Errorf("j.Protected = %q\nwant %q", j.Protected, tc.phead)
}
if j.Payload != payload {
t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload)
}
if j.Sig != tc.jwsig {
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
}
})
}
}
func TestJWSWithMAC(t *testing.T) {
// Example from RFC 7520 Section 4.4.3.
// https://tools.ietf.org/html/rfc7520#section-4.4.3
b64Key := "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
rawPayload := []byte("It\xe2\x80\x99s a dangerous business, Frodo, going out your " +
"door. You step onto the road, and if you don't keep your feet, " +
"there\xe2\x80\x99s no knowing where you might be swept off " +
"to.")
protected := "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" +
"VlZjMxNGJjNzAzNyJ9"
payload := "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywg" +
"Z29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9h" +
"ZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXi" +
"gJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9m" +
"ZiB0by4"
sig := "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
key, err := base64.RawURLEncoding.DecodeString(b64Key)
if err != nil {
t.Fatalf("unable to decode key: %q", b64Key)
}
got, err := jwsWithMAC(key, "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "", rawPayload)
if err != nil {
t.Fatalf("jwsWithMAC() = %q", err)
}
if got.Protected != protected {
t.Errorf("got.Protected = %q\nwant %q", got.Protected, protected)
}
if got.Payload != payload {
t.Errorf("got.Payload = %q\nwant %q", got.Payload, payload)
}
if got.Sig != sig {
t.Errorf("got.Signature = %q\nwant %q", got.Sig, sig)
}
}
func TestJWSWithMACError(t *testing.T) {
p := "{}"
if _, err := jwsWithMAC(nil, "", "", []byte(p)); err == nil {
t.Errorf("jwsWithMAC(nil, ...) = success; want err")
}
}
func TestJWKThumbprintRSA(t *testing.T) {
// Key example from RFC 7638
const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +
"VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" +
"4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" +
"W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" +
"1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" +
"aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
const base64E = "AQAB"
const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
b, err := base64.RawURLEncoding.DecodeString(base64N)
if err != nil {
t.Fatalf("Error parsing example key N: %v", err)
}
n := new(big.Int).SetBytes(b)
b, err = base64.RawURLEncoding.DecodeString(base64E)
if err != nil {
t.Fatalf("Error parsing example key E: %v", err)
}
e := new(big.Int).SetBytes(b)
pub := &rsa.PublicKey{N: n, E: int(e.Uint64())}
th, err := JWKThumbprint(pub)
if err != nil {
t.Error(err)
}
if th != expected {
t.Errorf("thumbprint = %q; want %q", th, expected)
}
}
func TestJWKThumbprintEC(t *testing.T) {
// Key example from RFC 7520
// expected was computed with
// printf '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
// openssl dgst -binary -sha256 | b64raw
const (
base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"
base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" +
"QkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M"
)
b, err := base64.RawURLEncoding.DecodeString(base64X)
if err != nil {
t.Fatalf("Error parsing example key X: %v", err)
}
x := new(big.Int).SetBytes(b)
b, err = base64.RawURLEncoding.DecodeString(base64Y)
if err != nil {
t.Fatalf("Error parsing example key Y: %v", err)
}
y := new(big.Int).SetBytes(b)
pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}
th, err := JWKThumbprint(pub)
if err != nil {
t.Error(err)
}
if th != expected {
t.Errorf("thumbprint = %q; want %q", th, expected)
}
}
func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
_, err := JWKThumbprint(struct{}{})
if err != ErrUnsupportedKey {
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
}
}
@@ -0,0 +1,476 @@
// 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"
"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 {
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
return err
}
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)
}
// getRegRFC 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
}
// accountKeyRollover attempts to perform account key rollover.
// On success it will change client.Key to the new key.
func (c *Client) accountKeyRollover(ctx context.Context, newKey crypto.Signer) error {
dir, err := c.Discover(ctx) // Also required by c.accountKID
if err != nil {
return err
}
kid := c.accountKID(ctx)
if kid == noKeyID {
return ErrNoAccount
}
oldKey, err := jwkEncode(c.Key.Public())
if err != nil {
return err
}
payload := struct {
Account string `json:"account"`
OldKey json.RawMessage `json:"oldKey"`
}{
Account: string(kid),
OldKey: json.RawMessage(oldKey),
}
inner, err := jwsEncodeJSON(payload, newKey, noKeyID, noNonce, dir.KeyChangeURL)
if err != nil {
return err
}
res, err := c.post(ctx, nil, dir.KeyChangeURL, base64.RawURLEncoding.EncodeToString(inner), wantStatus(http.StatusOK))
if err != nil {
return err
}
defer res.Body.Close()
c.Key = newKey
return 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 := io.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(io.Discard, res.Body); err != nil {
return nil, fmt.Errorf("acme: cert alternates response stream: %v", err)
}
alts := linkHeader(res.Header, "alternate")
return alts, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,614 @@
// Copyright 2016 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 (
"crypto"
"crypto/x509"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
// ACME status values of Account, Order, Authorization and Challenge objects.
// See https://tools.ietf.org/html/rfc8555#section-7.1.6 for details.
const (
StatusDeactivated = "deactivated"
StatusExpired = "expired"
StatusInvalid = "invalid"
StatusPending = "pending"
StatusProcessing = "processing"
StatusReady = "ready"
StatusRevoked = "revoked"
StatusUnknown = "unknown"
StatusValid = "valid"
)
// CRLReasonCode identifies the reason for a certificate revocation.
type CRLReasonCode int
// CRL reason codes as defined in RFC 5280.
const (
CRLReasonUnspecified CRLReasonCode = 0
CRLReasonKeyCompromise CRLReasonCode = 1
CRLReasonCACompromise CRLReasonCode = 2
CRLReasonAffiliationChanged CRLReasonCode = 3
CRLReasonSuperseded CRLReasonCode = 4
CRLReasonCessationOfOperation CRLReasonCode = 5
CRLReasonCertificateHold CRLReasonCode = 6
CRLReasonRemoveFromCRL CRLReasonCode = 8
CRLReasonPrivilegeWithdrawn CRLReasonCode = 9
CRLReasonAACompromise CRLReasonCode = 10
)
var (
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
// ErrAccountAlreadyExists indicates that the Client's key has already been registered
// with the CA. It is returned by Register method.
ErrAccountAlreadyExists = errors.New("acme: account already exists")
// ErrNoAccount indicates that the Client's key has not been registered with the CA.
ErrNoAccount = errors.New("acme: account does not exist")
)
// A Subproblem describes an ACME subproblem as reported in an Error.
type Subproblem struct {
// Type is a URI reference that identifies the problem type,
// typically in a "urn:acme:error:xxx" form.
Type string
// Detail is a human-readable explanation specific to this occurrence of the problem.
Detail string
// Instance indicates a URL that the client should direct a human user to visit
// in order for instructions on how to agree to the updated Terms of Service.
// In such an event CA sets StatusCode to 403, Type to
// "urn:ietf:params:acme:error:userActionRequired", and adds a Link header with relation
// "terms-of-service" containing the latest TOS URL.
Instance string
// Identifier may contain the ACME identifier that the error is for.
Identifier *AuthzID
}
func (sp Subproblem) String() string {
str := fmt.Sprintf("%s: ", sp.Type)
if sp.Identifier != nil {
str += fmt.Sprintf("[%s: %s] ", sp.Identifier.Type, sp.Identifier.Value)
}
str += sp.Detail
return str
}
// Error is an ACME error, defined in Problem Details for HTTP APIs doc
// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
type Error struct {
// StatusCode is The HTTP status code generated by the origin server.
StatusCode int
// ProblemType is a URI reference that identifies the problem type,
// typically in a "urn:acme:error:xxx" form.
ProblemType string
// Detail is a human-readable explanation specific to this occurrence of the problem.
Detail string
// Instance indicates a URL that the client should direct a human user to visit
// in order for instructions on how to agree to the updated Terms of Service.
// In such an event CA sets StatusCode to 403, ProblemType to
// "urn:ietf:params:acme:error:userActionRequired" and a Link header with relation
// "terms-of-service" containing the latest TOS URL.
Instance string
// Header is the original server error response headers.
// It may be nil.
Header http.Header
// Subproblems may contain more detailed information about the individual problems
// that caused the error. This field is only sent by RFC 8555 compatible ACME
// servers. Defined in RFC 8555 Section 6.7.1.
Subproblems []Subproblem
}
func (e *Error) Error() string {
str := fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
if len(e.Subproblems) > 0 {
str += fmt.Sprintf("; subproblems:")
for _, sp := range e.Subproblems {
str += fmt.Sprintf("\n\t%s", sp)
}
}
return str
}
// AuthorizationError indicates that an authorization for an identifier
// did not succeed.
// It contains all errors from Challenge items of the failed Authorization.
type AuthorizationError struct {
// URI uniquely identifies the failed Authorization.
URI string
// Identifier is an AuthzID.Value of the failed Authorization.
Identifier string
// Errors is a collection of non-nil error values of Challenge items
// of the failed Authorization.
Errors []error
}
func (a *AuthorizationError) Error() string {
e := make([]string, len(a.Errors))
for i, err := range a.Errors {
e[i] = err.Error()
}
if a.Identifier != "" {
return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
}
return fmt.Sprintf("acme: authorization error: %s", strings.Join(e, "; "))
}
// OrderError is returned from Client's order related methods.
// It indicates the order is unusable and the clients should start over with
// AuthorizeOrder.
//
// The clients can still fetch the order object from CA using GetOrder
// to inspect its state.
type OrderError struct {
OrderURL string
Status string
}
func (oe *OrderError) Error() string {
return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
}
// RateLimit reports whether err represents a rate limit error and
// any Retry-After duration returned by the server.
//
// See the following for more details on rate limiting:
// https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6
func RateLimit(err error) (time.Duration, bool) {
e, ok := err.(*Error)
if !ok {
return 0, false
}
// Some CA implementations may return incorrect values.
// Use case-insensitive comparison.
if !strings.HasSuffix(strings.ToLower(e.ProblemType), ":ratelimited") {
return 0, false
}
if e.Header == nil {
return 0, true
}
return retryAfter(e.Header.Get("Retry-After")), true
}
// Account is a user account. It is associated with a private key.
// Non-RFC 8555 fields are empty when interfacing with a compliant CA.
type Account struct {
// URI is the account unique ID, which is also a URL used to retrieve
// account data from the CA.
// When interfacing with RFC 8555-compliant CAs, URI is the "kid" field
// value in JWS signed requests.
URI string
// Contact is a slice of contact info used during registration.
// See https://tools.ietf.org/html/rfc8555#section-7.3 for supported
// formats.
Contact []string
// Status indicates current account status as returned by the CA.
// Possible values are StatusValid, StatusDeactivated, and StatusRevoked.
Status string
// OrdersURL is a URL from which a list of orders submitted by this account
// can be fetched.
OrdersURL string
// The terms user has agreed to.
// A value not matching CurrentTerms indicates that the user hasn't agreed
// to the actual Terms of Service of the CA.
//
// It is non-RFC 8555 compliant. Package users can store the ToS they agree to
// during Client's Register call in the prompt callback function.
AgreedTerms string
// Actual terms of a CA.
//
// It is non-RFC 8555 compliant. Use Directory's Terms field.
// When a CA updates their terms and requires an account agreement,
// a URL at which instructions to do so is available in Error's Instance field.
CurrentTerms string
// Authz is the authorization URL used to initiate a new authz flow.
//
// It is non-RFC 8555 compliant. Use Directory's AuthzURL or OrderURL.
Authz string
// Authorizations is a URI from which a list of authorizations
// granted to this account can be fetched via a GET request.
//
// It is non-RFC 8555 compliant and is obsoleted by OrdersURL.
Authorizations string
// Certificates is a URI from which a list of certificates
// issued for this account can be fetched via a GET request.
//
// It is non-RFC 8555 compliant and is obsoleted by OrdersURL.
Certificates string
// ExternalAccountBinding represents an arbitrary binding to an account of
// the CA which the ACME server is tied to.
// See https://tools.ietf.org/html/rfc8555#section-7.3.4 for more details.
ExternalAccountBinding *ExternalAccountBinding
}
// ExternalAccountBinding contains the data needed to form a request with
// an external account binding.
// See https://tools.ietf.org/html/rfc8555#section-7.3.4 for more details.
type ExternalAccountBinding struct {
// KID is the Key ID of the symmetric MAC key that the CA provides to
// identify an external account from ACME.
KID string
// Key is the bytes of the symmetric key that the CA provides to identify
// the account. Key must correspond to the KID.
Key []byte
}
func (e *ExternalAccountBinding) String() string {
return fmt.Sprintf("&{KID: %q, Key: redacted}", e.KID)
}
// Directory is ACME server discovery data.
// See https://tools.ietf.org/html/rfc8555#section-7.1.1 for more details.
type Directory struct {
// NonceURL indicates an endpoint where to fetch fresh nonce values from.
NonceURL string
// RegURL is an account endpoint URL, allowing for creating new accounts.
// Pre-RFC 8555 CAs also allow modifying existing accounts at this URL.
RegURL string
// OrderURL is used to initiate the certificate issuance flow
// as described in RFC 8555.
OrderURL string
// AuthzURL is used to initiate identifier pre-authorization flow.
// Empty string indicates the flow is unsupported by the CA.
AuthzURL string
// CertURL is a new certificate issuance endpoint URL.
// It is non-RFC 8555 compliant and is obsoleted by OrderURL.
CertURL string
// RevokeURL is used to initiate a certificate revocation flow.
RevokeURL string
// KeyChangeURL allows to perform account key rollover flow.
KeyChangeURL string
// Term is a URI identifying the current terms of service.
Terms string
// Website is an HTTP or HTTPS URL locating a website
// providing more information about the ACME server.
Website string
// CAA consists of lowercase hostname elements, which the ACME server
// recognises as referring to itself for the purposes of CAA record validation
// as defined in RFC 6844.
CAA []string
// ExternalAccountRequired indicates that the CA requires for all account-related
// requests to include external account binding information.
ExternalAccountRequired bool
}
// Order represents a client's request for a certificate.
// It tracks the request flow progress through to issuance.
type Order struct {
// URI uniquely identifies an order.
URI string
// Status represents the current status of the order.
// It indicates which action the client should take.
//
// Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid.
// Pending means the CA does not believe that the client has fulfilled the requirements.
// Ready indicates that the client has fulfilled all the requirements and can submit a CSR
// to obtain a certificate. This is done with Client's CreateOrderCert.
// Processing means the certificate is being issued.
// Valid indicates the CA has issued the certificate. It can be downloaded
// from the Order's CertURL. This is done with Client's FetchCert.
// Invalid means the certificate will not be issued. Users should consider this order
// abandoned.
Status string
// Expires is the timestamp after which CA considers this order invalid.
Expires time.Time
// Identifiers contains all identifier objects which the order pertains to.
Identifiers []AuthzID
// NotBefore is the requested value of the notBefore field in the certificate.
NotBefore time.Time
// NotAfter is the requested value of the notAfter field in the certificate.
NotAfter time.Time
// AuthzURLs represents authorizations to complete before a certificate
// for identifiers specified in the order can be issued.
// It also contains unexpired authorizations that the client has completed
// in the past.
//
// Authorization objects can be fetched using Client's GetAuthorization method.
//
// The required authorizations are dictated by CA policies.
// There may not be a 1:1 relationship between the identifiers and required authorizations.
// Required authorizations can be identified by their StatusPending status.
//
// For orders in the StatusValid or StatusInvalid state these are the authorizations
// which were completed.
AuthzURLs []string
// FinalizeURL is the endpoint at which a CSR is submitted to obtain a certificate
// once all the authorizations are satisfied.
FinalizeURL string
// CertURL points to the certificate that has been issued in response to this order.
CertURL string
// The error that occurred while processing the order as received from a CA, if any.
Error *Error
}
// OrderOption allows customizing Client.AuthorizeOrder call.
type OrderOption interface {
privateOrderOpt()
}
// WithOrderNotBefore sets order's NotBefore field.
func WithOrderNotBefore(t time.Time) OrderOption {
return orderNotBeforeOpt(t)
}
// WithOrderNotAfter sets order's NotAfter field.
func WithOrderNotAfter(t time.Time) OrderOption {
return orderNotAfterOpt(t)
}
type orderNotBeforeOpt time.Time
func (orderNotBeforeOpt) privateOrderOpt() {}
type orderNotAfterOpt time.Time
func (orderNotAfterOpt) privateOrderOpt() {}
// Authorization encodes an authorization response.
type Authorization struct {
// URI uniquely identifies a authorization.
URI string
// Status is the current status of an authorization.
// Possible values are StatusPending, StatusValid, StatusInvalid, StatusDeactivated,
// StatusExpired and StatusRevoked.
Status string
// Identifier is what the account is authorized to represent.
Identifier AuthzID
// The timestamp after which the CA considers the authorization invalid.
Expires time.Time
// Wildcard is true for authorizations of a wildcard domain name.
Wildcard bool
// Challenges that the client needs to fulfill in order to prove possession
// of the identifier (for pending authorizations).
// For valid authorizations, the challenge that was validated.
// For invalid authorizations, the challenge that was attempted and failed.
//
// RFC 8555 compatible CAs require users to fuflfill only one of the challenges.
Challenges []*Challenge
// A collection of sets of challenges, each of which would be sufficient
// to prove possession of the identifier.
// Clients must complete a set of challenges that covers at least one set.
// Challenges are identified by their indices in the challenges array.
// If this field is empty, the client needs to complete all challenges.
//
// This field is unused in RFC 8555.
Combinations [][]int
}
// AuthzID is an identifier that an account is authorized to represent.
type AuthzID struct {
Type string // The type of identifier, "dns" or "ip".
Value string // The identifier itself, e.g. "example.org".
}
// DomainIDs creates a slice of AuthzID with "dns" identifier type.
func DomainIDs(names ...string) []AuthzID {
a := make([]AuthzID, len(names))
for i, v := range names {
a[i] = AuthzID{Type: "dns", Value: v}
}
return a
}
// IPIDs creates a slice of AuthzID with "ip" identifier type.
// Each element of addr is textual form of an address as defined
// in RFC 1123 Section 2.1 for IPv4 and in RFC 5952 Section 4 for IPv6.
func IPIDs(addr ...string) []AuthzID {
a := make([]AuthzID, len(addr))
for i, v := range addr {
a[i] = AuthzID{Type: "ip", Value: v}
}
return a
}
// wireAuthzID is ACME JSON representation of authorization identifier objects.
type wireAuthzID struct {
Type string `json:"type"`
Value string `json:"value"`
}
// wireAuthz is ACME JSON representation of Authorization objects.
type wireAuthz struct {
Identifier wireAuthzID
Status string
Expires time.Time
Wildcard bool
Challenges []wireChallenge
Combinations [][]int
Error *wireError
}
func (z *wireAuthz) authorization(uri string) *Authorization {
a := &Authorization{
URI: uri,
Status: z.Status,
Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value},
Expires: z.Expires,
Wildcard: z.Wildcard,
Challenges: make([]*Challenge, len(z.Challenges)),
Combinations: z.Combinations, // shallow copy
}
for i, v := range z.Challenges {
a.Challenges[i] = v.challenge()
}
return a
}
func (z *wireAuthz) error(uri string) *AuthorizationError {
err := &AuthorizationError{
URI: uri,
Identifier: z.Identifier.Value,
}
if z.Error != nil {
err.Errors = append(err.Errors, z.Error.error(nil))
}
for _, raw := range z.Challenges {
if raw.Error != nil {
err.Errors = append(err.Errors, raw.Error.error(nil))
}
}
return err
}
// Challenge encodes a returned CA challenge.
// Its Error field may be non-nil if the challenge is part of an Authorization
// with StatusInvalid.
type Challenge struct {
// Type is the challenge type, e.g. "http-01", "tls-alpn-01", "dns-01".
Type string
// URI is where a challenge response can be posted to.
URI string
// Token is a random value that uniquely identifies the challenge.
Token string
// Status identifies the status of this challenge.
// In RFC 8555, possible values are StatusPending, StatusProcessing, StatusValid,
// and StatusInvalid.
Status string
// Validated is the time at which the CA validated this challenge.
// Always zero value in pre-RFC 8555.
Validated time.Time
// Error indicates the reason for an authorization failure
// when this challenge was used.
// The type of a non-nil value is *Error.
Error error
}
// wireChallenge is ACME JSON challenge representation.
type wireChallenge struct {
URL string `json:"url"` // RFC
URI string `json:"uri"` // pre-RFC
Type string
Token string
Status string
Validated time.Time
Error *wireError
}
func (c *wireChallenge) challenge() *Challenge {
v := &Challenge{
URI: c.URL,
Type: c.Type,
Token: c.Token,
Status: c.Status,
}
if v.URI == "" {
v.URI = c.URI // c.URL was empty; use legacy
}
if v.Status == "" {
v.Status = StatusPending
}
if c.Error != nil {
v.Error = c.Error.error(nil)
}
return v
}
// wireError is a subset of fields of the Problem Details object
// as described in https://tools.ietf.org/html/rfc7807#section-3.1.
type wireError struct {
Status int
Type string
Detail string
Instance string
Subproblems []Subproblem
}
func (e *wireError) error(h http.Header) *Error {
err := &Error{
StatusCode: e.Status,
ProblemType: e.Type,
Detail: e.Detail,
Instance: e.Instance,
Header: h,
Subproblems: e.Subproblems,
}
return err
}
// CertOption is an optional argument type for the TLS ChallengeCert methods for
// customizing a temporary certificate for TLS-based challenges.
type CertOption interface {
privateCertOpt()
}
// WithKey creates an option holding a private/public key pair.
// The private part signs a certificate, and the public part represents the signee.
func WithKey(key crypto.Signer) CertOption {
return &certOptKey{key}
}
type certOptKey struct {
key crypto.Signer
}
func (*certOptKey) privateCertOpt() {}
// WithTemplate creates an option for specifying a certificate template.
// See x509.CreateCertificate for template usage details.
//
// In TLS ChallengeCert methods, the template is also used as parent,
// resulting in a self-signed certificate.
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
func WithTemplate(t *x509.Certificate) CertOption {
return (*certOptTemplate)(t)
}
type certOptTemplate x509.Certificate
func (*certOptTemplate) privateCertOpt() {}
@@ -0,0 +1,219 @@
// Copyright 2017 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 (
"errors"
"net/http"
"reflect"
"testing"
"time"
)
func TestExternalAccountBindingString(t *testing.T) {
eab := ExternalAccountBinding{
KID: "kid",
Key: []byte("key"),
}
got := eab.String()
want := `&{KID: "kid", Key: redacted}`
if got != want {
t.Errorf("eab.String() = %q, want: %q", got, want)
}
}
func TestRateLimit(t *testing.T) {
now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC)
f := timeNow
defer func() { timeNow = f }()
timeNow = func() time.Time { return now }
h120, hTime := http.Header{}, http.Header{}
h120.Set("Retry-After", "120")
hTime.Set("Retry-After", "Tue Apr 27 11:00:00 2017")
err1 := &Error{
ProblemType: "urn:ietf:params:acme:error:nolimit",
Header: h120,
}
err2 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: h120,
}
err3 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: nil,
}
err4 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: hTime,
}
tt := []struct {
err error
res time.Duration
ok bool
}{
{nil, 0, false},
{errors.New("dummy"), 0, false},
{err1, 0, false},
{err2, 2 * time.Minute, true},
{err3, 0, true},
{err4, time.Hour, true},
}
for i, test := range tt {
res, ok := RateLimit(test.err)
if ok != test.ok {
t.Errorf("%d: RateLimit(%+v): ok = %v; want %v", i, test.err, ok, test.ok)
continue
}
if res != test.res {
t.Errorf("%d: RateLimit(%+v) = %v; want %v", i, test.err, res, test.res)
}
}
}
func TestAuthorizationError(t *testing.T) {
tests := []struct {
desc string
err *AuthorizationError
msg string
}{
{
desc: "when auth error identifier is set",
err: &AuthorizationError{
Identifier: "domain.com",
Errors: []error{
(&wireError{
Status: 403,
Type: "urn:ietf:params:acme:error:caa",
Detail: "CAA record for domain.com prevents issuance",
}).error(nil),
},
},
msg: "acme: authorization error for domain.com: 403 urn:ietf:params:acme:error:caa: CAA record for domain.com prevents issuance",
},
{
desc: "when auth error identifier is unset",
err: &AuthorizationError{
Errors: []error{
(&wireError{
Status: 403,
Type: "urn:ietf:params:acme:error:caa",
Detail: "CAA record for domain.com prevents issuance",
}).error(nil),
},
},
msg: "acme: authorization error: 403 urn:ietf:params:acme:error:caa: CAA record for domain.com prevents issuance",
},
}
for _, tt := range tests {
if tt.err.Error() != tt.msg {
t.Errorf("got: %s\nwant: %s", tt.err, tt.msg)
}
}
}
func TestSubproblems(t *testing.T) {
tests := []struct {
wire wireError
expectedOut Error
}{
{
wire: wireError{
Status: 1,
Type: "urn:error",
Detail: "it's an error",
},
expectedOut: Error{
StatusCode: 1,
ProblemType: "urn:error",
Detail: "it's an error",
},
},
{
wire: wireError{
Status: 1,
Type: "urn:error",
Detail: "it's an error",
Subproblems: []Subproblem{
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
},
},
},
expectedOut: Error{
StatusCode: 1,
ProblemType: "urn:error",
Detail: "it's an error",
Subproblems: []Subproblem{
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
},
},
},
},
{
wire: wireError{
Status: 1,
Type: "urn:error",
Detail: "it's an error",
Subproblems: []Subproblem{
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
Identifier: &AuthzID{Type: "dns", Value: "example"},
},
},
},
expectedOut: Error{
StatusCode: 1,
ProblemType: "urn:error",
Detail: "it's an error",
Subproblems: []Subproblem{
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
Identifier: &AuthzID{Type: "dns", Value: "example"},
},
},
},
},
}
for _, tc := range tests {
out := tc.wire.error(nil)
if !reflect.DeepEqual(*out, tc.expectedOut) {
t.Errorf("Unexpected error: wanted %v, got %v", tc.expectedOut, *out)
}
}
}
func TestErrorStringerWithSubproblems(t *testing.T) {
err := Error{
StatusCode: 1,
ProblemType: "urn:error",
Detail: "it's an error",
Subproblems: []Subproblem{
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
},
{
Type: "urn:error:sub",
Detail: "it's a subproblem",
Identifier: &AuthzID{Type: "dns", Value: "example"},
},
},
}
expectedStr := "1 urn:error: it's an error; subproblems:\n\turn:error:sub: it's a subproblem\n\turn:error:sub: [dns: example] it's a subproblem"
if err.Error() != expectedStr {
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
}
}
@@ -0,0 +1,27 @@
// 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.
//go:build go1.12
package acme
import "runtime/debug"
func init() {
// Set packageVersion if the binary was built in modules mode and x/crypto
// was not replaced with a different module.
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
for _, m := range info.Deps {
if m.Path != "golang.org/x/crypto" {
continue
}
if m.Replace == nil {
packageVersion = m.Version
}
break
}
}