| // 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" |
| "io" |
| "math/big" |
| "net/http" |
| "net/http/httptest" |
| "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 |
| } |
| } |
| |
| // 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.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(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() |
| type res struct { |
| authz *Authorization |
| err error |
| } |
| ch := make(chan res, 1) |
| go func() { |
| client := &Client{ |
| Key: testKey, |
| DirectoryURL: ts.URL, |
| dir: &Directory{}, |
| KID: "some-key-id", // set to avoid lookup attempt |
| } |
| 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.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") |
| } |
| } |
| |
| 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, 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 := 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) |
| } |
| } |