whatcanGOwrong
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user