The change should reduce resource quota consumed by the client overall. Instead of sending HEAD to an ACME resource URL to get a new nonce, the Client will now try to fetch it from the Directory URL first and only then from the ACME resource URL if the former fails. This builds up on an abandoned https://golang.org/cl/34623, only this time with a fallback to the original behaviour. Change-Id: I6e75c0e524c4bc751f3a651b290c0ac2493e0628 Reviewed-on: https://go-review.googlesource.com/c/162057 Run-TryBot: Alex Vaghin <ddos@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1402 lines
38 KiB
Go
1402 lines
38 KiB
Go
// 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"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided
|
|
// interface.
|
|
func decodeJWSRequest(t *testing.T, v interface{}, r *http.Request) {
|
|
// Decode request
|
|
var req struct{ Payload string }
|
|
if err := json.NewDecoder(r.Body).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
|
|
JWK map[string]string `json:"jwk"`
|
|
}
|
|
|
|
func decodeJWSHead(r *http.Request) (*jwsHead, error) {
|
|
var req struct{ Protected string }
|
|
if err := json.NewDecoder(r.Body).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 TestDiscover(t *testing.T) {
|
|
const (
|
|
reg = "https://example.com/acme/new-reg"
|
|
authz = "https://example.com/acme/new-authz"
|
|
cert = "https://example.com/acme/new-cert"
|
|
revoke = "https://example.com/acme/revoke-cert"
|
|
)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Replay-Nonce", "testnonce")
|
|
fmt.Fprintf(w, `{
|
|
"new-reg": %q,
|
|
"new-authz": %q,
|
|
"new-cert": %q,
|
|
"revoke-cert": %q
|
|
}`, reg, authz, cert, revoke)
|
|
}))
|
|
defer ts.Close()
|
|
c := Client{DirectoryURL: ts.URL}
|
|
dir, err := c.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dir.RegURL != reg {
|
|
t.Errorf("dir.RegURL = %q; want %q", dir.RegURL, reg)
|
|
}
|
|
if dir.AuthzURL != authz {
|
|
t.Errorf("dir.AuthzURL = %q; want %q", dir.AuthzURL, authz)
|
|
}
|
|
if dir.CertURL != cert {
|
|
t.Errorf("dir.CertURL = %q; want %q", dir.CertURL, cert)
|
|
}
|
|
if dir.RevokeURL != revoke {
|
|
t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke)
|
|
}
|
|
if _, exist := c.nonces["testnonce"]; !exist {
|
|
t.Errorf("c.nonces = %q; want 'testnonce' in the map", c.nonces)
|
|
}
|
|
}
|
|
|
|
func TestRegister(t *testing.T) {
|
|
contacts := []string{"mailto:admin@example.com"}
|
|
|
|
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
|
|
Contact []string
|
|
Agreement string
|
|
}
|
|
decodeJWSRequest(t, &j, r)
|
|
|
|
// Test request
|
|
if j.Resource != "new-reg" {
|
|
t.Errorf("j.Resource = %q; want new-reg", j.Resource)
|
|
}
|
|
if !reflect.DeepEqual(j.Contact, contacts) {
|
|
t.Errorf("j.Contact = %v; want %v", j.Contact, contacts)
|
|
}
|
|
|
|
w.Header().Set("Location", "https://ca.tld/acme/reg/1")
|
|
w.Header().Set("Link", `<https://ca.tld/acme/new-authz>;rel="next"`)
|
|
w.Header().Add("Link", `<https://ca.tld/acme/recover-reg>;rel="recover"`)
|
|
w.Header().Add("Link", `<https://ca.tld/acme/terms>;rel="terms-of-service"`)
|
|
w.WriteHeader(http.StatusCreated)
|
|
b, _ := json.Marshal(contacts)
|
|
fmt.Fprintf(w, `{"contact": %s}`, b)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
prompt := func(url string) bool {
|
|
const terms = "https://ca.tld/acme/terms"
|
|
if url != terms {
|
|
t.Errorf("prompt url = %q; want %q", url, terms)
|
|
}
|
|
return false
|
|
}
|
|
|
|
c := Client{
|
|
Key: testKeyEC,
|
|
DirectoryURL: ts.URL,
|
|
dir: &Directory{RegURL: ts.URL},
|
|
}
|
|
a := &Account{Contact: contacts}
|
|
var err error
|
|
if a, err = c.Register(context.Background(), a, prompt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.URI != "https://ca.tld/acme/reg/1" {
|
|
t.Errorf("a.URI = %q; want https://ca.tld/acme/reg/1", a.URI)
|
|
}
|
|
if a.Authz != "https://ca.tld/acme/new-authz" {
|
|
t.Errorf("a.Authz = %q; want https://ca.tld/acme/new-authz", a.Authz)
|
|
}
|
|
if a.CurrentTerms != "https://ca.tld/acme/terms" {
|
|
t.Errorf("a.CurrentTerms = %q; want https://ca.tld/acme/terms", a.CurrentTerms)
|
|
}
|
|
if !reflect.DeepEqual(a.Contact, contacts) {
|
|
t.Errorf("a.Contact = %v; want %v", a.Contact, contacts)
|
|
}
|
|
}
|
|
|
|
func TestUpdateReg(t *testing.T) {
|
|
const terms = "https://ca.tld/acme/terms"
|
|
contacts := []string{"mailto:admin@example.com"}
|
|
|
|
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
|
|
Contact []string
|
|
Agreement string
|
|
}
|
|
decodeJWSRequest(t, &j, r)
|
|
|
|
// Test request
|
|
if j.Resource != "reg" {
|
|
t.Errorf("j.Resource = %q; want reg", j.Resource)
|
|
}
|
|
if j.Agreement != terms {
|
|
t.Errorf("j.Agreement = %q; want %q", j.Agreement, terms)
|
|
}
|
|
if !reflect.DeepEqual(j.Contact, contacts) {
|
|
t.Errorf("j.Contact = %v; want %v", j.Contact, contacts)
|
|
}
|
|
|
|
w.Header().Set("Link", `<https://ca.tld/acme/new-authz>;rel="next"`)
|
|
w.Header().Add("Link", `<https://ca.tld/acme/recover-reg>;rel="recover"`)
|
|
w.Header().Add("Link", fmt.Sprintf(`<%s>;rel="terms-of-service"`, terms))
|
|
w.WriteHeader(http.StatusOK)
|
|
b, _ := json.Marshal(contacts)
|
|
fmt.Fprintf(w, `{"contact":%s, "agreement":%q}`, b, terms)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
c := Client{Key: testKeyEC}
|
|
a := &Account{URI: ts.URL, Contact: contacts, AgreedTerms: terms}
|
|
var err error
|
|
if a, err = c.UpdateReg(context.Background(), a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Authz != "https://ca.tld/acme/new-authz" {
|
|
t.Errorf("a.Authz = %q; want https://ca.tld/acme/new-authz", a.Authz)
|
|
}
|
|
if a.AgreedTerms != terms {
|
|
t.Errorf("a.AgreedTerms = %q; want %q", a.AgreedTerms, terms)
|
|
}
|
|
if a.CurrentTerms != terms {
|
|
t.Errorf("a.CurrentTerms = %q; want %q", a.CurrentTerms, terms)
|
|
}
|
|
if a.URI != ts.URL {
|
|
t.Errorf("a.URI = %q; want %q", a.URI, ts.URL)
|
|
}
|
|
}
|
|
|
|
func TestGetReg(t *testing.T) {
|
|
const terms = "https://ca.tld/acme/terms"
|
|
const newTerms = "https://ca.tld/acme/new-terms"
|
|
contacts := []string{"mailto:admin@example.com"}
|
|
|
|
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
|
|
Contact []string
|
|
Agreement string
|
|
}
|
|
decodeJWSRequest(t, &j, r)
|
|
|
|
// Test request
|
|
if j.Resource != "reg" {
|
|
t.Errorf("j.Resource = %q; want reg", j.Resource)
|
|
}
|
|
if len(j.Contact) != 0 {
|
|
t.Errorf("j.Contact = %v", j.Contact)
|
|
}
|
|
if j.Agreement != "" {
|
|
t.Errorf("j.Agreement = %q", j.Agreement)
|
|
}
|
|
|
|
w.Header().Set("Link", `<https://ca.tld/acme/new-authz>;rel="next"`)
|
|
w.Header().Add("Link", `<https://ca.tld/acme/recover-reg>;rel="recover"`)
|
|
w.Header().Add("Link", fmt.Sprintf(`<%s>;rel="terms-of-service"`, newTerms))
|
|
w.WriteHeader(http.StatusOK)
|
|
b, _ := json.Marshal(contacts)
|
|
fmt.Fprintf(w, `{"contact":%s, "agreement":%q}`, b, terms)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
c := Client{Key: testKeyEC}
|
|
a, err := c.GetReg(context.Background(), ts.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Authz != "https://ca.tld/acme/new-authz" {
|
|
t.Errorf("a.AuthzURL = %q; want https://ca.tld/acme/new-authz", a.Authz)
|
|
}
|
|
if a.AgreedTerms != terms {
|
|
t.Errorf("a.AgreedTerms = %q; want %q", a.AgreedTerms, terms)
|
|
}
|
|
if a.CurrentTerms != newTerms {
|
|
t.Errorf("a.CurrentTerms = %q; want %q", a.CurrentTerms, newTerms)
|
|
}
|
|
if a.URI != ts.URL {
|
|
t.Errorf("a.URI = %q; want %q", a.URI, ts.URL)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// 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 TestGetAuthorization(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("r.Method = %q; want GET", r.Method)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, `{
|
|
"identifier": {"type":"dns","value":"example.com"},
|
|
"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]]}`)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cl := Client{Key: testKeyEC}
|
|
auth, err := cl.GetAuthorization(context.Background(), ts.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if auth.Status != "pending" {
|
|
t.Errorf("Status = %q; want pending", auth.Status)
|
|
}
|
|
if auth.Identifier.Type != "dns" {
|
|
t.Errorf("Identifier.Type = %q; want dns", auth.Identifier.Type)
|
|
}
|
|
if auth.Identifier.Value != "example.com" {
|
|
t.Errorf("Identifier.Value = %q; want example.com", auth.Identifier.Value)
|
|
}
|
|
|
|
if n := len(auth.Challenges); n != 2 {
|
|
t.Fatalf("len(set.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 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("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.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
_, err := runWaitAuthorization(ctx, t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Retry-After", "60")
|
|
fmt.Fprintf(w, `{"status":"pending"}`)
|
|
})
|
|
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(h)
|
|
defer ts.Close()
|
|
type res struct {
|
|
authz *Authorization
|
|
err error
|
|
}
|
|
ch := make(chan res, 1)
|
|
go func() {
|
|
var client Client
|
|
a, err := client.WaitAuthorization(ctx, ts.URL)
|
|
ch <- res{a, err}
|
|
}()
|
|
select {
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("WaitAuthorization took too long to return")
|
|
case v := <-ch:
|
|
return v.authz, v.err
|
|
}
|
|
panic("runWaitAuthorization: out of select")
|
|
}
|
|
|
|
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)
|
|
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}
|
|
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 TestPollChallenge(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("r.Method = %q; want GET", r.Method)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, `{
|
|
"type":"http-01",
|
|
"status":"pending",
|
|
"uri":"https://ca.tld/acme/challenge/publickey/id1",
|
|
"token":"token1"}`)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cl := Client{Key: testKeyEC}
|
|
chall, err := cl.GetChallenge(context.Background(), ts.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if chall.Status != "pending" {
|
|
t.Errorf("Status = %q; want pending", chall.Status)
|
|
}
|
|
if chall.Type != "http-01" {
|
|
t.Errorf("c.Type = %q; want http-01", chall.Type)
|
|
}
|
|
if chall.URI != "https://ca.tld/acme/challenge/publickey/id1" {
|
|
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", chall.URI)
|
|
}
|
|
if chall.Token != "token1" {
|
|
t.Errorf("c.Token = %q; want token1", chall.Token)
|
|
}
|
|
}
|
|
|
|
func TestAcceptChallenge(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
|
|
Type string
|
|
Auth string `json:"keyAuthorization"`
|
|
}
|
|
decodeJWSRequest(t, &j, r)
|
|
|
|
// Test request
|
|
if j.Resource != "challenge" {
|
|
t.Errorf(`resource = %q; want "challenge"`, j.Resource)
|
|
}
|
|
if j.Type != "http-01" {
|
|
t.Errorf(`type = %q; want "http-01"`, j.Type)
|
|
}
|
|
keyAuth := "token1." + testKeyECThumbprint
|
|
if j.Auth != keyAuth {
|
|
t.Errorf(`keyAuthorization = %q; want %q`, j.Auth, keyAuth)
|
|
}
|
|
|
|
// Respond to request
|
|
w.WriteHeader(http.StatusAccepted)
|
|
fmt.Fprintf(w, `{
|
|
"type":"http-01",
|
|
"status":"pending",
|
|
"uri":"https://ca.tld/acme/challenge/publickey/id1",
|
|
"token":"token1",
|
|
"keyAuthorization":%q
|
|
}`, keyAuth)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cl := Client{Key: testKeyEC}
|
|
c, err := cl.Accept(context.Background(), &Challenge{
|
|
URI: ts.URL,
|
|
Token: "token1",
|
|
Type: "http-01",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestNewCert(t *testing.T) {
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.AddDate(0, 2, 0)
|
|
timeNow = func() time.Time { return notBefore }
|
|
|
|
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 `json:"resource"`
|
|
CSR string `json:"csr"`
|
|
NotBefore string `json:"notBefore,omitempty"`
|
|
NotAfter string `json:"notAfter,omitempty"`
|
|
}
|
|
decodeJWSRequest(t, &j, r)
|
|
|
|
// Test request
|
|
if j.Resource != "new-cert" {
|
|
t.Errorf(`resource = %q; want "new-cert"`, j.Resource)
|
|
}
|
|
if j.NotBefore != notBefore.Format(time.RFC3339) {
|
|
t.Errorf(`notBefore = %q; wanted %q`, j.NotBefore, notBefore.Format(time.RFC3339))
|
|
}
|
|
if j.NotAfter != notAfter.Format(time.RFC3339) {
|
|
t.Errorf(`notAfter = %q; wanted %q`, j.NotAfter, notAfter.Format(time.RFC3339))
|
|
}
|
|
|
|
// Respond to request
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(int64(1)),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"goacme"},
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
sampleCert, err := x509.CreateCertificate(rand.Reader, &template, &template, &testKeyEC.PublicKey, testKeyEC)
|
|
if err != nil {
|
|
t.Fatalf("Error creating certificate: %v", err)
|
|
}
|
|
|
|
w.Header().Set("Location", "https://ca.tld/acme/cert/1")
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write(sampleCert)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
csr := x509.CertificateRequest{
|
|
Version: 0,
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
Organization: []string{"goacme"},
|
|
},
|
|
}
|
|
csrb, err := x509.CreateCertificateRequest(rand.Reader, &csr, testKeyEC)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := Client{Key: testKeyEC, dir: &Directory{CertURL: ts.URL}}
|
|
cert, certURL, err := c.CreateCert(context.Background(), csrb, notAfter.Sub(notBefore), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cert == nil {
|
|
t.Errorf("cert is nil")
|
|
}
|
|
if certURL != "https://ca.tld/acme/cert/1" {
|
|
t.Errorf("certURL = %q; want https://ca.tld/acme/cert/1", certURL)
|
|
}
|
|
}
|
|
|
|
func TestFetchCert(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 < 3 {
|
|
up := fmt.Sprintf("<%s>;rel=up", ts.URL)
|
|
w.Header().Set("Link", up)
|
|
}
|
|
w.Write([]byte{count})
|
|
}))
|
|
defer ts.Close()
|
|
res, err := (&Client{}).FetchCert(context.Background(), ts.URL, true)
|
|
if err != nil {
|
|
t.Fatalf("FetchCert: %v", err)
|
|
}
|
|
cert := [][]byte{{1}, {2}, {3}}
|
|
if !reflect.DeepEqual(res, cert) {
|
|
t.Errorf("res = %v; want %v", res, cert)
|
|
}
|
|
}
|
|
|
|
func TestFetchCertRetry(t *testing.T) {
|
|
var count int
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if count < 1 {
|
|
w.Header().Set("Retry-After", "0")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
count++
|
|
return
|
|
}
|
|
w.Write([]byte{1})
|
|
}))
|
|
defer ts.Close()
|
|
res, err := (&Client{}).FetchCert(context.Background(), ts.URL, false)
|
|
if err != nil {
|
|
t.Fatalf("FetchCert: %v", err)
|
|
}
|
|
cert := [][]byte{{1}}
|
|
if !reflect.DeepEqual(res, cert) {
|
|
t.Errorf("res = %v; want %v", res, cert)
|
|
}
|
|
}
|
|
|
|
func TestFetchCertCancel(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Retry-After", "0")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}))
|
|
defer ts.Close()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
var err error
|
|
go func() {
|
|
_, err = (&Client{}).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()
|
|
_, err := (&Client{}).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()
|
|
_, err := (&Client{}).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()
|
|
_, err := (&Client{}).FetchCert(context.Background(), ts.URL, false)
|
|
if err == nil {
|
|
t.Errorf("err is nil")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCert(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
|
|
}
|
|
|
|
var req struct {
|
|
Resource string
|
|
Certificate string
|
|
Reason int
|
|
}
|
|
decodeJWSRequest(t, &req, r)
|
|
if req.Resource != "revoke-cert" {
|
|
t.Errorf("req.Resource = %q; want revoke-cert", req.Resource)
|
|
}
|
|
if req.Reason != 1 {
|
|
t.Errorf("req.Reason = %d; want 1", req.Reason)
|
|
}
|
|
// echo -n cert | base64 | tr -d '=' | tr '/+' '_-'
|
|
cert := "Y2VydA"
|
|
if req.Certificate != cert {
|
|
t.Errorf("req.Certificate = %q; want %q", req.Certificate, cert)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
client := &Client{
|
|
Key: testKeyEC,
|
|
dir: &Directory{RevokeURL: ts.URL},
|
|
}
|
|
ctx := context.Background()
|
|
if err := client.RevokeCert(ctx, nil, []byte("cert"), CRLReasonKeyCompromise); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
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 := &Client{}
|
|
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 := &Client{}
|
|
_, 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 TestNonce_postJWS(t *testing.T) {
|
|
var count int
|
|
seen := make(map[string]bool)
|
|
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 do a HEAD request
|
|
// but only to fetch the first nonce.
|
|
return
|
|
}
|
|
// Make client.Authorize happy; we're not testing its result.
|
|
defer func() {
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(`{"status":"valid"}`))
|
|
}()
|
|
|
|
head, err := decodeJWSHead(r)
|
|
if err != nil {
|
|
t.Errorf("decodeJWSHead: %v", err)
|
|
return
|
|
}
|
|
if head.Nonce == "" {
|
|
t.Error("head.Nonce is empty")
|
|
return
|
|
}
|
|
if seen[head.Nonce] {
|
|
t.Errorf("nonce is already used: %q", head.Nonce)
|
|
}
|
|
seen[head.Nonce] = true
|
|
}))
|
|
defer ts.Close()
|
|
|
|
client := Client{
|
|
Key: testKey,
|
|
DirectoryURL: ts.URL, // nonces are fetched from here first
|
|
dir: &Directory{AuthzURL: ts.URL},
|
|
}
|
|
if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
|
|
t.Errorf("client.Authorize 1: %v", err)
|
|
}
|
|
// The second call should not generate another extra HEAD request.
|
|
if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
|
|
t.Errorf("client.Authorize 2: %v", err)
|
|
}
|
|
|
|
if count != 3 {
|
|
t.Errorf("total requests count: %d; want 3", count)
|
|
}
|
|
if n := len(client.nonces); n != 1 {
|
|
t.Errorf("len(client.nonces) = %d; want 1", n)
|
|
}
|
|
for k := range seen {
|
|
if _, exist := client.nonces[k]; exist {
|
|
t.Errorf("used nonce %q in client.nonces", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
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"
|
|
)
|
|
|
|
client := &Client{Key: testKeyEC}
|
|
tlscert, name, err := client.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"
|
|
)
|
|
|
|
client := &Client{Key: testKeyEC}
|
|
tlscert, name, err := client.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)
|
|
}
|
|
|
|
client := &Client{Key: testKeyEC}
|
|
tlscert, err := client.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 idPeACMEIdentifierV1.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 := &Client{Key: testKeyEC}
|
|
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 := &Client{Key: testKeyEC}
|
|
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"
|
|
|
|
client := &Client{Key: testKeyEC}
|
|
val, err := client.DNS01ChallengeRecord("xxx")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if val != value {
|
|
t.Errorf("val = %q; want %q", val, value)
|
|
}
|
|
}
|