This commit is contained in:
Manfred Touron
2017-05-18 18:54:23 +02:00
parent dc386661ca
commit 5448f25fd6
645 changed files with 55908 additions and 33297 deletions

View File

@@ -15,6 +15,7 @@ package acme
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@@ -36,9 +37,6 @@ import (
"strings"
"sync"
"time"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
)
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
@@ -133,7 +131,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
if dirURL == "" {
dirURL = LetsEncryptURL
}
res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
res, err := c.get(ctx, dirURL)
if err != nil {
return Directory{}, err
}
@@ -154,7 +152,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
CAA []string `json:"caa-identities"`
}
}
if json.NewDecoder(res.Body).Decode(&v); err != nil {
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return Directory{}, err
}
c.dir = &Directory{
@@ -200,7 +198,7 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration,
req.NotAfter = now.Add(exp).Format(time.RFC3339)
}
res, err := c.postJWS(ctx, c.Key, c.dir.CertURL, req)
res, err := c.retryPostJWS(ctx, c.Key, c.dir.CertURL, req)
if err != nil {
return nil, "", err
}
@@ -216,7 +214,7 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration,
return cert, curl, err
}
// slurp issued cert and CA chain, if requested
cert, err := responseCert(ctx, c.HTTPClient, res, bundle)
cert, err := c.responseCert(ctx, res, bundle)
return cert, curl, err
}
@@ -231,13 +229,13 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration,
// and has expected features.
func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) {
for {
res, err := ctxhttp.Get(ctx, c.HTTPClient, url)
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
return responseCert(ctx, c.HTTPClient, res, bundle)
return c.responseCert(ctx, res, bundle)
}
if res.StatusCode > 299 {
return nil, responseError(res)
@@ -275,7 +273,7 @@ func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte,
if key == nil {
key = c.Key
}
res, err := c.postJWS(ctx, key, c.dir.RevokeURL, body)
res, err := c.retryPostJWS(ctx, key, c.dir.RevokeURL, body)
if err != nil {
return err
}
@@ -363,7 +361,7 @@ func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization,
Resource: "new-authz",
Identifier: authzID{Type: "dns", Value: domain},
}
res, err := c.postJWS(ctx, c.Key, c.dir.AuthzURL, req)
res, err := c.retryPostJWS(ctx, c.Key, c.dir.AuthzURL, req)
if err != nil {
return nil, err
}
@@ -387,7 +385,7 @@ func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization,
// 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) {
res, err := ctxhttp.Get(ctx, c.HTTPClient, url)
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
@@ -421,7 +419,7 @@ func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
Status: "deactivated",
Delete: true,
}
res, err := c.postJWS(ctx, c.Key, url, req)
res, err := c.retryPostJWS(ctx, c.Key, url, req)
if err != nil {
return err
}
@@ -438,25 +436,11 @@ func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
//
// 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 ErrAuthorizationFailed.
// If the Status is StatusInvalid, the returned error is of type *AuthorizationError.
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
var count int
sleep := func(v string, inc int) error {
count += inc
d := backoff(count, 10*time.Second)
d = retryAfter(v, d)
wakeup := time.NewTimer(d)
defer wakeup.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeup.C:
return nil
}
}
sleep := sleeper(ctx)
for {
res, err := ctxhttp.Get(ctx, c.HTTPClient, url)
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
@@ -481,7 +465,7 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat
return raw.authorization(url), nil
}
if raw.Status == StatusInvalid {
return nil, ErrAuthorizationFailed
return nil, raw.error(url)
}
if err := sleep(retry, 0); err != nil {
return nil, err
@@ -493,7 +477,7 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat
//
// A client typically polls a challenge status using this method.
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
res, err := ctxhttp.Get(ctx, c.HTTPClient, url)
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
@@ -527,7 +511,7 @@ func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error
Type: chal.Type,
Auth: auth,
}
res, err := c.postJWS(ctx, c.Key, chal.URI, req)
res, err := c.retryPostJWS(ctx, c.Key, chal.URI, req)
if err != nil {
return nil, err
}
@@ -660,7 +644,7 @@ func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Accoun
req.Contact = acct.Contact
req.Agreement = acct.AgreedTerms
}
res, err := c.postJWS(ctx, c.Key, url, req)
res, err := c.retryPostJWS(ctx, c.Key, url, req)
if err != nil {
return nil, err
}
@@ -697,6 +681,40 @@ func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Accoun
}, nil
}
// retryPostJWS will retry calls to postJWS if there is a badNonce error,
// clearing the stored nonces after each error.
// If the response was 4XX-5XX, then responseError is called on the body,
// the body is closed, and the error returned.
func (c *Client) retryPostJWS(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, error) {
sleep := sleeper(ctx)
for {
res, err := c.postJWS(ctx, key, url, body)
if err != nil {
return nil, err
}
// handle errors 4XX-5XX with responseError
if res.StatusCode >= 400 && res.StatusCode <= 599 {
err := responseError(res)
res.Body.Close()
// according to spec badNonce is urn:ietf:params:acme:error:badNonce
// however, acme servers in the wild return their version of the error
// https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4
if ae, ok := err.(*Error); ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce") {
// clear any nonces that we might've stored that might now be
// considered bad
c.clearNonces()
retry := res.Header.Get("retry-after")
if err := sleep(retry, 1); err != nil {
return nil, err
}
continue
}
return nil, err
}
return res, nil
}
}
// postJWS signs the body with the given key and POSTs it to the provided url.
// The body argument must be JSON-serializable.
func (c *Client) postJWS(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, error) {
@@ -708,7 +726,7 @@ func (c *Client) postJWS(ctx context.Context, key crypto.Signer, url string, bod
if err != nil {
return nil, err
}
res, err := ctxhttp.Post(ctx, c.HTTPClient, url, "application/jose+json", bytes.NewReader(b))
res, err := c.post(ctx, url, "application/jose+json", bytes.NewReader(b))
if err != nil {
return nil, err
}
@@ -722,7 +740,7 @@ func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
if len(c.nonces) == 0 {
return fetchNonce(ctx, c.HTTPClient, url)
return c.fetchNonce(ctx, url)
}
var nonce string
for nonce = range c.nonces {
@@ -732,6 +750,13 @@ func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
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)
@@ -749,8 +774,58 @@ func (c *Client) addNonce(h http.Header) {
c.nonces[v] = struct{}{}
}
func fetchNonce(ctx context.Context, client *http.Client, url string) (string, error) {
resp, err := ctxhttp.Head(ctx, client, url)
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
func (c *Client) get(ctx context.Context, urlStr string) (*http.Response, error) {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return nil, err
}
return c.do(ctx, req)
}
func (c *Client) head(ctx context.Context, urlStr string) (*http.Response, error) {
req, err := http.NewRequest("HEAD", urlStr, nil)
if err != nil {
return nil, err
}
return c.do(ctx, req)
}
func (c *Client) post(ctx context.Context, urlStr, contentType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", urlStr, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.do(ctx, req)
}
func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) {
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) fetchNonce(ctx context.Context, url string) (string, error) {
resp, err := c.head(ctx, url)
if err != nil {
return "", err
}
@@ -769,7 +844,7 @@ func nonceFromHeader(h http.Header) string {
return h.Get("Replay-Nonce")
}
func responseCert(ctx context.Context, client *http.Client, res *http.Response, bundle bool) ([][]byte, error) {
func (c *Client) responseCert(ctx context.Context, res *http.Response, bundle bool) ([][]byte, error) {
b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1))
if err != nil {
return nil, fmt.Errorf("acme: response stream: %v", err)
@@ -793,7 +868,7 @@ func responseCert(ctx context.Context, client *http.Client, res *http.Response,
return nil, errors.New("acme: rel=up link is too large")
}
for _, url := range up {
cc, err := chainCert(ctx, client, url, 0)
cc, err := c.chainCert(ctx, url, 0)
if err != nil {
return nil, err
}
@@ -807,14 +882,8 @@ func responseError(resp *http.Response) error {
// don't care if ReadAll returns an error:
// json.Unmarshal will fail in that case anyway
b, _ := ioutil.ReadAll(resp.Body)
e := struct {
Status int
Type string
Detail string
}{
Status: resp.StatusCode,
}
if err := json.Unmarshal(b, &e); err != nil {
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
@@ -823,12 +892,7 @@ func responseError(resp *http.Response) error {
e.Detail = resp.Status
}
}
return &Error{
StatusCode: e.Status,
ProblemType: e.Type,
Detail: e.Detail,
Header: resp.Header,
}
return e.error(resp.Header)
}
// chainCert fetches CA certificate chain recursively by following "up" links.
@@ -836,12 +900,12 @@ func responseError(resp *http.Response) error {
// if the recursion level reaches maxChainLen.
//
// First chainCert call starts with depth of 0.
func chainCert(ctx context.Context, client *http.Client, url string, depth int) ([][]byte, error) {
func (c *Client) chainCert(ctx context.Context, url string, depth int) ([][]byte, error) {
if depth >= maxChainLen {
return nil, errors.New("acme: certificate chain is too deep")
}
res, err := ctxhttp.Get(ctx, client, url)
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
@@ -863,7 +927,7 @@ func chainCert(ctx context.Context, client *http.Client, url string, depth int)
return nil, errors.New("acme: certificate chain is too large")
}
for _, up := range uplink {
cc, err := chainCert(ctx, client, up, depth+1)
cc, err := c.chainCert(ctx, up, depth+1)
if err != nil {
return nil, err
}
@@ -893,6 +957,28 @@ func linkHeader(h http.Header, rel string) []string {
return links
}
// sleeper returns a function that accepts the Retry-After HTTP header value
// and an increment that's used with backoff to increasingly sleep on
// consecutive calls until the context is done. If the Retry-After header
// cannot be parsed, then backoff is used with a maximum sleep time of 10
// seconds.
func sleeper(ctx context.Context) func(ra string, inc int) error {
var count int
return func(ra string, inc int) error {
count += inc
d := backoff(count, 10*time.Second)
d = retryAfter(ra, d)
wakeup := time.NewTimer(d)
defer wakeup.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeup.C:
return nil
}
}
}
// retryAfter parses a Retry-After HTTP header value,
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
// It returns d if v cannot be parsed.
@@ -974,7 +1060,8 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}
tmpl.DNSNames = san

View File

@@ -6,6 +6,7 @@ package acme
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
@@ -23,8 +24,6 @@ import (
"strings"
"testing"
"time"
"golang.org/x/net/context"
)
// Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided
@@ -544,6 +543,9 @@ func TestWaitAuthorizationInvalid(t *testing.T) {
if err == nil {
t.Error("err is nil")
}
if _, ok := err.(*AuthorizationError); !ok {
t.Errorf("err is %T; want *AuthorizationError", err)
}
}
}
@@ -981,7 +983,8 @@ func TestNonce_fetch(t *testing.T) {
defer ts.Close()
for ; i < len(tests); i++ {
test := tests[i]
n, err := fetchNonce(context.Background(), http.DefaultClient, ts.URL)
c := &Client{}
n, err := c.fetchNonce(context.Background(), ts.URL)
if n != test.nonce {
t.Errorf("%d: n=%q; want %q", i, n, test.nonce)
}
@@ -999,7 +1002,8 @@ func TestNonce_fetchError(t *testing.T) {
w.WriteHeader(http.StatusTooManyRequests)
}))
defer ts.Close()
_, err := fetchNonce(context.Background(), http.DefaultClient, ts.URL)
c := &Client{}
_, err := c.fetchNonce(context.Background(), ts.URL)
e, ok := err.(*Error)
if !ok {
t.Fatalf("err is %T; want *Error", err)
@@ -1064,6 +1068,44 @@ func TestNonce_postJWS(t *testing.T) {
}
}
func TestRetryPostJWS(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)
if err != nil {
t.Errorf("decodeJWSHead: %v", err)
} else if head.Nonce == "" {
t.Error("head.Nonce is empty")
} else if head.Nonce == "nonce1" {
// return a badNonce error to force the call to retry
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, 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 != 4 {
t.Errorf("total requests count: %d; want 4", count)
}
}
func TestLinkHeader(t *testing.T) {
h := http.Header{"Link": {
`<https://example.com/acme/new-authz>;rel="next"`,

View File

@@ -10,6 +10,7 @@ package autocert
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@@ -30,9 +31,14 @@ import (
"time"
"golang.org/x/crypto/acme"
"golang.org/x/net/context"
)
// createCertRetryAfter is how much time to wait before removing a failed state
// entry due to an unsuccessful createCert call.
// This is a variable instead of a const for testing.
// TODO: Consider making it configurable or an exp backoff?
var createCertRetryAfter = time.Minute
// pseudoRand is safe for concurrent use.
var pseudoRand *lockedMathRand
@@ -41,8 +47,9 @@ func init() {
pseudoRand = &lockedMathRand{rnd: mathrand.New(src)}
}
// AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service
// during account registration.
// AcceptTOS is a Manager.Prompt function that always returns true to
// indicate acceptance of the CA's Terms of Service during account
// registration.
func AcceptTOS(tosURL string) bool { return true }
// HostPolicy specifies which host names the Manager is allowed to respond to.
@@ -76,18 +83,6 @@ func defaultHostPolicy(context.Context, string) error {
// It obtains and refreshes certificates automatically,
// as well as providing them to a TLS server via tls.Config.
//
// A simple usage example:
//
// m := autocert.Manager{
// Prompt: autocert.AcceptTOS,
// HostPolicy: autocert.HostWhitelist("example.org"),
// }
// s := &http.Server{
// Addr: ":https",
// TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
// }
// s.ListenAndServeTLS("", "")
//
// To preserve issued certificates and improve overall performance,
// use a cache implementation of Cache. For instance, DirCache.
type Manager struct {
@@ -123,7 +118,7 @@ type Manager struct {
// RenewBefore optionally specifies how early certificates should
// be renewed before they expire.
//
// If zero, they're renewed 1 week before expiration.
// If zero, they're renewed 30 days before expiration.
RenewBefore time.Duration
// Client is used to perform low-level operations, such as account registration
@@ -173,10 +168,23 @@ type Manager struct {
// The error is propagated back to the caller of GetCertificate and is user-visible.
// This does not affect cached certs. See HostPolicy field description for more details.
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if m.Prompt == nil {
return nil, errors.New("acme/autocert: Manager.Prompt not set")
}
name := hello.ServerName
if name == "" {
return nil, errors.New("acme/autocert: missing server name")
}
if !strings.Contains(strings.Trim(name, "."), ".") {
return nil, errors.New("acme/autocert: server name component count invalid")
}
if strings.ContainsAny(name, `/\`) {
return nil, errors.New("acme/autocert: server name contains invalid character")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// check whether this is a token cert requested for TLS-SNI challenge
if strings.HasSuffix(name, ".acme.invalid") {
@@ -185,7 +193,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
if cert := m.tokenCert[name]; cert != nil {
return cert, nil
}
if cert, err := m.cacheGet(name); err == nil {
if cert, err := m.cacheGet(ctx, name); err == nil {
return cert, nil
}
// TODO: cache error results?
@@ -194,7 +202,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
// regular domain
name = strings.TrimSuffix(name, ".") // golang.org/issue/18114
cert, err := m.cert(name)
cert, err := m.cert(ctx, name)
if err == nil {
return cert, nil
}
@@ -203,7 +211,6 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
}
// first-time
ctx := context.Background() // TODO: use a deadline?
if err := m.hostPolicy()(ctx, name); err != nil {
return nil, err
}
@@ -211,14 +218,14 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
if err != nil {
return nil, err
}
m.cachePut(name, cert)
m.cachePut(ctx, name, cert)
return cert, nil
}
// cert returns an existing certificate either from m.state or cache.
// If a certificate is found in cache but not in m.state, the latter will be filled
// with the cached value.
func (m *Manager) cert(name string) (*tls.Certificate, error) {
func (m *Manager) cert(ctx context.Context, name string) (*tls.Certificate, error) {
m.stateMu.Lock()
if s, ok := m.state[name]; ok {
m.stateMu.Unlock()
@@ -227,7 +234,7 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
return s.tlscert()
}
defer m.stateMu.Unlock()
cert, err := m.cacheGet(name)
cert, err := m.cacheGet(ctx, name)
if err != nil {
return nil, err
}
@@ -249,12 +256,11 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
}
// cacheGet always returns a valid certificate, or an error otherwise.
func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
// If a cached certficate exists but is not valid, ErrCacheMiss is returned.
func (m *Manager) cacheGet(ctx context.Context, domain string) (*tls.Certificate, error) {
if m.Cache == nil {
return nil, ErrCacheMiss
}
// TODO: might want to define a cache timeout on m
ctx := context.Background()
data, err := m.Cache.Get(ctx, domain)
if err != nil {
return nil, err
@@ -263,7 +269,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
// private
priv, pub := pem.Decode(data)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: no private key found in cache")
return nil, ErrCacheMiss
}
privKey, err := parsePrivateKey(priv.Bytes)
if err != nil {
@@ -281,13 +287,14 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
pubDER = append(pubDER, b.Bytes)
}
if len(pub) > 0 {
return nil, errors.New("acme/autocert: invalid public key")
// Leftover content not consumed by pem.Decode. Corrupt. Ignore.
return nil, ErrCacheMiss
}
// verify and create TLS cert
leaf, err := validCert(domain, pubDER, privKey)
if err != nil {
return nil, err
return nil, ErrCacheMiss
}
tlscert := &tls.Certificate{
Certificate: pubDER,
@@ -297,7 +304,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
return tlscert, nil
}
func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
func (m *Manager) cachePut(ctx context.Context, domain string, tlscert *tls.Certificate) error {
if m.Cache == nil {
return nil
}
@@ -329,8 +336,6 @@ func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
}
}
// TODO: might want to define a cache timeout on m
ctx := context.Background()
return m.Cache.Put(ctx, domain, buf.Bytes())
}
@@ -370,6 +375,23 @@ func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certifica
der, leaf, err := m.authorizedCert(ctx, state.key, domain)
if err != nil {
// Remove the failed state after some time,
// making the manager call createCert again on the following TLS hello.
time.AfterFunc(createCertRetryAfter, func() {
defer testDidRemoveState(domain)
m.stateMu.Lock()
defer m.stateMu.Unlock()
// Verify the state hasn't changed and it's still invalid
// before deleting.
s, ok := m.state[domain]
if !ok {
return
}
if _, err := validCert(domain, s.cert, s.key); err == nil {
return
}
delete(m.state, domain)
})
return nil, err
}
state.cert = der
@@ -418,7 +440,6 @@ func (m *Manager) certState(domain string) (*certState, error) {
// authorizedCert starts domain ownership verification process and requests a new cert upon success.
// The key argument is the certificate private key.
func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain string) (der [][]byte, leaf *x509.Certificate, err error) {
// TODO: make m.verify retry or retry m.verify calls here
if err := m.verify(ctx, domain); err != nil {
return nil, nil, err
}
@@ -494,7 +515,7 @@ func (m *Manager) verify(ctx context.Context, domain string) error {
if err != nil {
return err
}
m.putTokenCert(name, &cert)
m.putTokenCert(ctx, name, &cert)
defer func() {
// verification has ended at this point
// don't need token cert anymore
@@ -512,14 +533,14 @@ func (m *Manager) verify(ctx context.Context, domain string) error {
// putTokenCert stores the cert under the named key in both m.tokenCert map
// and m.Cache.
func (m *Manager) putTokenCert(name string, cert *tls.Certificate) {
func (m *Manager) putTokenCert(ctx context.Context, name string, cert *tls.Certificate) {
m.tokenCertMu.Lock()
defer m.tokenCertMu.Unlock()
if m.tokenCert == nil {
m.tokenCert = make(map[string]*tls.Certificate)
}
m.tokenCert[name] = cert
m.cachePut(name, cert)
m.cachePut(ctx, name, cert)
}
// deleteTokenCert removes the token certificate for the specified domain name
@@ -644,10 +665,10 @@ func (m *Manager) hostPolicy() HostPolicy {
}
func (m *Manager) renewBefore() time.Duration {
if m.RenewBefore > maxRandRenew {
if m.RenewBefore > renewJitter {
return m.RenewBefore
}
return 7 * 24 * time.Hour // 1 week
return 720 * time.Hour // 30 days
}
// certState is ready when its mutex is unlocked for reading.
@@ -789,5 +810,10 @@ func (r *lockedMathRand) int63n(max int64) int64 {
return n
}
// for easier testing
var timeNow = time.Now
// For easier testing.
var (
timeNow = time.Now
// Called when a state is removed.
testDidRemoveState = func(domain string) {}
)

View File

@@ -5,6 +5,7 @@
package autocert
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@@ -27,7 +28,6 @@ import (
"time"
"golang.org/x/crypto/acme"
"golang.org/x/net/context"
)
var discoTmpl = template.Must(template.New("disco").Parse(`{
@@ -150,7 +150,7 @@ func TestGetCertificate_ForceRSA(t *testing.T) {
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
testGetCertificate(t, man, "example.org", hello)
cert, err := man.cacheGet("example.org")
cert, err := man.cacheGet(context.Background(), "example.org")
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
@@ -159,9 +159,110 @@ func TestGetCertificate_ForceRSA(t *testing.T) {
}
}
// tests man.GetCertificate flow using the provided hello argument.
func TestGetCertificate_nilPrompt(t *testing.T) {
man := &Manager{}
defer man.stopRenew()
url, finish := startACMEServerStub(t, man, "example.org")
defer finish()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
man.Client = &acme.Client{
Key: key,
DirectoryURL: url,
}
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
if _, err := man.GetCertificate(hello); err == nil {
t.Error("got certificate for example.org; wanted error")
}
}
func TestGetCertificate_expiredCache(t *testing.T) {
// Make an expired cert and cache it.
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "example.org"},
NotAfter: time.Now(),
}
pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pk.PublicKey, pk)
if err != nil {
t.Fatal(err)
}
tlscert := &tls.Certificate{
Certificate: [][]byte{pub},
PrivateKey: pk,
}
man := &Manager{Prompt: AcceptTOS, Cache: newMemCache()}
defer man.stopRenew()
if err := man.cachePut(context.Background(), "example.org", tlscert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
// The expired cached cert should trigger a new cert issuance
// and return without an error.
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
testGetCertificate(t, man, "example.org", hello)
}
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()
const example = "example.org"
d := createCertRetryAfter
f := testDidRemoveState
defer func() {
createCertRetryAfter = d
testDidRemoveState = f
}()
createCertRetryAfter = 0
done := make(chan struct{})
testDidRemoveState = func(domain string) {
if domain != example {
t.Errorf("testDidRemoveState: domain = %q; want %q", domain, example)
}
close(done)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
man := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{
Key: key,
DirectoryURL: ts.URL,
},
}
defer man.stopRenew()
hello := &tls.ClientHelloInfo{ServerName: example}
if _, err := man.GetCertificate(hello); err == nil {
t.Error("GetCertificate: err is nil")
}
select {
case <-time.After(5 * time.Second):
t.Errorf("took too long to remove the %q state", example)
case <-done:
man.stateMu.Lock()
defer man.stateMu.Unlock()
if v, exist := man.state[example]; exist {
t.Errorf("state exists for %q: %+v", example, v)
}
}
}
// startACMEServerStub runs an ACME server
// The domain argument is the expected domain name of a certificate request.
func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
func startACMEServerStub(t *testing.T, man *Manager, domain string) (url string, finish func()) {
// echo token-02 | shasum -a 256
// then divide result in 2 parts separated by dot
tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid"
@@ -187,7 +288,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
// discovery
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
t.Fatalf("discoTmpl: %v", err)
t.Errorf("discoTmpl: %v", err)
}
// client key registration
case "/new-reg":
@@ -197,7 +298,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
w.Header().Set("location", ca.URL+"/authz/1")
w.WriteHeader(http.StatusCreated)
if err := authzTmpl.Execute(w, ca.URL); err != nil {
t.Fatalf("authzTmpl: %v", err)
t.Errorf("authzTmpl: %v", err)
}
// accept tls-sni-02 challenge
case "/challenge/2":
@@ -215,14 +316,14 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
t.Fatalf("new-cert: CSR: %v", err)
t.Errorf("new-cert: CSR: %v", err)
}
if csr.Subject.CommonName != domain {
t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain)
}
der, err := dummyCert(csr.PublicKey, domain)
if err != nil {
t.Fatalf("new-cert: dummyCert: %v", err)
t.Errorf("new-cert: dummyCert: %v", err)
}
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
w.Header().Set("link", chainUp)
@@ -232,14 +333,51 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
case "/ca-cert":
der, err := dummyCert(nil, "ca")
if err != nil {
t.Fatalf("ca-cert: dummyCert: %v", err)
t.Errorf("ca-cert: dummyCert: %v", err)
}
w.Write(der)
default:
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
defer ca.Close()
finish = func() {
ca.Close()
// make sure token cert was removed
cancel := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
if _, err := man.GetCertificate(hello); err != nil {
return
}
select {
case <-tick.C:
case <-cancel:
return
}
}
}()
select {
case <-done:
case <-time.After(5 * time.Second):
close(cancel)
t.Error("token cert was not removed")
<-done
}
}
return ca.URL, finish
}
// tests man.GetCertificate flow using the provided hello argument.
// The domain argument is the expected domain name of a certificate request.
func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
url, finish := startACMEServerStub(t, man, domain)
defer finish()
// use EC key to run faster on 386
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -248,7 +386,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
}
man.Client = &acme.Client{
Key: key,
DirectoryURL: ca.URL,
DirectoryURL: url,
}
// simulate tls.Config.GetCertificate
@@ -279,23 +417,6 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, domain)
}
// make sure token cert was removed
done = make(chan struct{})
go func() {
for {
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
if _, err := man.GetCertificate(hello); err != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
close(done)
}()
select {
case <-time.After(5 * time.Second):
t.Error("token cert was not removed")
case <-done:
}
}
func TestAccountKeyCache(t *testing.T) {
@@ -335,10 +456,11 @@ func TestCache(t *testing.T) {
man := &Manager{Cache: newMemCache()}
defer man.stopRenew()
if err := man.cachePut("example.org", tlscert); err != nil {
ctx := context.Background()
if err := man.cachePut(ctx, "example.org", tlscert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
res, err := man.cacheGet("example.org")
res, err := man.cacheGet(ctx, "example.org")
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
@@ -438,3 +560,47 @@ func TestValidCert(t *testing.T) {
}
}
}
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(&tls.ClientHelloInfo{ServerName: tt.name})
got := fmt.Sprint(err)
if got != tt.wantErr {
t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr)
}
}
}

View File

@@ -5,12 +5,11 @@
package autocert
import (
"context"
"errors"
"io/ioutil"
"os"
"path/filepath"
"golang.org/x/net/context"
)
// ErrCacheMiss is returned when a certificate is not found in cache.
@@ -78,12 +77,13 @@ func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
if tmp, err = d.writeTempFile(name, data); err != nil {
return
}
// prevent overwriting the file if the context was cancelled
if ctx.Err() != nil {
return // no need to set err
select {
case <-ctx.Done():
// Don't overwrite the file if the context was canceled.
default:
newName := filepath.Join(string(d), name)
err = os.Rename(tmp, newName)
}
name = filepath.Join(string(d), name)
err = os.Rename(tmp, name)
}()
select {
case <-ctx.Done():

View File

@@ -5,13 +5,12 @@
package autocert
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"golang.org/x/net/context"
)
// make sure DirCache satisfies Cache interface

View File

@@ -0,0 +1,34 @@
// 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 (
"crypto/tls"
"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{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org"),
}
s := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
}
s.ListenAndServeTLS("", "")
}

