The standard library context package has been available since Go 1.7, and the Go build dashboard (build.golang.org) only tests master and the past two releases. Also, the acme package makes no backwards compatibility guarantees, not that I expect this to influence many people. Change-Id: Ia1a294212e1e531f28f53cd954d4743a64611cab Reviewed-on: https://go-review.googlesource.com/39272 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Alex Vaghin <ddos@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
192 lines
4.7 KiB
Go
192 lines
4.7 KiB
Go
// 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/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/acme"
|
|
)
|
|
|
|
func TestRenewalNext(t *testing.T) {
|
|
now := time.Now()
|
|
timeNow = func() time.Time { return now }
|
|
defer func() { timeNow = time.Now }()
|
|
|
|
man := &Manager{RenewBefore: 7 * 24 * time.Hour}
|
|
defer man.stopRenew()
|
|
tt := []struct {
|
|
expiry time.Time
|
|
min, max time.Duration
|
|
}{
|
|
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - maxRandRenew, 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) {
|
|
const domain = "example.org"
|
|
|
|
// ACME CA server stub
|
|
var ca *httptest.Server
|
|
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("replay-nonce", "nonce")
|
|
if r.Method == "HEAD" {
|
|
// a nonce request
|
|
return
|
|
}
|
|
|
|
switch r.URL.Path {
|
|
// discovery
|
|
case "/":
|
|
if err := discoTmpl.Execute(w, ca.URL); err != nil {
|
|
t.Fatalf("discoTmpl: %v", err)
|
|
}
|
|
// client key registration
|
|
case "/new-reg":
|
|
w.Write([]byte("{}"))
|
|
// domain authorization
|
|
case "/new-authz":
|
|
w.Header().Set("location", ca.URL+"/authz/1")
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(`{"status": "valid"}`))
|
|
// cert request
|
|
case "/new-cert":
|
|
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 {
|
|
t.Fatalf("new-cert: CSR: %v", err)
|
|
}
|
|
der, err := dummyCert(csr.PublicKey, domain)
|
|
if err != nil {
|
|
t.Fatalf("new-cert: dummyCert: %v", err)
|
|
}
|
|
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
|
|
w.Header().Set("link", chainUp)
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write(der)
|
|
// CA chain cert
|
|
case "/ca-cert":
|
|
der, err := dummyCert(nil, "ca")
|
|
if err != nil {
|
|
t.Fatalf("ca-cert: dummyCert: %v", err)
|
|
}
|
|
w.Write(der)
|
|
default:
|
|
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer ca.Close()
|
|
|
|
// use EC key to run faster on 386
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
man := &Manager{
|
|
Prompt: AcceptTOS,
|
|
Cache: newMemCache(),
|
|
RenewBefore: 24 * time.Hour,
|
|
Client: &acme.Client{
|
|
Key: key,
|
|
DirectoryURL: ca.URL,
|
|
},
|
|
}
|
|
defer man.stopRenew()
|
|
|
|
// cache an almost expired cert
|
|
now := time.Now()
|
|
cert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}}
|
|
if err := man.cachePut(context.Background(), domain, tlscert); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// veriy the renewal happened
|
|
defer func() {
|
|
testDidRenewLoop = func(next time.Duration, err error) {}
|
|
}()
|
|
done := make(chan struct{})
|
|
testDidRenewLoop = func(next time.Duration, err error) {
|
|
defer close(done)
|
|
if err != nil {
|
|
t.Errorf("testDidRenewLoop: %v", err)
|
|
}
|
|
// Next should be about 90 days:
|
|
// dummyCert 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(), domain)
|
|
if err != nil {
|
|
t.Fatalf("man.cacheGet: %v", err)
|
|
}
|
|
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[domain]
|
|
if s == nil {
|
|
t.Fatalf("m.state[%q] is nil", domain)
|
|
}
|
|
tlscert, err = s.tlscert()
|
|
if err != nil {
|
|
t.Fatalf("s.tlscert: %v", err)
|
|
}
|
|
if !tlscert.Leaf.NotAfter.After(after) {
|
|
t.Errorf("state leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
|
|
}
|
|
}
|
|
|
|
// trigger renew
|
|
hello := &tls.ClientHelloInfo{ServerName: domain}
|
|
if _, err := man.GetCertificate(hello); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// wait for renew loop
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("renew took too long to occur")
|
|
case <-done:
|
|
}
|
|
}
|