mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	Pull in https://github.com/tailscale/golang-x-crypto/pull/16 Updates #15542 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
		
			
				
	
	
		
			1025 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1025 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2019 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/hmac"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/sha256"
 | |
| 	"crypto/x509"
 | |
| 	"crypto/x509/pkix"
 | |
| 	"encoding/base64"
 | |
| 	"encoding/json"
 | |
| 	"encoding/pem"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"math/big"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // While contents of this file is pertinent only to RFC8555,
 | |
| // it is complementary to the tests in the other _test.go files
 | |
| // many of which are valid for both pre- and RFC8555.
 | |
| // This will make it easier to clean up the tests once non-RFC compliant
 | |
| // code is removed.
 | |
| 
 | |
| func TestRFC_Discover(t *testing.T) {
 | |
| 	const (
 | |
| 		nonce       = "https://example.com/acme/new-nonce"
 | |
| 		reg         = "https://example.com/acme/new-acct"
 | |
| 		order       = "https://example.com/acme/new-order"
 | |
| 		authz       = "https://example.com/acme/new-authz"
 | |
| 		revoke      = "https://example.com/acme/revoke-cert"
 | |
| 		keychange   = "https://example.com/acme/key-change"
 | |
| 		metaTerms   = "https://example.com/acme/terms/2017-5-30"
 | |
| 		metaWebsite = "https://www.example.com/"
 | |
| 		metaCAA     = "example.com"
 | |
| 	)
 | |
| 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 		fmt.Fprintf(w, `{
 | |
| 			"newNonce": %q,
 | |
| 			"newAccount": %q,
 | |
| 			"newOrder": %q,
 | |
| 			"newAuthz": %q,
 | |
| 			"revokeCert": %q,
 | |
| 			"keyChange": %q,
 | |
| 			"meta": {
 | |
| 				"termsOfService": %q,
 | |
| 				"website": %q,
 | |
| 				"caaIdentities": [%q],
 | |
| 				"externalAccountRequired": true
 | |
| 			}
 | |
| 		}`, nonce, reg, order, authz, revoke, keychange, metaTerms, metaWebsite, metaCAA)
 | |
| 	}))
 | |
| 	defer ts.Close()
 | |
| 	c := &Client{DirectoryURL: ts.URL}
 | |
| 	dir, err := c.Discover(context.Background())
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if dir.NonceURL != nonce {
 | |
| 		t.Errorf("dir.NonceURL = %q; want %q", dir.NonceURL, nonce)
 | |
| 	}
 | |
| 	if dir.RegURL != reg {
 | |
| 		t.Errorf("dir.RegURL = %q; want %q", dir.RegURL, reg)
 | |
| 	}
 | |
| 	if dir.OrderURL != order {
 | |
| 		t.Errorf("dir.OrderURL = %q; want %q", dir.OrderURL, order)
 | |
| 	}
 | |
| 	if dir.AuthzURL != authz {
 | |
| 		t.Errorf("dir.AuthzURL = %q; want %q", dir.AuthzURL, authz)
 | |
| 	}
 | |
| 	if dir.RevokeURL != revoke {
 | |
| 		t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke)
 | |
| 	}
 | |
| 	if dir.KeyChangeURL != keychange {
 | |
| 		t.Errorf("dir.KeyChangeURL = %q; want %q", dir.KeyChangeURL, keychange)
 | |
| 	}
 | |
| 	if dir.Terms != metaTerms {
 | |
| 		t.Errorf("dir.Terms = %q; want %q", dir.Terms, metaTerms)
 | |
| 	}
 | |
| 	if dir.Website != metaWebsite {
 | |
| 		t.Errorf("dir.Website = %q; want %q", dir.Website, metaWebsite)
 | |
| 	}
 | |
| 	if len(dir.CAA) == 0 || dir.CAA[0] != metaCAA {
 | |
| 		t.Errorf("dir.CAA = %q; want [%q]", dir.CAA, metaCAA)
 | |
| 	}
 | |
| 	if !dir.ExternalAccountRequired {
 | |
| 		t.Error("dir.Meta.ExternalAccountRequired is false")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_popNonce(t *testing.T) {
 | |
| 	var count int
 | |
| 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		// The Client uses only Directory.NonceURL when specified.
 | |
| 		// Expect no other URL paths.
 | |
| 		if r.URL.Path != "/new-nonce" {
 | |
| 			t.Errorf("r.URL.Path = %q; want /new-nonce", r.URL.Path)
 | |
| 		}
 | |
| 		if count > 0 {
 | |
| 			w.WriteHeader(http.StatusTooManyRequests)
 | |
| 			return
 | |
| 		}
 | |
| 		count++
 | |
| 		w.Header().Set("Replay-Nonce", "second")
 | |
| 	}))
 | |
