This repository has been archived on 2019-06-23. You can view files and clone it, but cannot push or open issues/pull-requests.
mumbledj/vendor/golang.org/x/crypto/acme/internal/acme/acme_test.go

760 lines
20 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 (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"golang.org/x/net/context"
)
// 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)
}
}
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")
fmt.Fprintf(w, `{
"new-reg": %q,
"new-authz": %q,
"new-cert": %q,
"revoke-cert": %q
}`, reg, authz, cert, revoke)
}))
defer ts.Close()
ep, err := (&Client{}).Discover(ts.URL)
if err != nil {
t.Fatal(err)
}
if ep.RegURL != reg {
t.Errorf("RegURL = %q; want %q", ep.RegURL, reg)
}
if ep.AuthzURL != authz {
t.Errorf("authzURL = %q; want %q", ep.AuthzURL, authz)
}
if ep.CertURL != cert {
t.Errorf("certURL = %q; want %q", ep.CertURL, cert)
}
if ep.RevokeURL != revoke {
t.Errorf("revokeURL = %q; want %q", ep.RevokeURL, revoke)
}
}
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, `{
"key":%q,
"contact":%s
}`, testKeyThumbprint, b)
}))
defer ts.Close()
c := Client{Key: testKey}
a := &Account{Contact: contacts}
var err error
if a, err = c.Register(ts.URL, a); 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, `{
"key":%q,
"contact":%s,
"agreement":%q
}`, testKeyThumbprint, b, terms)
}))
defer ts.Close()
c := Client{Key: testKey}
a := &Account{Contact: contacts, AgreedTerms: terms}
var err error
if a, err = c.UpdateReg(ts.URL, 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)
}
}
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, `{
"key":%q,
"contact":%s,
"agreement":%q
}`, testKeyThumbprint, b, terms)
}))
defer ts.Close()
c := Client{Key: testKey}
a, err := c.GetReg(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)
}
}
func TestAuthorize(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 != "dns" {
t.Errorf("j.Identifier.Type = %q; want dns", j.Identifier.Type)
}
if j.Identifier.Value != "example.com" {
t.Errorf("j.Identifier.Value = %q; want example.com", j.Identifier.Value)
}
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
w.WriteHeader(http.StatusCreated)
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: testKey}
auth, err := cl.Authorize(ts.URL, "example.com")
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 != "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(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.Type)
}
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.Type)
}
combs := [][]int{{0}, {1}}
if !reflect.DeepEqual(auth.Combinations, combs) {
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
}
}
func TestPollAuthz(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: testKey}
auth, err := cl.GetAuthz(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.Type)
}
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.Type)
}
combs := [][]int{{0}, {1}}
if !reflect.DeepEqual(auth.Combinations, combs) {
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
}
}
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: testKey}
chall, err := cl.GetChallenge(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.Type)
}
}
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." + testKeyThumbprint
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: testKey}
c, err := cl.Accept(&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.Type)
}
}
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, &testKey.PublicKey, testKey)
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, testKey)
if err != nil {
t.Fatal(err)
}
c := Client{Key: testKey}
cert, certURL, err := c.CreateCert(context.Background(), ts.URL, 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) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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 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.StatusAccepted)
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 TestFetchNonce(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]
n, err := fetchNonce(http.DefaultClient, 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 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"`,
}}
tests := []struct{ in, out string }{
{"next", "https://example.com/acme/new-authz"},
{"recover", "https://example.com/acme/recover-reg"},
{"terms-of-service", "https://example.com/acme/terms"},
{"empty", ""},
}
for i, test := range tests {
if v := linkHeader(h, test.in); v != test.out {
t.Errorf("%d: parseLinkHeader(%q): %q; want %q", i, test.in, v, test.out)
}
}
}
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)
}
}