mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Pull in https://github.com/tailscale/golang-x-crypto/pull/16 Updates #15542 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
		
			
				
	
	
		
			971 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			971 lines
		
	
	
		
			28 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"
 | |
| 	"encoding/pem"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"math/big"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"net/url"
 | |
| 	"reflect"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // newTestClient creates a client with a non-nil Directory so that it skips
 | |
| // the discovery which is otherwise done on the first call of almost every
 | |
| // exported method.
 | |
| func newTestClient() *Client {
 | |
| 	return &Client{
 | |
| 		Key: testKeyEC,
 | |
| 		dir: &Directory{}, // skip discovery
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // newTestClientWithMockDirectory creates a client with a non-nil Directory
 | |
| // that contains mock field values.
 | |
| func newTestClientWithMockDirectory() *Client {
 | |
| 	return &Client{
 | |
| 		Key: testKeyEC,
 | |
| 		dir: &Directory{
 | |
| 			RenewalInfoURL: "https://example.com/acme/renewal-info/",
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided
 | |
| // interface.
 | |
| func decodeJWSRequest(t *testing.T, v interface{}, r io.Reader) {
 | |
| 	// Decode request
 | |
| 	var req struct{ Payload string }
 | |
| 	if err := json.NewDecoder(r).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
 | |
| 	URL   string            `json:"url"`
 | |
| 	KID   string            `json:"kid"`
 | |
| 	JWK   map[string]string `json:"jwk"`
 | |
| }
 | |
| 
 | |
| func decodeJWSHead(r io.Reader) (*jwsHead, error) {
 | |
| 	var req struct{ Protected string }
 | |
| 	if err := json.NewDecoder(r).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 TestRegisterWithoutKey(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
 | |
| 		}
 | |
| 		w.WriteHeader(http.StatusCreated)
 | |
| 		fmt.Fprint(w, `{}`)
 | |
| 	}))
 | |
| 	defer ts.Close()
 | |
| 	// First verify that using a complete client results in success.
 | |
| 	c := Client{
 | |
| 		Key:          testKeyEC,
 | |
| 		DirectoryURL: ts.URL,
 | |
| 		dir:          &Directory{RegURL: ts.URL},
 | |
| 	}
 | |
| 	if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err != nil {
 | |
| 		t.Fatalf("c.Register() = %v; want success with a complete test client", err)
 | |
| 	}
 | |
| 	c.Key = nil
 | |
| 	if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err == nil {
 | |
| 		t.Error("c.Register() from client without key succeeded, wanted error")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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.Body)
 | |
| 
 | |
| 				// 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 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("invalid status with error returns the authorization error", func(t *testing.T) {
 | |
| 		_, err := runWaitAuthorization(context.Background(), t, func(w http.ResponseWriter, r *http.Request) {
 | |
| 			fmt.Fprintf(w, `{
 | |
| 				"type": "dns-01",
 | |
| 				"status": "invalid",
 | |
| 				"error": {
 | |
| 				  "type": "urn:ietf:params:acme:error:caa",
 | |
| 				  "detail": "CAA record for <domain> prevents issuance",
 | |
| 				  "status": 403
 | |
| 				},
 | |
| 				"url": "https://acme-v02.api.letsencrypt.org/acme/chall-v3/xxx/xxx",
 | |
| 				"token": "xxx",
 | |
| 				"validationRecord": [
 | |
| 				  {
 | |
| 					"hostname": "<domain>"
 | |
| 				  }
 | |
| 				]
 | |
| 			  }`)
 | |
| 		})
 | |
| 
 | |
| 		want := &AuthorizationError{
 | |
| 			Errors: []error{
 | |
| 				(&wireError{
 | |
| 					Status: 403,
 | |
| 					Type:   "urn:ietf:params:acme:error:caa",
 | |
| 					Detail: "CAA record for <domain> prevents issuance",
 | |
| 				}).error(nil),
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		_, ok := err.(*AuthorizationError)
 | |
| 		if !ok {
 | |
| 			t.Errorf("err is %T; want non-nil *AuthorizationError", err)
 | |
| 		}
 | |
| 
 | |
| 		if err.Error() != want.Error() {
 | |
| 			t.Errorf("err is %v; want %v", err, want)
 | |
| 		}
 | |
| 	})
 | |
| 	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.WithCancel(context.Background())
 | |
| 		defer cancel()
 | |
| 		_, err := runWaitAuthorization(ctx, t, func(w http.ResponseWriter, r *http.Request) {
 | |
| 			w.Header().Set("Retry-After", "60")
 | |
| 			fmt.Fprintf(w, `{"status":"pending"}`)
 | |
| 			time.AfterFunc(1*time.Millisecond, cancel)
 | |
| 		})
 | |
| 		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(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		w.Header().Set("Replay-Nonce", fmt.Sprintf("bad-test-nonce-%v", time.Now().UnixNano()))
 | |
| 		h(w, r)
 | |
| 	}))
 | |
| 	defer ts.Close()
 | |
| 
 | |
| 	client := &Client{
 | |
| 		Key:          testKey,
 | |
| 		DirectoryURL: ts.URL,
 | |
| 		dir:          &Directory{},
 | |
| 		KID:          "some-key-id", // set to avoid lookup attempt
 | |
| 	}
 | |
| 	return client.WaitAuthorization(ctx, ts.URL)
 | |
| }
 | |