| 	cl := &Client{
 | |
| 		DirectoryURL: ts.URL,
 | |
| 		dir:          &Directory{NonceURL: ts.URL + "/new-nonce"},
 | |
| 	}
 | |
| 	cl.addNonce(http.Header{"Replay-Nonce": {"first"}})
 | |
| 
 | |
| 	for i, nonce := range []string{"first", "second"} {
 | |
| 		v, err := cl.popNonce(context.Background(), "")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%d: cl.popNonce: %v", i, err)
 | |
| 		}
 | |
| 		if v != nonce {
 | |
| 			t.Errorf("%d: cl.popNonce = %q; want %q", i, v, nonce)
 | |
| 		}
 | |
| 	}
 | |
| 	// No more nonces and server replies with an error past first nonce fetch.
 | |
| 	// Expected to fail.
 | |
| 	if _, err := cl.popNonce(context.Background(), ""); err == nil {
 | |
| 		t.Error("last cl.popNonce returned nil error")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_postKID(t *testing.T) {
 | |
| 	var ts *httptest.Server
 | |
| 	ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		switch r.URL.Path {
 | |
| 		case "/new-nonce":
 | |
| 			w.Header().Set("Replay-Nonce", "nonce")
 | |
| 		case "/new-account":
 | |
| 			w.Header().Set("Location", "/account-1")
 | |
| 			w.Write([]byte(`{"status":"valid"}`))
 | |
| 		case "/post":
 | |
| 			b, _ := io.ReadAll(r.Body) // check err later in decodeJWSxxx
 | |
| 			head, err := decodeJWSHead(bytes.NewReader(b))
 | |
| 			if err != nil {
 | |
| 				t.Errorf("decodeJWSHead: %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			if head.KID != "/account-1" {
 | |
| 				t.Errorf("head.KID = %q; want /account-1", head.KID)
 | |
| 			}
 | |
| 			if len(head.JWK) != 0 {
 | |
| 				t.Errorf("head.JWK = %q; want zero map", head.JWK)
 | |
| 			}
 | |
| 			if v := ts.URL + "/post"; head.URL != v {
 | |
| 				t.Errorf("head.URL = %q; want %q", head.URL, v)
 | |
| 			}
 | |
| 
 | |
| 			var payload struct{ Msg string }
 | |
| 			decodeJWSRequest(t, &payload, bytes.NewReader(b))
 | |
| 			if payload.Msg != "ping" {
 | |
| 				t.Errorf("payload.Msg = %q; want ping", payload.Msg)
 | |
| 			}
 | |
| 			w.Write([]byte("pong"))
 | |
| 		default:
 | |
| 			t.Errorf("unhandled %s %s", r.Method, r.URL)
 | |
| 			w.WriteHeader(http.StatusBadRequest)
 | |
| 		}
 | |
| 	}))
 | |
| 	defer ts.Close()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	cl := &Client{
 | |
| 		Key:          testKey,
 | |
| 		DirectoryURL: ts.URL,
 | |
| 		dir: &Directory{
 | |
| 			NonceURL: ts.URL + "/new-nonce",
 | |
| 			RegURL:   ts.URL + "/new-account",
 | |
| 			OrderURL: "/force-rfc-mode",
 | |
| 		},
 | |
| 	}
 | |
| 	req := json.RawMessage(`{"msg":"ping"}`)
 | |
| 	res, err := cl.post(ctx, nil /* use kid */, ts.URL+"/post", req, wantStatus(http.StatusOK))
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer res.Body.Close()
 | |
| 	b, _ := io.ReadAll(res.Body) // don't care about err - just checking b
 | |
