2015-06-13 15:00:18 +00:00
|
|
|
package acme
|
|
|
|
|
|
|
|
import (
|
2016-01-27 01:01:58 +00:00
|
|
|
"crypto"
|
2015-06-13 15:00:18 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
2015-09-26 20:59:16 +00:00
|
|
|
"encoding/json"
|
2018-10-29 16:35:49 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2016-01-08 07:04:50 +00:00
|
|
|
"net"
|
2015-09-26 20:59:16 +00:00
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2015-06-13 15:00:18 +00:00
|
|
|
"testing"
|
2017-02-19 03:48:45 +00:00
|
|
|
"time"
|
2018-09-15 17:16:35 +00:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
2018-10-29 16:35:49 +00:00
|
|
|
"gopkg.in/square/go-jose.v2"
|
2015-06-13 15:00:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestNewClient(t *testing.T) {
|
|
|
|
keyBits := 32 // small value keeps test fast
|
2016-01-27 01:01:58 +00:00
|
|
|
keyType := RSA2048
|
2015-06-13 15:00:18 +00:00
|
|
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not generate test key")
|
|
|
|
|
2015-06-13 15:00:18 +00:00
|
|
|
user := mockUser{
|
|
|
|
email: "test@test.com",
|
|
|
|
regres: new(RegistrationResource),
|
|
|
|
privatekey: key,
|
|
|
|
}
|
2015-09-26 20:59:16 +00:00
|
|
|
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2018-05-30 17:53:04 +00:00
|
|
|
data, _ := json.Marshal(directory{
|
|
|
|
NewNonceURL: "http://test",
|
|
|
|
NewAccountURL: "http://test",
|
|
|
|
NewOrderURL: "http://test",
|
|
|
|
RevokeCertURL: "http://test",
|
|
|
|
KeyChangeURL: "http://test",
|
|
|
|
})
|
2018-09-24 19:07:20 +00:00
|
|
|
|
|
|
|
_, err = w.Write(data)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2015-09-26 20:59:16 +00:00
|
|
|
}))
|
|
|
|
|
2016-01-27 01:01:58 +00:00
|
|
|
client, err := NewClient(ts.URL, user, keyType)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not create client")
|
2015-06-13 15:00:18 +00:00
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NotNil(t, client.jws, "client.jws")
|
|
|
|
assert.Equal(t, key, client.jws.privKey, "client.jws.privKey")
|
|
|
|
assert.Equal(t, keyType, client.keyType, "client.keyType")
|
|
|
|
assert.Len(t, client.solvers, 2, "solvers")
|
2015-12-05 21:01:08 +00:00
|
|
|
}
|
|
|
|
|
2015-12-27 17:56:44 +00:00
|
|
|
func TestClientOptPort(t *testing.T) {
|
2015-12-05 21:01:08 +00:00
|
|
|
keyBits := 32 // small value keeps test fast
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not generate test key")
|
|
|
|
|
2015-12-05 21:01:08 +00:00
|
|
|
user := mockUser{
|
|
|
|
email: "test@test.com",
|
|
|
|
regres: new(RegistrationResource),
|
|
|
|
privatekey: key,
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2018-05-30 17:53:04 +00:00
|
|
|
data, _ := json.Marshal(directory{
|
|
|
|
NewNonceURL: "http://test",
|
|
|
|
NewAccountURL: "http://test",
|
|
|
|
NewOrderURL: "http://test",
|
|
|
|
RevokeCertURL: "http://test",
|
|
|
|
KeyChangeURL: "http://test",
|
|
|
|
})
|
2018-09-24 19:07:20 +00:00
|
|
|
|
|
|
|
_, err = w.Write(data)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2015-12-05 21:01:08 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
optPort := "1234"
|
2016-01-08 07:04:50 +00:00
|
|
|
optHost := ""
|
2018-09-15 17:16:35 +00:00
|
|
|
|
2016-01-27 01:01:58 +00:00
|
|
|
client, err := NewClient(ts.URL, user, RSA2048)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not create client")
|
|
|
|
|
2018-09-24 19:07:20 +00:00
|
|
|
err = client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
|
|
|
require.NoError(t, err)
|
2015-06-13 15:00:18 +00:00
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
require.IsType(t, &httpChallenge{}, client.solvers[HTTP01])
|
|
|
|
httpSolver := client.solvers[HTTP01].(*httpChallenge)
|
|
|
|
|
|
|
|
assert.Equal(t, httpSolver.jws, client.jws, "Expected http-01 to have same jws as client")
|
|
|
|
|
|
|
|
httpProviderServer := httpSolver.provider.(*HTTPProviderServer)
|
|
|
|
assert.Equal(t, optPort, httpProviderServer.port, "port")
|
|
|
|
assert.Equal(t, optHost, httpProviderServer.iface, "iface")
|
2015-12-27 17:56:44 +00:00
|
|
|
|
2016-01-08 07:04:50 +00:00
|
|
|
// test setting different host
|
|
|
|
optHost = "127.0.0.1"
|
2018-09-24 19:07:20 +00:00
|
|
|
err = client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
|
|
|
require.NoError(t, err)
|
2016-01-08 07:04:50 +00:00
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
assert.Equal(t, optHost, httpSolver.provider.(*HTTPProviderServer).iface, "iface")
|
2015-06-13 15:00:18 +00:00
|
|
|
}
|
|
|
|
|
2017-02-19 03:48:45 +00:00
|
|
|
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
w.Header().Add("Replay-Nonce", "12345")
|
|
|
|
w.Header().Add("Retry-After", "0")
|
2018-05-30 17:53:04 +00:00
|
|
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
2017-02-19 03:48:45 +00:00
|
|
|
}))
|
|
|
|
defer ts.Close()
|
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
privKey, err := rsa.GenerateKey(rand.Reader, 512)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
2017-02-19 03:48:45 +00:00
|
|
|
ch := make(chan bool)
|
|
|
|
resultCh := make(chan bool)
|
|
|
|
go func() {
|
2018-09-24 19:07:20 +00:00
|
|
|
_, errN := j.Nonce()
|
|
|
|
if errN != nil {
|
|
|
|
t.Log(errN)
|
|
|
|
}
|
2017-02-19 03:48:45 +00:00
|
|
|
ch <- true
|
|
|
|
}()
|
|
|
|
go func() {
|
2018-09-24 19:07:20 +00:00
|
|
|
_, errN := j.Nonce()
|
|
|
|
if errN != nil {
|
|
|
|
t.Log(errN)
|
|
|
|
}
|
2017-02-19 03:48:45 +00:00
|
|
|
ch <- true
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
<-ch
|
|
|
|
<-ch
|
|
|
|
resultCh <- true
|
|
|
|
}()
|
|
|
|
select {
|
|
|
|
case <-resultCh:
|
|
|
|
case <-time.After(400 * time.Millisecond):
|
|
|
|
t.Fatal("JWS is probably holding a lock while making HTTP request")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-05 14:53:53 +00:00
|
|
|
func TestValidate(t *testing.T) {
|
|
|
|
var statuses []string
|
2018-09-15 17:16:35 +00:00
|
|
|
|
2018-10-29 16:35:49 +00:00
|
|
|
privKey, err := rsa.GenerateKey(rand.Reader, 512)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// validateNoBody reads the http.Request POST body, parses the JWS and
|
|
|
|
// validates it to read the body. If there is an error doing this, or if the
|
|
|
|
// JWS body is not the empty JSON payload "{}" an error is returned. We use
|
|
|
|
// this to verify challenge POSTs to the ts below do not send a JWS body.
|
|
|
|
validateNoBody := func(r *http.Request) error {
|
|
|
|
reqBody, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
jws, err := jose.ParseSigned(string(reqBody))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := jws.Verify(&jose.JSONWebKey{
|
|
|
|
Key: privKey.Public(),
|
|
|
|
Algorithm: "RSA",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if bodyStr := string(body); bodyStr != "{}" {
|
|
|
|
return fmt.Errorf(`expected JWS POST body "{}", got %q`, bodyStr)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-12-05 14:53:53 +00:00
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Minimal stub ACME server for validation.
|
|
|
|
w.Header().Add("Replay-Nonce", "12345")
|
2015-12-05 21:32:53 +00:00
|
|
|
w.Header().Add("Retry-After", "0")
|
2018-09-15 17:16:35 +00:00
|
|
|
|
2015-12-05 14:53:53 +00:00
|
|
|
switch r.Method {
|
2018-06-21 17:06:16 +00:00
|
|
|
case http.MethodHead:
|
|
|
|
case http.MethodPost:
|
2018-10-29 16:35:49 +00:00
|
|
|
if err := validateNoBody(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-12-05 14:53:53 +00:00
|
|
|
st := statuses[0]
|
|
|
|
statuses = statuses[1:]
|
2018-05-30 17:53:04 +00:00
|
|
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
2015-12-05 14:53:53 +00:00
|
|
|
|
2018-06-21 17:06:16 +00:00
|
|
|
case http.MethodGet:
|
2015-12-05 14:53:53 +00:00
|
|
|
st := statuses[0]
|
|
|
|
statuses = statuses[1:]
|
2018-05-30 17:53:04 +00:00
|
|
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
2015-12-05 14:53:53 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
defer ts.Close()
|
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
2015-12-05 14:53:53 +00:00
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
testCases := []struct {
|
2015-12-05 14:53:53 +00:00
|
|
|
name string
|
|
|
|
statuses []string
|
|
|
|
want string
|
|
|
|
}{
|
2018-09-15 17:16:35 +00:00
|
|
|
{
|
|
|
|
name: "POST-unexpected",
|
|
|
|
statuses: []string{"weird"},
|
|
|
|
want: "unexpected",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "POST-valid",
|
|
|
|
statuses: []string{"valid"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "POST-invalid",
|
|
|
|
statuses: []string{"invalid"},
|
|
|
|
want: "Error",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET-unexpected",
|
|
|
|
statuses: []string{"pending", "weird"},
|
|
|
|
want: "unexpected",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET-valid",
|
|
|
|
statuses: []string{"pending", "valid"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET-invalid",
|
|
|
|
statuses: []string{"pending", "invalid"},
|
|
|
|
want: "Error",
|
|
|
|
},
|
2015-12-05 14:53:53 +00:00
|
|
|
}
|
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
for _, test := range testCases {
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
statuses = test.statuses
|
|
|
|
|
|
|
|
err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"})
|
|
|
|
if test.want == "" {
|
2018-10-12 17:29:18 +00:00
|
|
|
require.NoError(t, err)
|
2018-09-15 17:16:35 +00:00
|
|
|
} else {
|
|
|
|
assert.Error(t, err)
|
|
|
|
assert.Contains(t, err.Error(), test.want)
|
|
|
|
}
|
|
|
|
})
|
2015-12-05 14:53:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-19 04:17:22 +00:00
|
|
|
func TestGetChallenges(t *testing.T) {
|
|
|
|
var ts *httptest.Server
|
|
|
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch r.Method {
|
2018-06-21 17:06:16 +00:00
|
|
|
case http.MethodGet, http.MethodHead:
|
2017-02-19 04:17:22 +00:00
|
|
|
w.Header().Add("Replay-Nonce", "12345")
|
|
|
|
w.Header().Add("Retry-After", "0")
|
2018-05-30 17:53:04 +00:00
|
|
|
writeJSONResponse(w, directory{
|
|
|
|
NewNonceURL: ts.URL,
|
|
|
|
NewAccountURL: ts.URL,
|
|
|
|
NewOrderURL: ts.URL,
|
|
|
|
RevokeCertURL: ts.URL,
|
|
|
|
KeyChangeURL: ts.URL,
|
|
|
|
})
|
2018-06-21 17:06:16 +00:00
|
|
|
case http.MethodPost:
|
2018-05-30 17:53:04 +00:00
|
|
|
writeJSONResponse(w, orderMessage{})
|
2017-02-19 04:17:22 +00:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
keyBits := 512 // small value keeps test fast
|
|
|
|
keyType := RSA2048
|
2018-09-15 17:16:35 +00:00
|
|
|
|
2017-02-19 04:17:22 +00:00
|
|
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not generate test key")
|
|
|
|
|
2017-02-19 04:17:22 +00:00
|
|
|
user := mockUser{
|
|
|
|
email: "test@test.com",
|
2018-05-30 17:53:04 +00:00
|
|
|
regres: &RegistrationResource{URI: ts.URL},
|
2017-02-19 04:17:22 +00:00
|
|
|
privatekey: key,
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := NewClient(ts.URL, user, keyType)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not create client")
|
2017-02-19 04:17:22 +00:00
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
_, err = client.createOrderForIdentifiers([]string{"example.com"})
|
2018-10-12 17:29:18 +00:00
|
|
|
require.NoError(t, err)
|
2017-02-19 04:17:22 +00:00
|
|
|
}
|
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
func TestResolveAccountByKey(t *testing.T) {
|
|
|
|
keyBits := 512
|
|
|
|
keyType := RSA2048
|
2018-09-15 17:16:35 +00:00
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not generate test key")
|
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
user := mockUser{
|
|
|
|
email: "test@test.com",
|
|
|
|
regres: new(RegistrationResource),
|
|
|
|
privatekey: key,
|
|
|
|
}
|
|
|
|
|
|
|
|
var ts *httptest.Server
|
|
|
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch r.RequestURI {
|
|
|
|
case "/directory":
|
|
|
|
writeJSONResponse(w, directory{
|
|
|
|
NewNonceURL: ts.URL + "/nonce",
|
|
|
|
NewAccountURL: ts.URL + "/account",
|
|
|
|
NewOrderURL: ts.URL + "/newOrder",
|
|
|
|
RevokeCertURL: ts.URL + "/revokeCert",
|
|
|
|
KeyChangeURL: ts.URL + "/keyChange",
|
|
|
|
})
|
|
|
|
case "/nonce":
|
|
|
|
w.Header().Add("Replay-Nonce", "12345")
|
|
|
|
w.Header().Add("Retry-After", "0")
|
|
|
|
case "/account":
|
|
|
|
w.Header().Set("Location", ts.URL+"/account_recovery")
|
|
|
|
case "/account_recovery":
|
|
|
|
writeJSONResponse(w, accountMessage{
|
|
|
|
Status: "valid",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
|
|
|
|
client, err := NewClient(ts.URL+"/directory", user, keyType)
|
2018-09-15 17:16:35 +00:00
|
|
|
require.NoError(t, err, "Could not create client")
|
2018-05-30 17:53:04 +00:00
|
|
|
|
2018-09-15 17:16:35 +00:00
|
|
|
res, err := client.ResolveAccountByKey()
|
|
|
|
require.NoError(t, err, "Unexpected error resolving account by key")
|
|
|
|
|
|
|
|
assert.Equal(t, "valid", res.Body.Status, "Unexpected account status")
|
2018-05-30 17:53:04 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 14:53:53 +00:00
|
|
|
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
|
|
|
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
|
|
|
bs, err := json.Marshal(body)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if _, err := w.Write(bs); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// stubValidate is like validate, except it does nothing.
|
2018-09-15 17:16:35 +00:00
|
|
|
func stubValidate(_ *jws, _, _ string, _ challenge) error {
|
2015-12-05 14:53:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-06-13 15:00:18 +00:00
|
|
|
type mockUser struct {
|
|
|
|
email string
|
|
|
|
regres *RegistrationResource
|
|
|
|
privatekey *rsa.PrivateKey
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u mockUser) GetEmail() string { return u.email }
|
|
|
|
func (u mockUser) GetRegistration() *RegistrationResource { return u.regres }
|
2016-01-27 01:01:58 +00:00
|
|
|
func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }
|