153
vendor/golang.org/x/crypto/acme/autocert/listener.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
// 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 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 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{
m: m,
conf: &tls.Config{
GetCertificate: m.GetCertificate, // bonus: panic on nil m
},
}
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
return ln
}
type listener struct {
m *Manager
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)
}

View File

@@ -5,15 +5,14 @@
package autocert
import (
"context"
"crypto"
"sync"
"time"
"golang.org/x/net/context"
)
// maxRandRenew is a maximum deviation from Manager.RenewBefore.
const maxRandRenew = time.Hour
// 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.
@@ -65,7 +64,7 @@ func (dr *domainRenewal) renew() {
// TODO: rotate dr.key at some point?
next, err := dr.do(ctx)
if err != nil {
next = maxRandRenew / 2
next = renewJitter / 2
next += time.Duration(pseudoRand.int63n(int64(next)))
}
dr.timer = time.AfterFunc(next, dr.renew)
@@ -83,9 +82,9 @@ func (dr *domainRenewal) renew() {
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(dr.domain); err == nil {
if tlscert, err := dr.m.cacheGet(ctx, dr.domain); err == nil {
next := dr.next(tlscert.Leaf.NotAfter)
if next > dr.m.renewBefore()+maxRandRenew {
if next > dr.m.renewBefore()+renewJitter {
return next, nil
}
}
@@ -103,7 +102,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
if err != nil {
return 0, err
}
dr.m.cachePut(dr.domain, tlscert)
dr.m.cachePut(ctx, dr.domain, tlscert)
dr.m.stateMu.Lock()
defer dr.m.stateMu.Unlock()
// m.state is guaranteed to be non-nil at this point
@@ -114,7 +113,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
d := expiry.Sub(timeNow()) - dr.m.renewBefore()
// add a bit of randomness to renew deadline
n := pseudoRand.int63n(int64(maxRandRenew))
n := pseudoRand.int63n(int64(renewJitter))
d -= time.Duration(n)
if d < 0 {
return 0

View File

@@ -5,6 +5,7 @@
package autocert
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -31,7 +32,7 @@ func TestRenewalNext(t *testing.T) {
expiry time.Time
min, max time.Duration
}{
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - maxRandRenew, 83 * 24 * time.Hour},
{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},
@@ -127,7 +128,7 @@ func TestRenewFromCache(t *testing.T) {
t.Fatal(err)
}
tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}}
if err := man.cachePut(domain, tlscert); err != nil {
if err := man.cachePut(context.Background(), domain, tlscert); err != nil {
t.Fatal(err)
}
@@ -151,7 +152,7 @@ func TestRenewFromCache(t *testing.T) {
// ensure the new cert is cached
after := time.Now().Add(future)
tlscert, err := man.cacheGet(domain)
tlscert, err := man.cacheGet(context.Background(), domain)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}

View File

@@ -134,7 +134,7 @@ func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
return "ES256", crypto.SHA256
case "P-384":
return "ES384", crypto.SHA384
case "P-512":
case "P-521":
return "ES512", crypto.SHA512
}
}

View File

@@ -12,11 +12,13 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"testing"
)
const testKeyPEM = `
const (
testKeyPEM = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30
@@ -46,10 +48,9 @@ EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO
-----END RSA PRIVATE KEY-----
`
// This thumbprint is for the testKey defined above.
const testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
// This thumbprint is for the testKey defined above.
testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
const (
// openssl ecparam -name secp256k1 -genkey -noout
testKeyECPEM = `
-----BEGIN EC PRIVATE KEY-----
@@ -58,39 +59,78 @@ AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5
QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ==
-----END EC PRIVATE KEY-----
`
// 1. opnessl ec -in key.pem -noout -text
// 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. covert each with: echo <val> | xxd -r -p | base64 | tr -d '=' | tr '/+' '_-'
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
// 3. convert each with: echo <val> | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-'
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 | base64 | tr -d '=' | tr '/+' '_-'
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
)
var (
testKey *rsa.PrivateKey
testKeyEC *ecdsa.PrivateKey
testKey *rsa.PrivateKey
testKeyEC *ecdsa.PrivateKey
testKeyEC384 *ecdsa.PrivateKey
testKeyEC512 *ecdsa.PrivateKey
)
func init() {
d, _ := pem.Decode([]byte(testKeyPEM))
if d == nil {
panic("no block found in testKeyPEM")
}
var err error
testKey, err = x509.ParsePKCS1PrivateKey(d.Bytes)
if err != nil {
panic(err.Error())
}
testKey = parseRSA(testKeyPEM, "testKeyPEM")
testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM")
testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM")
testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM")
}
if d, _ = pem.Decode([]byte(testKeyECPEM)); d == nil {
panic("no block found in testKeyECPEM")
func decodePEM(s, name string) []byte {
d, _ := pem.Decode([]byte(s))
if d == nil {
panic("no block found in " + name)
}
testKeyEC, err = x509.ParseECPrivateKey(d.Bytes)
return d.Bytes
}
func parseRSA(s, name string) *rsa.PrivateKey {
b := decodePEM(s, name)
k, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
panic(err.Error())
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) {
@@ -141,50 +181,63 @@ func TestJWSEncodeJSON(t *testing.T) {
}
func TestJWSEncodeJSONEC(t *testing.T) {
claims := struct{ Msg string }{"Hello JWS"}
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, "nonce")
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 := jwsEncodeJSON(claims, testKeyEC, "nonce")
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 b, err = base64.RawURLEncoding.DecodeString(jws.Protected); err != nil {
t.Fatalf("jws.Protected: %v", err)
}
var head struct {
Alg string
Nonce string
JWK struct {
Crv string
Kty string
X string
Y string
} `json:"jwk"`
}
if err := json.Unmarshal(b, &head); err != nil {
t.Fatalf("jws.Protected: %v", err)
}
if head.Alg != "ES256" {
t.Errorf("head.Alg = %q; want ES256", head.Alg)
}
if head.Nonce != "nonce" {
t.Errorf("head.Nonce = %q; want nonce", head.Nonce)
}
if head.JWK.Crv != "P-256" {
t.Errorf("head.JWK.Crv = %q; want P-256", head.JWK.Crv)
}
if head.JWK.Kty != "EC" {
t.Errorf("head.JWK.Kty = %q; want EC", head.JWK.Kty)
}
if head.JWK.X != testKeyECPubX {
t.Errorf("head.JWK.X = %q; want %q", head.JWK.X, testKeyECPubX)
}
if head.JWK.Y != testKeyECPubY {
t.Errorf("head.JWK.Y = %q; want %q", head.JWK.Y, testKeyECPubY)
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
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.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)
}
}
}