| 	if string(b) != "pong" {
 | |
| 		t.Errorf("res.Body = %q; want pong", b)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // acmeServer simulates a subset of RFC 8555 compliant CA.
 | |
| //
 | |
| // TODO: We also have x/crypto/acme/autocert/acmetest and startACMEServerStub in autocert_test.go.
 | |
| // It feels like this acmeServer is a sweet spot between usefulness and added complexity.
 | |
| // Also, acmetest and startACMEServerStub were both written for draft-02, no RFC support.
 | |
| // The goal is to consolidate all into one ACME test server.
 | |
| type acmeServer struct {
 | |
| 	ts      *httptest.Server
 | |
| 	handler map[string]http.HandlerFunc // keyed by r.URL.Path
 | |
| 
 | |
| 	mu     sync.Mutex
 | |
| 	nnonce int
 | |
| }
 | |
| 
 | |
| func newACMEServer() *acmeServer {
 | |
| 	return &acmeServer{handler: make(map[string]http.HandlerFunc)}
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) handle(path string, f func(http.ResponseWriter, *http.Request)) {
 | |
| 	s.handler[path] = http.HandlerFunc(f)
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) start() {
 | |
| 	s.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 
 | |
| 		// Directory request.
 | |
| 		if r.URL.Path == "/" {
 | |
| 			fmt.Fprintf(w, `{
 | |
| 				"newNonce": %q,
 | |
| 				"newAccount": %q,
 | |
| 				"newOrder": %q,
 | |
| 				"newAuthz": %q,
 | |
| 				"revokeCert": %q,
 | |
| 				"keyChange": %q,
 | |
| 				"meta": {"termsOfService": %q}
 | |
| 				}`,
 | |
| 				s.url("/acme/new-nonce"),
 | |
| 				s.url("/acme/new-account"),
 | |
| 				s.url("/acme/new-order"),
 | |
| 				s.url("/acme/new-authz"),
 | |
| 				s.url("/acme/revoke-cert"),
 | |
| 				s.url("/acme/key-change"),
 | |
| 				s.url("/terms"),
 | |
| 			)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// All other responses contain a nonce value unconditionally.
 | |
| 		w.Header().Set("Replay-Nonce", s.nonce())
 | |
| 		if r.URL.Path == "/acme/new-nonce" {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		h := s.handler[r.URL.Path]
 | |
| 		if h == nil {
 | |
| 			w.WriteHeader(http.StatusBadRequest)
 | |
| 			fmt.Fprintf(w, "Unhandled %s", r.URL.Path)
 | |
| 			return
 | |
| 		}
 | |
| 		h.ServeHTTP(w, r)
 | |
| 	}))
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) close() {
 | |
| 	s.ts.Close()
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) url(path string) string {
 | |
| 	return s.ts.URL + path
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) nonce() string {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	s.nnonce++
 | |
| 	return fmt.Sprintf("nonce%d", s.nnonce)
 | |
| }
 | |
| 
 | |
| func (s *acmeServer) error(w http.ResponseWriter, e *wireError) {
 | |
| 	w.WriteHeader(e.Status)
 | |
| 	json.NewEncoder(w).Encode(e)
 | |
| }
 | |
| 
 | |
| func TestRFC_Register(t *testing.T) {
 | |
| 	const email = "mailto:user@example.org"
 | |
| 
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusCreated) // 201 means new account created
 | |
| 		fmt.Fprintf(w, `{
 | |
| 			"status": "valid",
 | |
| 			"contact": [%q],
 | |
| 			"orders": %q
 | |
| 		}`, email, s.url("/accounts/1/orders"))
 | |
| 
 | |
| 		b, _ := io.ReadAll(r.Body) // check err later in decodeJWSxxx
 | |
| 		head, err := decodeJWSHead(bytes.NewReader(b))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("decodeJWSHead: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		if len(head.JWK) == 0 {
 | |
| 			t.Error("head.JWK is empty")
 | |
| 		}
 | |
| 
 | |
| 		var req struct{ Contact []string }
 | |
| 		decodeJWSRequest(t, &req, bytes.NewReader(b))
 | |
| 		if len(req.Contact) != 1 || req.Contact[0] != email {
 | |
| 			t.Errorf("req.Contact = %q; want [%q]", req.Contact, email)
 | |
| 		}
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	cl := &Client{
 | |
| 		Key:          testKeyEC,
 | |
| 		DirectoryURL: s.url("/"),
 | |
| 	}
 | |
| 
 | |
| 	var didPrompt bool
 | |
| 	a := &Account{Contact: []string{email}}
 | |