| 
 | |
| 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.Body)
 | |
| 			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,
 | |
| 		DirectoryURL: ts.URL,       // don't dial outside of localhost
 | |
| 		dir:          &Directory{}, // don't do discovery
 | |
| 	}
 | |
| 	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 TestFetchCertCancel(t *testing.T) {
 | |
| 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		<-r.Context().Done()
 | |
| 		w.Header().Set("Retry-After", "0")
 | |
| 		w.WriteHeader(http.StatusBadRequest)
 | |
| 	}))
 | |
| 	defer ts.Close()
 | |
| 	ctx, cancel := context.WithCancel(context.Background())
 | |
| 	done := make(chan struct{})
 | |
| 	var err error
 | |
| 	go func() {
 | |
| 		cl := newTestClient()
 | |
| 		_, err = cl.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()
 | |
| 	cl := newTestClient()
 | |
| 	_, err := cl.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()
 | |
| 	cl := newTestClient()
 | |
| 	_, err := cl.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()
 | |
| 	cl := newTestClient()
 | |
| 	_, err := cl.FetchCert(context.Background(), ts.URL, false)
 | |
| 	if err == nil {
 | |
| 		t.Errorf("err is nil")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	leafPEM = `-----BEGIN CERTIFICATE-----
 | |
| MIIEizCCAvOgAwIBAgIRAITApw7R8HSs7GU7cj8dEyUwDQYJKoZIhvcNAQELBQAw
 | |
| gYUxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEtMCsGA1UECwwkY3Bh
 | |
| bG1lckBwdW1wa2luLmxvY2FsIChDaHJpcyBQYWxtZXIpMTQwMgYDVQQDDCtta2Nl
 | |
| cnQgY3BhbG1lckBwdW1wa2luLmxvY2FsIChDaHJpcyBQYWxtZXIpMB4XDTIzMDcx
 | |
| MjE4MjIxNloXDTI1MTAxMjE4MjIxNlowWDEnMCUGA1UEChMebWtjZXJ0IGRldmVs
 | |
| b3BtZW50IGNlcnRpZmljYXRlMS0wKwYDVQQLDCRjcGFsbWVyQHB1bXBraW4ubG9j
 | |
| YWwgKENocmlzIFBhbG1lcikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
 | |
| AQDNDO8P4MI9jaqVcPtF8C4GgHnTP5EK3U9fgyGApKGxTpicMQkA6z4GXwUP/Fvq
 | |
| 7RuCU9Wg7By5VetKIHF7FxkxWkUMrssr7mV8v6mRCh/a5GqDs14aj5ucjLQAJV74
 | |
| tLAdrCiijQ1fkPWc82fob+LkfKWGCWw7Cxf6ZtEyC8jz/DnfQXUvOiZS729ndGF7
 | |
| FobKRfIoirD+GI2NTYIp3LAUFSPR6HXTe7HAg8J81VoUKli8z504+FebfMmHePm/
 | |
| zIfiI0njAj4czOlZD56/oLsV0WRUizFjafHHUFz1HVdfFw8Qf9IOOTydYOe8M5i0
 | |
| lVbVO5G+HP+JDn3cr9MT41B9AgMBAAGjgaEwgZ4wDgYDVR0PAQH/BAQDAgWgMBMG
 | |
| A1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFPpL4Q0O7Z7voTkjn2rrFCsf
 | |
| s8TbMFYGA1UdEQRPME2CC2V4YW1wbGUuY29tgg0qLmV4YW1wbGUuY29tggxleGFt
 | |
| cGxlLnRlc3SCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkq
 | |
| hkiG9w0BAQsFAAOCAYEAMlOb7lrHuSxwcnAu7mL1ysTGqKn1d2TyDJAN5W8YFY+4
 | |
| XLpofNkK2UzZ0t9LQRnuFUcjmfqmfplh5lpC7pKmtL4G5Qcdc+BczQWcopbxd728
 | |
| sht9BKRkH+Bo1I+1WayKKNXW+5bsMv4CH641zxaMBlzjEnPvwKkNaGLMH3x5lIeX
 | |
| GGgkKNXwVtINmyV+lTNVtu2IlHprxJGCjRfEuX7mEv6uRnqz3Wif+vgyh3MBgM/1
 | |
| dUOsTBNH4a6Jl/9VPSOfRdQOStqIlwTa/J1bhTvivsYt1+eWjLnsQJLgZQqwKvYH
 | |
| BJ30gAk1oNnuSkx9dHbx4mO+4mB9oIYUALXUYakb8JHTOnuMSj9qelVj5vjVxl9q
 | |
| KRitptU+kLYRA4HSgUXrhDIm4Q6D/w8/ascPqQ3HxPIDFLe+gTofEjqnnsnQB29L
 | |
| gWpI8l5/MtXAOMdW69eEovnADc2pgaiif0T+v9nNKBc5xfDZHnrnqIqVzQEwL5Qv
 | |
| niQI8IsWD5LcQ1Eg7kCq
 | |
| -----END CERTIFICATE-----`
 | |
| )
 | |
| 
 | |
| func TestGetRenewalURL(t *testing.T) {
 | |
| 	leaf, _ := pem.Decode([]byte(leafPEM))
 | |
| 
 | |
| 	parsedLeaf, err := x509.ParseCertificate(leaf.Bytes)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	client := newTestClientWithMockDirectory()
 | |
| 	urlString := client.getRenewalURL(parsedLeaf)
 | |
| 
 | |
| 	parsedURL, err := url.Parse(urlString)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if scheme := parsedURL.Scheme; scheme == "" {
 | |
| 		t.Fatalf("malformed URL scheme: %q from %q", scheme, urlString)
 | |
| 	}
 | |
| 	if host := parsedURL.Host; host == "" {
 | |
| 		t.Fatalf("malformed URL host: %q from %q", host, urlString)
 | |
| 	}
 | |
| 	if parsedURL.RawQuery != "" {
 | |
| 		t.Fatalf("malformed URL: should not have a query")
 | |
| 	}
 | |
| 	path := parsedURL.EscapedPath()
 | |
| 	slash := strings.LastIndex(path, "/")
 | |
| 	if slash == -1 {
 | |
| 		t.Fatalf("malformed URL path: %q from %q", path, urlString)
 | |
| 	}
 | |
| 	certID := path[slash+1:]
 | |
| 	if certID == "" {
 | |
| 		t.Fatalf("missing certificate identifier in URL path: %q from %q", path, urlString)
 | |
| 	}
 | |
| 	certIDParts := strings.Split(certID, ".")
 | |
| 	if len(certIDParts) != 2 {
 | |
| 		t.Fatalf("certificate identifier should consist of 2 base64-encoded values separated by a dot: %q from %q", certID, urlString)
 | |
| 	}
 | |
| 	if _, err := base64.RawURLEncoding.DecodeString(certIDParts[0]); err != nil {
 | |
| 		t.Fatalf("malformed AKI part in certificate identifier: %q from %q: %v", certIDParts[0], urlString, err)
 | |
| 	}
 | |
| 	if _, err := base64.RawURLEncoding.DecodeString(certIDParts[1]); err != nil {
 | |
| 		t.Fatalf("malformed Serial part in certificate identifier: %q from %q: %v", certIDParts[1], urlString, err)
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| func TestUnmarshalRenewalInfo(t *testing.T) {
 | |
| 	renewalInfoJSON := `{
 | |
|      "suggestedWindow": {
 | |
|        "start": "2021-01-03T00:00:00Z",
 | |
|        "end": "2021-01-07T00:00:00Z"
 | |
|      },
 | |
|      "explanationURL": "https://example.com/docs/example-mass-reissuance-event"
 | |
|    }`
 | |
| 	expectedStart := time.Date(2021, time.January, 3, 0, 0, 0, 0, time.UTC)
 | |
| 	expectedEnd := time.Date(2021, time.January, 7, 0, 0, 0, 0, time.UTC)
 | |
| 
 | |
| 	var info RenewalInfo
 | |
| 	if err := json.Unmarshal([]byte(renewalInfoJSON), &info); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if _, err := url.Parse(info.ExplanationURL); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if !info.SuggestedWindow.Start.Equal(expectedStart) {
 | |
| 		t.Fatalf("%v != %v", expectedStart, info.SuggestedWindow.Start)
 | |
| 	}
 | |
| 	if !info.SuggestedWindow.End.Equal(expectedEnd) {
 | |
| 		t.Fatalf("%v != %v", expectedEnd, info.SuggestedWindow.End)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 := newTestClient()
 | |
| 		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 := newTestClient()
 | |
| 	_, 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 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"
 | |
| 	)
 | |
| 
 | |
| 	tlscert, name, err := newTestClient().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"
 | |
| 	)
 | |
| 
 | |
| 	tlscert, name, err := newTestClient().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)
 | |
| 	}
 | |
| 
 | |
| 	tlscert, err := newTestClient().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 idPeACMEIdentifier.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, 1024)
 | |
| 	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 := newTestClient()
 | |
| 	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 := newTestClient()
 | |
| 	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"
 | |
| 
 | |
| 	val, err := newTestClient().DNS01ChallengeRecord("xxx")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if val != value {
 | |
| 		t.Errorf("val = %q; want %q", val, value)
 | |
| 	}
 | |
| }
 |