View File

@@ -1,9 +1,15 @@
// 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 (
"errors"
"fmt"
"net/http"
"strings"
"time"
)
// ACME server response statuses used to describe Authorization and Challenge states.
@@ -33,14 +39,8 @@ const (
CRLReasonAACompromise CRLReasonCode = 10
)
var (
// ErrAuthorizationFailed indicates that an authorization for an identifier
// did not succeed.
ErrAuthorizationFailed = errors.New("acme: identifier authorization failed")
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
)
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
// Error is an ACME error, defined in Problem Details for HTTP APIs doc
// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
@@ -53,6 +53,7 @@ type Error struct {
// Detail is a human-readable explanation specific to this occurrence of the problem.
Detail string
// Header is the original server error response headers.
// It may be nil.
Header http.Header
}
@@ -60,6 +61,50 @@ func (e *Error) Error() string {
return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
}
// 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()
}
return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
}
// 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"), 0), true
}
// Account is a user account. It is associated with a private key.
type Account struct {
// URI is the account unique ID, which is also a URL used to retrieve
@@ -118,6 +163,8 @@ type Directory struct {
}
// 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-sni-02", "dns-01".
Type string
@@ -130,6 +177,11 @@ type Challenge struct {
// Status identifies the status of this challenge.
Status string
// Error indicates the reason for an authorization failure
// when this challenge was used.
// The type of a non-nil value is *Error.
Error error
}
// Authorization encodes an authorization response.
@@ -187,12 +239,26 @@ func (z *wireAuthz) authorization(uri string) *Authorization {
return a
}
func (z *wireAuthz) error(uri string) *AuthorizationError {
err := &AuthorizationError{
URI: uri,
Identifier: z.Identifier.Value,
}
for _, raw := range z.Challenges {
if raw.Error != nil {
err.Errors = append(err.Errors, raw.Error.error(nil))
}
}
return err
}
// wireChallenge is ACME JSON challenge representation.
type wireChallenge struct {
URI string `json:"uri"`
Type string
Token string
Status string
Error *wireError
}
func (c *wireChallenge) challenge() *Challenge {
@@ -205,5 +271,25 @@ func (c *wireChallenge) challenge() *Challenge {
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
}
func (e *wireError) error(h http.Header) *Error {
return &Error{
StatusCode: e.Status,
ProblemType: e.Type,
Detail: e.Detail,
Header: h,
}
}

63
vendor/golang.org/x/crypto/acme/types_test.go generated vendored Normal file
View File

@@ -0,0 +1,63 @@
// 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"
"testing"
"time"
)
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)
}
}
}