| 	acct, err := cl.Register(ctx, a, func(tos string) bool {
 | |
| 		didPrompt = true
 | |
| 		terms := s.url("/terms")
 | |
| 		if tos != terms {
 | |
| 			t.Errorf("tos = %q; want %q", tos, terms)
 | |
| 		}
 | |
| 		return true
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	okAccount := &Account{
 | |
| 		URI:       s.url("/accounts/1"),
 | |
| 		Status:    StatusValid,
 | |
| 		Contact:   []string{email},
 | |
| 		OrdersURL: s.url("/accounts/1/orders"),
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(acct, okAccount) {
 | |
| 		t.Errorf("acct = %+v; want %+v", acct, okAccount)
 | |
| 	}
 | |
| 	if !didPrompt {
 | |
| 		t.Error("tos prompt wasn't called")
 | |
| 	}
 | |
| 	if v := cl.accountKID(ctx); v != KeyID(okAccount.URI) {
 | |
| 		t.Errorf("account kid = %q; want %q", v, okAccount.URI)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_RegisterExternalAccountBinding(t *testing.T) {
 | |
| 	eab := &ExternalAccountBinding{
 | |
| 		KID: "kid-1",
 | |
| 		Key: []byte("secret"),
 | |
| 	}
 | |
| 
 | |
| 	type protected struct {
 | |
| 		Algorithm string `json:"alg"`
 | |
| 		KID       string `json:"kid"`
 | |
| 		URL       string `json:"url"`
 | |
| 	}
 | |
| 	const email = "mailto:user@example.org"
 | |
| 
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		if r.Method != "POST" {
 | |
| 			t.Errorf("r.Method = %q; want POST", r.Method)
 | |
| 		}
 | |
| 
 | |
| 		var j struct {
 | |
| 			Protected              string
 | |
| 			Contact                []string
 | |
| 			TermsOfServiceAgreed   bool
 | |
| 			ExternalaccountBinding struct {
 | |
| 				Protected string
 | |
| 				Payload   string
 | |
| 				Signature string
 | |
| 			}
 | |
| 		}
 | |
| 		decodeJWSRequest(t, &j, r.Body)
 | |
| 		protData, err := base64.RawURLEncoding.DecodeString(j.ExternalaccountBinding.Protected)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		var prot protected
 | |
| 		err = json.Unmarshal(protData, &prot)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if !reflect.DeepEqual(j.Contact, []string{email}) {
 | |
| 			t.Errorf("j.Contact = %v; want %v", j.Contact, []string{email})
 | |
| 		}
 | |
| 		if !j.TermsOfServiceAgreed {
 | |
| 			t.Error("j.TermsOfServiceAgreed = false; want true")
 | |
| 		}
 | |
| 
 | |
| 		// Ensure same KID.
 | |
| 		if prot.KID != eab.KID {
 | |
| 			t.Errorf("j.ExternalAccountBinding.KID = %s; want %s", prot.KID, eab.KID)
 | |
| 		}
 | |
| 		// Ensure expected Algorithm.
 | |
| 		if prot.Algorithm != "HS256" {
 | |
| 			t.Errorf("j.ExternalAccountBinding.Alg = %s; want %s",
 | |
| 				prot.Algorithm, "HS256")
 | |
| 		}
 | |
| 
 | |
| 		// Ensure same URL as outer JWS.
 | |
| 		url := fmt.Sprintf("http://%s/acme/new-account", r.Host)
 | |
| 		if prot.URL != url {
 | |
| 			t.Errorf("j.ExternalAccountBinding.URL = %s; want %s",
 | |
| 				prot.URL, url)
 | |
| 		}
 | |
| 
 | |
| 		// Ensure payload is base64URL encoded string of JWK in outer JWS
 | |
| 		jwk, err := jwkEncode(testKeyEC.Public())
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		decodedPayload, err := base64.RawURLEncoding.DecodeString(j.ExternalaccountBinding.Payload)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		if jwk != string(decodedPayload) {
 | |
| 			t.Errorf("j.ExternalAccountBinding.Payload = %s; want %s", decodedPayload, jwk)
 | |
| 		}
 | |
| 
 | |
| 		// Check signature on inner external account binding JWS
 | |
| 		hmac := hmac.New(sha256.New, []byte("secret"))
 | |
| 		_, err = hmac.Write([]byte(j.ExternalaccountBinding.Protected + "." + j.ExternalaccountBinding.Payload))
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		mac := hmac.Sum(nil)
 | |
| 		encodedMAC := base64.RawURLEncoding.EncodeToString(mac)
 | |
| 
 | |
| 		if !bytes.Equal([]byte(encodedMAC), []byte(j.ExternalaccountBinding.Signature)) {
 | |
| 			t.Errorf("j.ExternalAccountBinding.Signature = %v; want %v",
 | |
| 				[]byte(j.ExternalaccountBinding.Signature), encodedMAC)
 | |
| 		}
 | |
| 
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusCreated)
 | |
| 		b, _ := json.Marshal([]string{email})
 | |
| 		fmt.Fprintf(w, `{"status":"valid","orders":"%s","contact":%s}`, s.url("/accounts/1/orders"), b)
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	cl := &Client{
 | |
| 		Key:          testKeyEC,
 | |
| 		DirectoryURL: s.url("/"),
 | |
| 	}
 | |
| 
 | |
| 	var didPrompt bool
 | |
| 	a := &Account{Contact: []string{email}, ExternalAccountBinding: eab}
 | |
| 	acct, err := cl.Register(ctx, a, func(tos string) bool {
 | |
| 		didPrompt = true
 | |
| 		terms := s.url("/terms")
 | |
| 		if tos != terms {
 | |
| 			t.Errorf("tos = %q; want %q", tos, terms)
 | |
| 		}
 | |
| 		return true
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	okAccount := &Account{
 | |
| 		URI:       s.url("/accounts/1"),
 | |
| 		Status:    StatusValid,
 | |
| 		Contact:   []string{email},
 | |
| 		OrdersURL: s.url("/accounts/1/orders"),
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(acct, okAccount) {
 | |
| 		t.Errorf("acct = %+v; want %+v", acct, okAccount)
 | |
| 	}
 | |
| 	if !didPrompt {
 | |
| 		t.Error("tos prompt wasn't called")
 | |
| 	}
 | |
| 	if v := cl.accountKID(ctx); v != KeyID(okAccount.URI) {
 | |
| 		t.Errorf("account kid = %q; want %q", v, okAccount.URI)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_RegisterExisting(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK) // 200 means account already exists
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	_, err := cl.Register(context.Background(), &Account{}, AcceptTOS)
 | |
| 	if err != ErrAccountAlreadyExists {
 | |
| 		t.Errorf("err = %v; want %v", err, ErrAccountAlreadyExists)
 | |
| 	}
 | |
| 	kid := KeyID(s.url("/accounts/1"))
 | |
| 	if v := cl.accountKID(context.Background()); v != kid {
 | |
| 		t.Errorf("account kid = %q; want %q", v, kid)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_UpdateReg(t *testing.T) {
 | |
| 	const email = "mailto:user@example.org"
 | |
| 
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	var didUpdate bool
 | |
| 	s.handle("/accounts/1", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		didUpdate = true
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 
 | |
| 		b, _ := io.ReadAll(r.Body) // check err later in decodeJWSxxx
 | |
| 		head, err := decodeJWSHead(bytes.NewReader(b))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("decodeJWSHead: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		if len(head.JWK) != 0 {
 | |
| 			t.Error("head.JWK is non-zero")
 | |
| 		}
 | |
| 		kid := s.url("/accounts/1")
 | |
| 		if head.KID != kid {
 | |
| 			t.Errorf("head.KID = %q; want %q", head.KID, kid)
 | |
| 		}
 | |
| 
 | |
| 		var req struct{ Contact []string }
 | |
| 		decodeJWSRequest(t, &req, bytes.NewReader(b))
 | |
| 		if len(req.Contact) != 1 || req.Contact[0] != email {
 | |
| 			t.Errorf("req.Contact = %q; want [%q]", req.Contact, email)
 | |
| 		}
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	_, err := cl.UpdateReg(context.Background(), &Account{Contact: []string{email}})
 | |
| 	if err != nil {
 | |
| 		t.Error(err)
 | |
| 	}
 | |
| 	if !didUpdate {
 | |
| 		t.Error("UpdateReg didn't update the account")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_GetReg(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 
 | |
| 		head, err := decodeJWSHead(r.Body)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("decodeJWSHead: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		if len(head.JWK) == 0 {
 | |
| 			t.Error("head.JWK is empty")
 | |
| 		}
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	acct, err := cl.GetReg(context.Background(), "")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	okAccount := &Account{
 | |
| 		URI:    s.url("/accounts/1"),
 | |
| 		Status: StatusValid,
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(acct, okAccount) {
 | |
| 		t.Errorf("acct = %+v; want %+v", acct, okAccount)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_GetRegNoAccount(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		s.error(w, &wireError{
 | |
| 			Status: http.StatusBadRequest,
 | |
| 			Type:   "urn:ietf:params:acme:error:accountDoesNotExist",
 | |
| 		})
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	if _, err := cl.GetReg(context.Background(), ""); err != ErrNoAccount {
 | |
| 		t.Errorf("err = %v; want %v", err, ErrNoAccount)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_GetRegOtherError(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.WriteHeader(http.StatusBadRequest)
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	if _, err := cl.GetReg(context.Background(), ""); err == nil || err == ErrNoAccount {
 | |
| 		t.Errorf("GetReg: %v; want any other non-nil err", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_AccountKeyRollover(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	s.handle("/acme/key-change", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	if err := cl.AccountKeyRollover(context.Background(), testKeyEC384); err != nil {
 | |
| 		t.Errorf("AccountKeyRollover: %v, wanted no error", err)
 | |
| 	} else if cl.Key != testKeyEC384 {
 | |
| 		t.Error("AccountKeyRollover did not rotate the client key")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_DeactivateReg(t *testing.T) {
 | |
| 	const email = "mailto:user@example.org"
 | |
| 	curStatus := StatusValid
 | |
| 
 | |
| 	type account struct {
 | |
| 		Status    string   `json:"status"`
 | |
| 		Contact   []string `json:"contact"`
 | |
| 		AcceptTOS bool     `json:"termsOfServiceAgreed"`
 | |
| 		Orders    string   `json:"orders"`
 | |
| 	}
 | |
| 
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK) // 200 means existing account
 | |
| 		json.NewEncoder(w).Encode(account{
 | |
| 			Status:    curStatus,
 | |
| 			Contact:   []string{email},
 | |
| 			AcceptTOS: true,
 | |
| 			Orders:    s.url("/accounts/1/orders"),
 | |
| 		})
 | |
| 
 | |
| 		b, _ := io.ReadAll(r.Body) // check err later in decodeJWSxxx
 | |
| 		head, err := decodeJWSHead(bytes.NewReader(b))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("decodeJWSHead: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		if len(head.JWK) == 0 {
 | |
| 			t.Error("head.JWK is empty")
 | |
| 		}
 | |
| 
 | |
| 		var req struct {
 | |
| 			Status       string   `json:"status"`
 | |
| 			Contact      []string `json:"contact"`
 | |
| 			AcceptTOS    bool     `json:"termsOfServiceAgreed"`
 | |
| 			OnlyExisting bool     `json:"onlyReturnExisting"`
 | |
| 		}
 | |
| 		decodeJWSRequest(t, &req, bytes.NewReader(b))
 | |
| 		if !req.OnlyExisting {
 | |
| 			t.Errorf("req.OnlyReturnExisting = %t; want = %t", req.OnlyExisting, true)
 | |
| 		}
 | |
| 	})
 | |
| 	s.handle("/accounts/1", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		if curStatus == StatusValid {
 | |
| 			curStatus = StatusDeactivated
 | |
| 			w.WriteHeader(http.StatusOK)
 | |
| 		} else {
 | |
| 			s.error(w, &wireError{
 | |
| 				Status: http.StatusUnauthorized,
 | |
| 				Type:   "urn:ietf:params:acme:error:unauthorized",
 | |
| 			})
 | |
| 		}
 | |
| 		var req account
 | |
| 		b, _ := io.ReadAll(r.Body) // check err later in decodeJWSxxx
 | |
| 		head, err := decodeJWSHead(bytes.NewReader(b))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("decodeJWSHead: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		if len(head.JWK) != 0 {
 | |
| 			t.Error("head.JWK is not empty")
 | |
| 		}
 | |
| 		if !strings.HasSuffix(head.KID, "/accounts/1") {
 | |
| 			t.Errorf("head.KID = %q; want suffix /accounts/1", head.KID)
 | |
| 		}
 | |
| 
 | |
| 		decodeJWSRequest(t, &req, bytes.NewReader(b))
 | |
| 		if req.Status != StatusDeactivated {
 | |
| 			t.Errorf("req.Status = %q; want = %q", req.Status, StatusDeactivated)
 | |
| 		}
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	if err := cl.DeactivateReg(context.Background()); err != nil {
 | |
| 		t.Errorf("DeactivateReg: %v, wanted no error", err)
 | |
| 	}
 | |
| 	if err := cl.DeactivateReg(context.Background()); err == nil {
 | |
| 		t.Errorf("DeactivateReg: %v, wanted error for unauthorized", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRF_DeactivateRegNoAccount(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		s.error(w, &wireError{
 | |
| 			Status: http.StatusBadRequest,
 | |
| 			Type:   "urn:ietf:params:acme:error:accountDoesNotExist",
 | |
| 		})
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	if err := cl.DeactivateReg(context.Background()); !errors.Is(err, ErrNoAccount) {
 | |
| 		t.Errorf("DeactivateReg: %v, wanted ErrNoAccount", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_AuthorizeOrder(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	s.handle("/acme/new-order", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/orders/1"))
 | |
| 		w.WriteHeader(http.StatusCreated)
 | |
| 		fmt.Fprintf(w, `{
 | |
| 			"status": "pending",
 | |
| 			"expires": "2019-09-01T00:00:00Z",
 | |
| 			"notBefore": "2019-08-31T00:00:00Z",
 | |
| 			"notAfter": "2019-09-02T00:00:00Z",
 | |
| 			"identifiers": [{"type":"dns", "value":"example.org"}],
 | |
| 			"authorizations": [%q]
 | |
| 		}`, s.url("/authz/1"))
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	prevCertDER, _ := pem.Decode([]byte(leafPEM))
 | |
| 	prevCert, err := x509.ParseCertificate(prevCertDER.Bytes)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	o, err := cl.AuthorizeOrder(context.Background(), DomainIDs("example.org"),
 | |
| 		WithOrderNotBefore(time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC)),
 | |
| 		WithOrderNotAfter(time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC)),
 | |
| 		WithOrderReplacesCert(prevCert),
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	okOrder := &Order{
 | |
| 		URI:         s.url("/orders/1"),
 | |
| 		Status:      StatusPending,
 | |
| 		Expires:     time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC),
 | |
| 		NotBefore:   time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC),
 | |
| 		NotAfter:    time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC),
 | |
| 		Identifiers: []AuthzID{AuthzID{Type: "dns", Value: "example.org"}},
 | |
| 		AuthzURLs:   []string{s.url("/authz/1")},
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(o, okOrder) {
 | |
| 		t.Errorf("AuthorizeOrder = %+v; want %+v", o, okOrder)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_GetOrder(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/orders/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{
 | |
| 			"status": "invalid",
 | |
| 			"expires": "2019-09-01T00:00:00Z",
 | |
| 			"notBefore": "2019-08-31T00:00:00Z",
 | |
| 			"notAfter": "2019-09-02T00:00:00Z",
 | |
| 			"identifiers": [{"type":"dns", "value":"example.org"}],
 | |
| 			"authorizations": ["/authz/1"],
 | |
| 			"finalize": "/orders/1/fin",
 | |
| 			"certificate": "/orders/1/cert",
 | |
| 			"error": {"type": "badRequest"}
 | |
| 		}`))
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	o, err := cl.GetOrder(context.Background(), s.url("/orders/1"))
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	okOrder := &Order{
 | |
| 		URI:         s.url("/orders/1"),
 | |
| 		Status:      StatusInvalid,
 | |
| 		Expires:     time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC),
 | |
| 		NotBefore:   time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC),
 | |
| 		NotAfter:    time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC),
 | |
| 		Identifiers: []AuthzID{AuthzID{Type: "dns", Value: "example.org"}},
 | |
| 		AuthzURLs:   []string{"/authz/1"},
 | |
| 		FinalizeURL: "/orders/1/fin",
 | |
| 		CertURL:     "/orders/1/cert",
 | |
| 		Error:       &Error{ProblemType: "badRequest"},
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(o, okOrder) {
 | |
| 		t.Errorf("GetOrder = %+v\nwant %+v", o, okOrder)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_WaitOrder(t *testing.T) {
 | |
| 	for _, st := range []string{StatusReady, StatusValid} {
 | |
| 		t.Run(st, func(t *testing.T) {
 | |
| 			testWaitOrderStatus(t, st)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func testWaitOrderStatus(t *testing.T, okStatus string) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	var count int
 | |
| 	s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/orders/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		s := StatusPending
 | |
| 		if count > 0 {
 | |
| 			s = okStatus
 | |
| 		}
 | |
| 		fmt.Fprintf(w, `{"status": %q}`, s)
 | |
| 		count++
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	order, err := cl.WaitOrder(context.Background(), s.url("/orders/1"))
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("WaitOrder: %v", err)
 | |
| 	}
 | |
| 	if order.Status != okStatus {
 | |
| 		t.Errorf("order.Status = %q; want %q", order.Status, okStatus)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_WaitOrderError(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	var count int
 | |
| 	s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/orders/1"))
 | |
| 		w.WriteHeader(http.StatusOK)
 | |
| 		s := StatusPending
 | |
| 		if count > 0 {
 | |
| 			s = StatusInvalid
 | |
| 		}
 | |
| 		fmt.Fprintf(w, `{"status": %q}`, s)
 | |
| 		count++
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	_, err := cl.WaitOrder(context.Background(), s.url("/orders/1"))
 | |
| 	if err == nil {
 | |
| 		t.Fatal("WaitOrder returned nil error")
 | |
| 	}
 | |
| 	e, ok := err.(*OrderError)
 | |
| 	if !ok {
 | |
| 		t.Fatalf("err = %v (%T); want OrderError", err, err)
 | |
| 	}
 | |
| 	if e.OrderURL != s.url("/orders/1") {
 | |
| 		t.Errorf("e.OrderURL = %q; want %q", e.OrderURL, s.url("/orders/1"))
 | |
| 	}
 | |
| 	if e.Status != StatusInvalid {
 | |
| 		t.Errorf("e.Status = %q; want %q", e.Status, StatusInvalid)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_CreateOrderCert(t *testing.T) {
 | |
| 	q := &x509.CertificateRequest{
 | |
| 		Subject: pkix.Name{CommonName: "example.org"},
 | |
| 	}
 | |
| 	csr, err := x509.CreateCertificateRequest(rand.Reader, q, testKeyEC)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	tmpl := &x509.Certificate{SerialNumber: big.NewInt(1)}
 | |
| 	leaf, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &testKeyEC.PublicKey, testKeyEC)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/accounts/1"))
 | |
| 		w.Write([]byte(`{"status": "valid"}`))
 | |
| 	})
 | |
| 	var count int
 | |
| 	s.handle("/pleaseissue", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Location", s.url("/pleaseissue"))
 | |
| 		st := StatusProcessing
 | |
| 		if count > 0 {
 | |
| 			st = StatusValid
 | |
| 		}
 | |
| 		fmt.Fprintf(w, `{"status":%q, "certificate":%q}`, st, s.url("/crt"))
 | |
| 		count++
 | |
| 	})
 | |
| 	s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Content-Type", "application/pem-certificate-chain")
 | |
| 		pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: leaf})
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	cert, curl, err := cl.CreateOrderCert(ctx, s.url("/pleaseissue"), csr, true)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("CreateOrderCert: %v", err)
 | |
| 	}
 | |
| 	if _, err := x509.ParseCertificate(cert[0]); err != nil {
 | |
| 		t.Errorf("ParseCertificate: %v", err)
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(cert[0], leaf) {
 | |
| 		t.Errorf("cert and leaf bytes don't match")
 | |
| 	}
 | |
| 	if u := s.url("/crt"); curl != u {
 | |
| 		t.Errorf("curl = %q; want %q", curl, u)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_AlreadyRevokedCert(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/acme/revoke-cert", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		s.error(w, &wireError{
 | |
| 			Status: http.StatusBadRequest,
 | |
| 			Type:   "urn:ietf:params:acme:error:alreadyRevoked",
 | |
| 		})
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	err := cl.RevokeCert(context.Background(), testKeyEC, []byte{0}, CRLReasonUnspecified)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("RevokeCert: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRFC_ListCertAlternates(t *testing.T) {
 | |
| 	s := newACMEServer()
 | |
| 	s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Content-Type", "application/pem-certificate-chain")
 | |
| 		w.Header().Add("Link", `<https://example.com/crt/2>;rel="alternate"`)
 | |
| 		w.Header().Add("Link", `<https://example.com/crt/3>; rel="alternate"`)
 | |
| 		w.Header().Add("Link", `<https://example.com/acme>; rel="index"`)
 | |
| 	})
 | |
| 	s.handle("/crt2", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Content-Type", "application/pem-certificate-chain")
 | |
| 	})
 | |
| 	s.start()
 | |
| 	defer s.close()
 | |
| 
 | |
| 	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
 | |
| 	crts, err := cl.ListCertAlternates(context.Background(), s.url("/crt"))
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("ListCertAlternates: %v", err)
 | |
| 	}
 | |
| 	want := []string{"https://example.com/crt/2", "https://example.com/crt/3"}
 | |
| 	if !reflect.DeepEqual(crts, want) {
 | |
| 		t.Errorf("ListCertAlternates(/crt): %v; want %v", crts, want)
 | |
| 	}
 | |
| 	crts, err = cl.ListCertAlternates(context.Background(), s.url("/crt2"))
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("ListCertAlternates: %v", err)
 | |
| 	}
 | |
| 	if crts != nil {
 | |
| 		t.Errorf("ListCertAlternates(/crt2): %v; want nil", crts)
 | |
| 	}
 | |
| }
 |