2019-05-27 00:41:10 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2021-03-11 07:05:46 +00:00
|
|
|
"crypto/x509"
|
2019-05-27 00:41:10 +00:00
|
|
|
"encoding/json"
|
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2021-03-11 21:10:14 +00:00
|
|
|
"net/http"
|
2019-05-27 00:41:10 +00:00
|
|
|
"net/http/httptest"
|
2020-05-07 03:18:12 +00:00
|
|
|
"net/url"
|
2019-05-27 00:41:10 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-chi/chi"
|
2021-03-11 21:10:14 +00:00
|
|
|
"github.com/pkg/errors"
|
2019-05-27 00:41:10 +00:00
|
|
|
"github.com/smallstep/assert"
|
|
|
|
"github.com/smallstep/certificates/acme"
|
2021-03-11 21:10:14 +00:00
|
|
|
"go.step.sm/crypto/jose"
|
2020-08-14 22:33:50 +00:00
|
|
|
"go.step.sm/crypto/pemutil"
|
2019-05-27 00:41:10 +00:00
|
|
|
)
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
func TestHandler_GetNonce(t *testing.T) {
|
2019-05-27 00:41:10 +00:00
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
statusCode int
|
|
|
|
}{
|
|
|
|
{"GET", 204},
|
|
|
|
{"HEAD", 200},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request with chi context
|
|
|
|
req := httptest.NewRequest("GET", "http://ca.smallstep.com/nonce", nil)
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2021-03-09 06:35:57 +00:00
|
|
|
h := &Handler{}
|
2019-05-27 00:41:10 +00:00
|
|
|
w := httptest.NewRecorder()
|
|
|
|
req.Method = tt.name
|
|
|
|
h.GetNonce(w, req)
|
|
|
|
res := w.Result()
|
|
|
|
|
|
|
|
if res.StatusCode != tt.statusCode {
|
|
|
|
t.Errorf("Handler.GetNonce StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
func TestHandler_GetDirectory(t *testing.T) {
|
2021-03-11 07:05:46 +00:00
|
|
|
linker := NewLinker("ca.smallstep.com", "acme")
|
2020-05-07 03:18:12 +00:00
|
|
|
|
2019-05-27 00:41:10 +00:00
|
|
|
prov := newProv()
|
2020-05-07 03:18:12 +00:00
|
|
|
provName := url.PathEscape(prov.GetName())
|
|
|
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
2019-05-27 00:41:10 +00:00
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
expDir := Directory{
|
2020-05-07 03:18:12 +00:00
|
|
|
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
|
|
|
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
|
|
|
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
|
|
|
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
|
|
|
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type test struct {
|
|
|
|
statusCode int
|
2021-03-09 06:35:57 +00:00
|
|
|
err *acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2021-03-09 06:35:57 +00:00
|
|
|
h := &Handler{linker: linker}
|
2020-05-07 03:18:12 +00:00
|
|
|
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
|
|
|
req = req.WithContext(ctx)
|
2019-05-27 00:41:10 +00:00
|
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetDirectory(w, req)
|
|
|
|
res := w.Result()
|
|
|
|
|
|
|
|
assert.Equals(t, res.StatusCode, tc.statusCode)
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
|
|
|
var ae acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
assert.Equals(t, ae.Type, tc.err.Type)
|
|
|
|
assert.Equals(t, ae.Detail, tc.err.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
|
|
|
} else {
|
2021-03-09 06:35:57 +00:00
|
|
|
var dir Directory
|
2019-05-27 00:41:10 +00:00
|
|
|
json.Unmarshal(bytes.TrimSpace(body), &dir)
|
|
|
|
assert.Equals(t, dir, expDir)
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 07:59:02 +00:00
|
|
|
func TestHandler_GetAuthorization(t *testing.T) {
|
2019-05-27 00:41:10 +00:00
|
|
|
expiry := time.Now().UTC().Add(6 * time.Hour)
|
2021-03-09 06:35:57 +00:00
|
|
|
az := acme.Authorization{
|
2021-03-11 07:59:02 +00:00
|
|
|
ID: "authzID",
|
|
|
|
AccountID: "accID",
|
2019-05-27 00:41:10 +00:00
|
|
|
Identifier: acme.Identifier{
|
|
|
|
Type: "dns",
|
|
|
|
Value: "example.com",
|
|
|
|
},
|
2021-03-19 21:37:45 +00:00
|
|
|
Status: "pending",
|
|
|
|
ExpiresAt: expiry,
|
|
|
|
Wildcard: false,
|
2019-05-27 00:41:10 +00:00
|
|
|
Challenges: []*acme.Challenge{
|
|
|
|
{
|
2021-03-25 07:23:57 +00:00
|
|
|
Type: "http-01",
|
|
|
|
Status: "pending",
|
|
|
|
Token: "tok2",
|
|
|
|
URL: "https://ca.smallstep.com/acme/challenge/chHTTPID",
|
|
|
|
ID: "chHTTP01ID",
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-25 07:23:57 +00:00
|
|
|
Type: "dns-01",
|
|
|
|
Status: "pending",
|
|
|
|
Token: "tok2",
|
|
|
|
URL: "https://ca.smallstep.com/acme/challenge/chDNSID",
|
|
|
|
ID: "chDNSID",
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
prov := newProv()
|
2020-05-07 03:18:12 +00:00
|
|
|
provName := url.PathEscape(prov.GetName())
|
|
|
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
2019-05-27 00:41:10 +00:00
|
|
|
|
|
|
|
// Request with chi context
|
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("authzID", az.ID)
|
2021-03-11 07:59:02 +00:00
|
|
|
url := fmt.Sprintf("%s/acme/%s/authz/%s",
|
2020-05-07 03:18:12 +00:00
|
|
|
baseURL.String(), provName, az.ID)
|
2019-05-27 00:41:10 +00:00
|
|
|
|
|
|
|
type test struct {
|
2021-03-09 06:35:57 +00:00
|
|
|
db acme.DB
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
2021-03-09 06:35:57 +00:00
|
|
|
err *acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{},
|
|
|
|
ctx: context.Background(),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, nil)
|
2019-05-27 00:41:10 +00:00
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{},
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx: ctx,
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2021-03-11 07:59:02 +00:00
|
|
|
"fail/db.GetAuthorization-error": func(t *testing.T) test {
|
2019-05-27 00:41:10 +00:00
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{
|
|
|
|
MockError: acme.NewErrorISE("force"),
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewErrorISE("force"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2021-03-11 07:59:02 +00:00
|
|
|
"fail/account-id-mismatch": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
|
|
|
assert.Equals(t, id, az.ID)
|
|
|
|
return &acme.Authorization{
|
|
|
|
AccountID: "foo",
|
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 401,
|
|
|
|
err: acme.NewError(acme.ErrorUnauthorizedType, "account id mismatch"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/db.UpdateAuthorization-error": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
|
|
|
assert.Equals(t, id, az.ID)
|
|
|
|
return &acme.Authorization{
|
|
|
|
AccountID: "accID",
|
|
|
|
Status: acme.StatusPending,
|
2021-03-19 21:37:45 +00:00
|
|
|
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
2021-03-11 07:59:02 +00:00
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
MockUpdateAuthorization: func(ctx context.Context, az *acme.Authorization) error {
|
|
|
|
assert.Equals(t, az.Status, acme.StatusInvalid)
|
|
|
|
return acme.NewErrorISE("force")
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("force"),
|
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
2019-05-27 00:41:10 +00:00
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, id, az.ID)
|
|
|
|
return &az, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2021-03-11 07:59:02 +00:00
|
|
|
h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")}
|
2020-05-07 03:18:12 +00:00
|
|
|
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
2019-05-27 00:41:10 +00:00
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
2021-03-09 06:35:57 +00:00
|
|
|
h.GetAuthorization(w, req)
|
2019-05-27 00:41:10 +00:00
|
|
|
res := w.Result()
|
|
|
|
|
|
|
|
assert.Equals(t, res.StatusCode, tc.statusCode)
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
|
|
|
var ae acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
assert.Equals(t, ae.Type, tc.err.Type)
|
|
|
|
assert.Equals(t, ae.Detail, tc.err.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
|
|
|
} else {
|
|
|
|
//var gotAz acme.Authz
|
|
|
|
//assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &gotAz))
|
|
|
|
expB, err := json.Marshal(az)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, bytes.TrimSpace(body), expB)
|
|
|
|
assert.Equals(t, res.Header["Location"], []string{url})
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
func TestHandler_GetCertificate(t *testing.T) {
|
2019-05-27 00:41:10 +00:00
|
|
|
leaf, err := pemutil.ReadCertificate("../../authority/testdata/certs/foo.crt")
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
inter, err := pemutil.ReadCertificate("../../authority/testdata/certs/intermediate_ca.crt")
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
root, err := pemutil.ReadCertificate("../../authority/testdata/certs/root_ca.crt")
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
|
|
|
certBytes := append(pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "CERTIFICATE",
|
|
|
|
Bytes: leaf.Raw,
|
|
|
|
}), pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "CERTIFICATE",
|
|
|
|
Bytes: inter.Raw,
|
|
|
|
})...)
|
|
|
|
certBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "CERTIFICATE",
|
|
|
|
Bytes: root.Raw,
|
|
|
|
})...)
|
|
|
|
certID := "certID"
|
|
|
|
|
|
|
|
prov := newProv()
|
2020-05-07 03:18:12 +00:00
|
|
|
provName := url.PathEscape(prov.GetName())
|
|
|
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
2019-05-27 00:41:10 +00:00
|
|
|
// Request with chi context
|
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("certID", certID)
|
2020-05-07 03:18:12 +00:00
|
|
|
url := fmt.Sprintf("%s/acme/%s/certificate/%s",
|
|
|
|
baseURL.String(), provName, certID)
|
2019-05-27 00:41:10 +00:00
|
|
|
|
|
|
|
type test struct {
|
2021-03-09 06:35:57 +00:00
|
|
|
db acme.DB
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
2021-03-09 06:35:57 +00:00
|
|
|
err *acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{},
|
|
|
|
ctx: context.Background(),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, nil)
|
2019-05-27 00:41:10 +00:00
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{},
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx: ctx,
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2021-03-11 07:05:46 +00:00
|
|
|
"fail/db.GetCertificate-error": func(t *testing.T) test {
|
2019-05-27 00:41:10 +00:00
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{
|
|
|
|
MockError: acme.NewErrorISE("force"),
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewErrorISE("force"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2021-03-11 07:05:46 +00:00
|
|
|
"fail/account-id-mismatch": func(t *testing.T) test {
|
2020-08-12 22:50:45 +00:00
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
2020-08-12 22:50:45 +00:00
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {
|
2020-08-12 22:50:45 +00:00
|
|
|
assert.Equals(t, id, certID)
|
2021-03-11 07:05:46 +00:00
|
|
|
return &acme.Certificate{AccountID: "foo"}, nil
|
2020-08-12 22:50:45 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
2021-03-11 07:05:46 +00:00
|
|
|
statusCode: 401,
|
|
|
|
err: acme.NewError(acme.ErrorUnauthorizedType, "account id mismatch"),
|
2020-08-12 22:50:45 +00:00
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, id, certID)
|
2021-03-11 07:05:46 +00:00
|
|
|
return &acme.Certificate{
|
|
|
|
AccountID: "accID",
|
|
|
|
OrderID: "ordID",
|
|
|
|
Leaf: leaf,
|
|
|
|
Intermediates: []*x509.Certificate{inter, root},
|
|
|
|
ID: id,
|
|
|
|
}, nil
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2021-03-09 06:35:57 +00:00
|
|
|
h := &Handler{db: tc.db}
|
2019-05-27 00:41:10 +00:00
|
|
|
req := httptest.NewRequest("GET", url, nil)
|
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetCertificate(w, req)
|
|
|
|
res := w.Result()
|
|
|
|
|
|
|
|
assert.Equals(t, res.StatusCode, tc.statusCode)
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
|
|
|
var ae acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
assert.Equals(t, ae.Type, tc.err.Type)
|
|
|
|
assert.HasPrefix(t, ae.Detail, tc.err.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
|
|
|
} else {
|
|
|
|
assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain; charset=utf-8"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 07:59:02 +00:00
|
|
|
func TestHandler_GetChallenge(t *testing.T) {
|
2019-05-27 00:41:10 +00:00
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("chID", "chID")
|
2021-03-11 21:10:14 +00:00
|
|
|
chiCtx.URLParams.Add("authzID", "authzID")
|
2019-05-27 00:41:10 +00:00
|
|
|
prov := newProv()
|
2020-05-07 03:18:12 +00:00
|
|
|
provName := url.PathEscape(prov.GetName())
|
2021-03-11 21:10:14 +00:00
|
|
|
|
2020-05-07 03:18:12 +00:00
|
|
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
2021-03-11 21:10:14 +00:00
|
|
|
|
|
|
|
url := fmt.Sprintf("%s/acme/%s/challenge/%s/%s",
|
|
|
|
baseURL.String(), provName, "authzID", "chID")
|
2019-05-27 00:41:10 +00:00
|
|
|
|
|
|
|
type test struct {
|
2021-03-09 06:35:57 +00:00
|
|
|
db acme.DB
|
2021-03-11 21:10:14 +00:00
|
|
|
vco *acme.ValidateChallengeOptions
|
2019-05-27 00:41:10 +00:00
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
2021-03-11 21:10:14 +00:00
|
|
|
ch *acme.Challenge
|
2021-03-09 06:35:57 +00:00
|
|
|
err *acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx: context.Background(),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
|
|
|
return test{
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx: context.WithValue(context.Background(), accContextKey, nil),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2021-03-09 06:35:57 +00:00
|
|
|
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/no-payload": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
2019-05-27 00:41:10 +00:00
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2021-03-25 03:46:02 +00:00
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("payload expected in request context"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-payload": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
2021-03-09 06:35:57 +00:00
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, nil)
|
2019-05-27 00:41:10 +00:00
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2021-03-25 03:46:02 +00:00
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("payload expected in request context"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2021-03-11 21:10:14 +00:00
|
|
|
"fail/db.GetChallenge-error": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return nil, acme.NewErrorISE("force")
|
2019-05-27 00:41:10 +00:00
|
|
|
},
|
2021-03-11 21:10:14 +00:00
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("force"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/account-id-mismatch": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return &acme.Challenge{AccountID: "foo"}, nil
|
2021-03-11 07:59:02 +00:00
|
|
|
},
|
2021-03-11 21:10:14 +00:00
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 401,
|
|
|
|
err: acme.NewError(acme.ErrorUnauthorizedType, "accout id mismatch"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/no-jwk": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return &acme.Challenge{AccountID: "accID"}, nil
|
2021-03-11 07:59:02 +00:00
|
|
|
},
|
2021-03-11 21:10:14 +00:00
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("missing jwk"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-jwk": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
ctx = context.WithValue(ctx, jwkContextKey, nil)
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return &acme.Challenge{AccountID: "accID"}, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("nil jwk"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/validate-challenge-error": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
_pub := _jwk.Public()
|
|
|
|
ctx = context.WithValue(ctx, jwkContextKey, &_pub)
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return &acme.Challenge{
|
|
|
|
Status: acme.StatusPending,
|
|
|
|
Type: "http-01",
|
|
|
|
AccountID: "accID",
|
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
|
|
|
assert.Equals(t, ch.Status, acme.StatusPending)
|
|
|
|
assert.Equals(t, ch.Type, "http-01")
|
|
|
|
assert.Equals(t, ch.AccountID, "accID")
|
2021-03-29 19:04:14 +00:00
|
|
|
assert.Equals(t, ch.AuthorizationID, "authzID")
|
2021-03-11 21:10:14 +00:00
|
|
|
assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())
|
|
|
|
return acme.NewErrorISE("force")
|
|
|
|
},
|
|
|
|
},
|
|
|
|
vco: &acme.ValidateChallengeOptions{
|
|
|
|
HTTPGet: func(string) (*http.Response, error) {
|
|
|
|
return nil, errors.New("force")
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
err: acme.NewErrorISE("force"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
|
|
|
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
_pub := _jwk.Public()
|
|
|
|
ctx = context.WithValue(ctx, jwkContextKey, &_pub)
|
|
|
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
db: &acme.MockDB{
|
|
|
|
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, chID, "chID")
|
|
|
|
assert.Equals(t, azID, "authzID")
|
|
|
|
return &acme.Challenge{
|
|
|
|
ID: "chID",
|
|
|
|
Status: acme.StatusPending,
|
|
|
|
Type: "http-01",
|
|
|
|
AccountID: "accID",
|
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
|
|
|
assert.Equals(t, ch.Status, acme.StatusPending)
|
|
|
|
assert.Equals(t, ch.Type, "http-01")
|
|
|
|
assert.Equals(t, ch.AccountID, "accID")
|
2021-03-29 19:04:14 +00:00
|
|
|
assert.Equals(t, ch.AuthorizationID, "authzID")
|
2021-03-11 21:10:14 +00:00
|
|
|
assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ch: &acme.Challenge{
|
2021-03-29 19:04:14 +00:00
|
|
|
ID: "chID",
|
|
|
|
Status: acme.StatusPending,
|
|
|
|
AuthorizationID: "authzID",
|
|
|
|
Type: "http-01",
|
|
|
|
AccountID: "accID",
|
|
|
|
URL: url,
|
|
|
|
Error: acme.NewError(acme.ErrorConnectionType, "force"),
|
2021-03-11 21:10:14 +00:00
|
|
|
},
|
|
|
|
vco: &acme.ValidateChallengeOptions{
|
|
|
|
HTTPGet: func(string) (*http.Response, error) {
|
|
|
|
return nil, errors.New("force")
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2021-03-11 21:10:14 +00:00
|
|
|
h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco}
|
2019-05-27 00:41:10 +00:00
|
|
|
req := httptest.NewRequest("GET", url, nil)
|
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetChallenge(w, req)
|
|
|
|
res := w.Result()
|
|
|
|
|
|
|
|
assert.Equals(t, res.StatusCode, tc.statusCode)
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
|
|
|
var ae acme.Error
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
|
2021-03-09 06:35:57 +00:00
|
|
|
assert.Equals(t, ae.Type, tc.err.Type)
|
|
|
|
assert.Equals(t, ae.Detail, tc.err.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
|
|
|
} else {
|
|
|
|
expB, err := json.Marshal(tc.ch)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, bytes.TrimSpace(body), expB)
|
2021-03-25 07:23:57 +00:00
|
|
|
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<%s/acme/%s/authz/%s>;rel=\"up\"", baseURL, provName, "authzID")})
|
2019-05-27 00:41:10 +00:00
|
|
|
assert.Equals(t, res.Header["Location"], []string{url})
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|