Most important change in this CL is that Client is now able to correctly format and sign requests in KID form with a valid "kid" value. According to the RFC, most requests must include KID field in the protected head of JWS requests. The KID value is the account identity provided by the CA during registration. The KID value is also the Account URL. Hence, the CL is tied to account management. Updates golang/go#21081 Change-Id: I13f51e1fc52db7596eb933b47fa2014beb93c1ab Reviewed-on: https://go-review.googlesource.com/c/crypto/+/191602 Run-TryBot: Alex Vaghin <ddos@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
241 lines
6.4 KiB
Go
241 lines
6.4 KiB
Go
// 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/ioutil"
|
|
"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: ioutil.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 != 4 {
|
|
t.Errorf("total requests count: %d; want 4", 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(`{}`))
|
|
}))
|
|
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)
|
|
}
|
|
}
|
|
}
|