2019-05-27 00:41:10 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/json"
|
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http/httptest"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-chi/chi"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/smallstep/assert"
|
|
|
|
"github.com/smallstep/certificates/acme"
|
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
2019-10-08 01:17:20 +00:00
|
|
|
"github.com/smallstep/certificates/db"
|
2019-05-27 00:41:10 +00:00
|
|
|
"github.com/smallstep/cli/crypto/pemutil"
|
|
|
|
"github.com/smallstep/cli/jose"
|
|
|
|
)
|
|
|
|
|
|
|
|
type mockAcmeAuthority struct {
|
|
|
|
deactivateAccount func(provisioner.Interface, string) (*acme.Account, error)
|
|
|
|
finalizeOrder func(p provisioner.Interface, accID string, id string, csr *x509.CertificateRequest) (*acme.Order, error)
|
|
|
|
getAccount func(p provisioner.Interface, id string) (*acme.Account, error)
|
|
|
|
getAccountByKey func(provisioner.Interface, *jose.JSONWebKey) (*acme.Account, error)
|
|
|
|
getAuthz func(p provisioner.Interface, accID string, id string) (*acme.Authz, error)
|
|
|
|
getCertificate func(accID string, id string) ([]byte, error)
|
|
|
|
getChallenge func(p provisioner.Interface, accID string, id string) (*acme.Challenge, error)
|
|
|
|
getDirectory func(provisioner.Interface) *acme.Directory
|
|
|
|
getLink func(acme.Link, string, bool, ...string) string
|
|
|
|
getOrder func(p provisioner.Interface, accID string, id string) (*acme.Order, error)
|
|
|
|
getOrdersByAccount func(p provisioner.Interface, id string) ([]string, error)
|
|
|
|
loadProvisionerByID func(string) (provisioner.Interface, error)
|
|
|
|
newAccount func(provisioner.Interface, acme.AccountOptions) (*acme.Account, error)
|
|
|
|
newNonce func() (string, error)
|
|
|
|
newOrder func(provisioner.Interface, acme.OrderOptions) (*acme.Order, error)
|
|
|
|
updateAccount func(provisioner.Interface, string, []string) (*acme.Account, error)
|
|
|
|
useNonce func(string) error
|
|
|
|
validateChallenge func(p provisioner.Interface, accID string, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error)
|
2020-04-30 11:41:49 +00:00
|
|
|
backoffChallenge func(p provisioner.Interface, accID, chID string, jwk *jose.JSONWebKey) (time.Duration, error)
|
2019-05-27 00:41:10 +00:00
|
|
|
ret1 interface{}
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) DeactivateAccount(p provisioner.Interface, id string) (*acme.Account, error) {
|
|
|
|
if m.deactivateAccount != nil {
|
|
|
|
return m.deactivateAccount(p, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Account), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) FinalizeOrder(p provisioner.Interface, accID, id string, csr *x509.CertificateRequest) (*acme.Order, error) {
|
|
|
|
if m.finalizeOrder != nil {
|
|
|
|
return m.finalizeOrder(p, accID, id, csr)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Order), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetAccount(p provisioner.Interface, id string) (*acme.Account, error) {
|
|
|
|
if m.getAccount != nil {
|
|
|
|
return m.getAccount(p, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Account), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetAccountByKey(p provisioner.Interface, jwk *jose.JSONWebKey) (*acme.Account, error) {
|
|
|
|
if m.getAccountByKey != nil {
|
|
|
|
return m.getAccountByKey(p, jwk)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Account), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetAuthz(p provisioner.Interface, accID, id string) (*acme.Authz, error) {
|
|
|
|
if m.getAuthz != nil {
|
|
|
|
return m.getAuthz(p, accID, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Authz), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetCertificate(accID, id string) ([]byte, error) {
|
|
|
|
if m.getCertificate != nil {
|
|
|
|
return m.getCertificate(accID, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.([]byte), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetChallenge(p provisioner.Interface, accID, id string) (*acme.Challenge, error) {
|
|
|
|
if m.getChallenge != nil {
|
|
|
|
return m.getChallenge(p, accID, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Challenge), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetDirectory(p provisioner.Interface) *acme.Directory {
|
|
|
|
if m.getDirectory != nil {
|
|
|
|
return m.getDirectory(p)
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Directory)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetLink(typ acme.Link, provID string, abs bool, in ...string) string {
|
|
|
|
if m.getLink != nil {
|
|
|
|
return m.getLink(typ, provID, abs, in...)
|
|
|
|
}
|
|
|
|
return m.ret1.(string)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetOrder(p provisioner.Interface, accID, id string) (*acme.Order, error) {
|
|
|
|
if m.getOrder != nil {
|
|
|
|
return m.getOrder(p, accID, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Order), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) GetOrdersByAccount(p provisioner.Interface, id string) ([]string, error) {
|
|
|
|
if m.getOrdersByAccount != nil {
|
|
|
|
return m.getOrdersByAccount(p, id)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.([]string), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) LoadProvisionerByID(provID string) (provisioner.Interface, error) {
|
|
|
|
if m.loadProvisionerByID != nil {
|
|
|
|
return m.loadProvisionerByID(provID)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(provisioner.Interface), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) NewAccount(p provisioner.Interface, ops acme.AccountOptions) (*acme.Account, error) {
|
|
|
|
if m.newAccount != nil {
|
|
|
|
return m.newAccount(p, ops)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Account), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) NewNonce() (string, error) {
|
|
|
|
if m.newNonce != nil {
|
|
|
|
return m.newNonce()
|
|
|
|
} else if m.err != nil {
|
|
|
|
return "", m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(string), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) NewOrder(p provisioner.Interface, ops acme.OrderOptions) (*acme.Order, error) {
|
|
|
|
if m.newOrder != nil {
|
|
|
|
return m.newOrder(p, ops)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Order), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) UpdateAccount(p provisioner.Interface, id string, contact []string) (*acme.Account, error) {
|
|
|
|
if m.updateAccount != nil {
|
|
|
|
return m.updateAccount(p, id, contact)
|
|
|
|
} else if m.err != nil {
|
|
|
|
return nil, m.err
|
|
|
|
}
|
|
|
|
return m.ret1.(*acme.Account), m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) UseNonce(nonce string) error {
|
|
|
|
if m.useNonce != nil {
|
|
|
|
return m.useNonce(nonce)
|
|
|
|
}
|
|
|
|
return m.err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockAcmeAuthority) ValidateChallenge(p provisioner.Interface, accID string, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
|
|
|
|
switch {
|
|
|
|
case m.validateChallenge != nil:
|
|
|
|
return m.validateChallenge(p, accID, id, jwk)
|
|
|
|
case m.err != nil:
|
|
|
|
return nil, m.err
|
|
|
|
default:
|
|
|
|
return m.ret1.(*acme.Challenge), m.err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-30 11:41:49 +00:00
|
|
|
func (m *mockAcmeAuthority) BackoffChallenge(p provisioner.Interface, accID, chID string, jwk *jose.JSONWebKey) (time.Duration, error) {
|
|
|
|
switch {
|
|
|
|
case m.backoffChallenge != nil:
|
|
|
|
return m.backoffChallenge(p, accID, chID, jwk)
|
|
|
|
case m.err != nil:
|
|
|
|
return -1, m.err
|
|
|
|
default:
|
|
|
|
return m.ret1.(time.Duration), m.err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-27 00:41:10 +00:00
|
|
|
func TestHandlerGetNonce(t *testing.T) {
|
|
|
|
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) {
|
|
|
|
h := New(nil).(*Handler)
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandlerGetDirectory(t *testing.T) {
|
2019-10-08 01:17:20 +00:00
|
|
|
auth, err := acme.NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
|
|
|
assert.FatalError(t, err)
|
2019-05-27 00:41:10 +00:00
|
|
|
prov := newProv()
|
|
|
|
url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/directory", acme.URLSafeProvisionerName(prov))
|
|
|
|
|
|
|
|
expDir := acme.Directory{
|
|
|
|
NewNonce: fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-nonce", acme.URLSafeProvisionerName(prov)),
|
|
|
|
NewAccount: fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-account", acme.URLSafeProvisionerName(prov)),
|
|
|
|
NewOrder: fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-order", acme.URLSafeProvisionerName(prov)),
|
|
|
|
RevokeCert: fmt.Sprintf("https://ca.smallstep.com/acme/%s/revoke-cert", acme.URLSafeProvisionerName(prov)),
|
|
|
|
KeyChange: fmt.Sprintf("https://ca.smallstep.com/acme/%s/key-change", acme.URLSafeProvisionerName(prov)),
|
|
|
|
}
|
|
|
|
|
|
|
|
type test struct {
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
problem *acme.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.Background(),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, nil),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
h := New(auth).(*Handler)
|
|
|
|
req := httptest.NewRequest("GET", url, nil)
|
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
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)
|
|
|
|
|
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
|
|
|
var ae acme.AError
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
prob := tc.problem.ToACME()
|
|
|
|
|
|
|
|
assert.Equals(t, ae.Type, prob.Type)
|
|
|
|
assert.Equals(t, ae.Detail, prob.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, prob.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
|
|
|
} else {
|
|
|
|
var dir acme.Directory
|
|
|
|
json.Unmarshal(bytes.TrimSpace(body), &dir)
|
|
|
|
assert.Equals(t, dir, expDir)
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandlerGetAuthz(t *testing.T) {
|
|
|
|
expiry := time.Now().UTC().Add(6 * time.Hour)
|
|
|
|
az := acme.Authz{
|
|
|
|
ID: "authzID",
|
|
|
|
Identifier: acme.Identifier{
|
|
|
|
Type: "dns",
|
|
|
|
Value: "example.com",
|
|
|
|
},
|
|
|
|
Status: "pending",
|
|
|
|
Expires: expiry.Format(time.RFC3339),
|
|
|
|
Wildcard: false,
|
|
|
|
Challenges: []*acme.Challenge{
|
|
|
|
{
|
|
|
|
Type: "http-01",
|
|
|
|
Status: "pending",
|
|
|
|
Token: "tok2",
|
|
|
|
URL: "https://ca.smallstep.com/acme/challenge/chHTTPID",
|
|
|
|
ID: "chHTTP01ID",
|
|
|
|
AuthzID: "authzID",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Type: "dns-01",
|
|
|
|
Status: "pending",
|
|
|
|
Token: "tok2",
|
|
|
|
URL: "https://ca.smallstep.com/acme/challenge/chDNSID",
|
|
|
|
ID: "chDNSID",
|
|
|
|
AuthzID: "authzID",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
prov := newProv()
|
|
|
|
|
|
|
|
// Request with chi context
|
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("authzID", az.ID)
|
|
|
|
url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/challenge/%s",
|
|
|
|
acme.URLSafeProvisionerName(prov), az.ID)
|
|
|
|
|
|
|
|
type test struct {
|
|
|
|
auth acme.Interface
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
problem *acme.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: context.Background(),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, nil),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, nil)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: ctx,
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/getAuthz-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, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
err: acme.ServerInternalErr(errors.New("force")),
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("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, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
getAuthz: func(p provisioner.Interface, accID, id string) (*acme.Authz, error) {
|
|
|
|
assert.Equals(t, p, prov)
|
|
|
|
assert.Equals(t, accID, acc.ID)
|
|
|
|
assert.Equals(t, id, az.ID)
|
|
|
|
return &az, nil
|
|
|
|
},
|
|
|
|
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
|
|
|
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
|
|
|
assert.Equals(t, typ, acme.AuthzLink)
|
|
|
|
assert.True(t, abs)
|
|
|
|
assert.Equals(t, in, []string{az.ID})
|
|
|
|
return url
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
h := New(tc.auth).(*Handler)
|
|
|
|
req := httptest.NewRequest("GET", url, nil)
|
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetAuthz(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)
|
|
|
|
|
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
|
|
|
var ae acme.AError
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
prob := tc.problem.ToACME()
|
|
|
|
|
|
|
|
assert.Equals(t, ae.Type, prob.Type)
|
|
|
|
assert.Equals(t, ae.Detail, prob.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, prob.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
|
|
|
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"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandlerGetCertificate(t *testing.T) {
|
|
|
|
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()
|
|
|
|
// Request with chi context
|
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("certID", certID)
|
|
|
|
url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/certificate/%s",
|
|
|
|
acme.URLSafeProvisionerName(prov), certID)
|
|
|
|
|
|
|
|
type test struct {
|
|
|
|
auth acme.Interface
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
problem *acme.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
|
|
|
ctx := context.WithValue(context.Background(), accContextKey, nil)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{},
|
|
|
|
ctx: ctx,
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/getCertificate-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{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
err: acme.ServerInternalErr(errors.New("force")),
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("force")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok": 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{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
getCertificate: func(accID, id string) ([]byte, error) {
|
|
|
|
assert.Equals(t, accID, acc.ID)
|
|
|
|
assert.Equals(t, id, certID)
|
|
|
|
return certBytes, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
h := New(tc.auth).(*Handler)
|
|
|
|
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)
|
|
|
|
|
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
|
|
|
var ae acme.AError
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
prob := tc.problem.ToACME()
|
|
|
|
|
|
|
|
assert.Equals(t, ae.Type, prob.Type)
|
|
|
|
assert.Equals(t, ae.Detail, prob.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, prob.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
|
|
|
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"})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ch() acme.Challenge {
|
|
|
|
return acme.Challenge{
|
|
|
|
Type: "http-01",
|
|
|
|
Status: "pending",
|
|
|
|
Token: "tok2",
|
|
|
|
URL: "https://ca.smallstep.com/acme/challenge/chID",
|
|
|
|
ID: "chID",
|
|
|
|
AuthzID: "authzID",
|
2020-04-30 11:40:56 +00:00
|
|
|
Retry: &acme.Retry{Called: 0, Active: false},
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandlerGetChallenge(t *testing.T) {
|
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
chiCtx.URLParams.Add("chID", "chID")
|
|
|
|
url := fmt.Sprintf("http://ca.smallstep.com/acme/challenge/%s", "chID")
|
|
|
|
prov := newProv()
|
|
|
|
|
|
|
|
type test struct {
|
|
|
|
auth acme.Interface
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
ch acme.Challenge
|
|
|
|
problem *acme.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
|
|
|
"fail/no-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.Background(),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-provisioner": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, nil),
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/no-account": func(t *testing.T) test {
|
|
|
|
return test{
|
|
|
|
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-account": func(t *testing.T) test {
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, nil)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2020-02-02 01:35:41 +00:00
|
|
|
statusCode: 400,
|
2019-05-27 00:41:10 +00:00
|
|
|
problem: acme.AccountDoesNotExistErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/no-payload": func(t *testing.T) test {
|
|
|
|
acc := &acme.Account{ID: "accID"}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/nil-payload": 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, nil)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 500,
|
|
|
|
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"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})
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
err: acme.UnauthorizedErr(nil),
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 401,
|
|
|
|
problem: acme.UnauthorizedErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail/get-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{isPostAsGet: true})
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
err: acme.UnauthorizedErr(nil),
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 401,
|
|
|
|
problem: acme.UnauthorizedErr(nil),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok/validate-challenge": func(t *testing.T) test {
|
|
|
|
key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
acc := &acme.Account{ID: "accID", Key: key}
|
|
|
|
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)
|
|
|
|
ch := ch()
|
|
|
|
ch.Status = "valid"
|
|
|
|
ch.Validated = time.Now().UTC().Format(time.RFC3339)
|
|
|
|
count := 0
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
validateChallenge: func(p provisioner.Interface, accID, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, p, prov)
|
|
|
|
assert.Equals(t, accID, acc.ID)
|
|
|
|
assert.Equals(t, id, ch.ID)
|
|
|
|
assert.Equals(t, jwk.KeyID, key.KeyID)
|
|
|
|
return &ch, nil
|
|
|
|
},
|
|
|
|
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
|
|
|
var ret string
|
|
|
|
switch count {
|
|
|
|
case 0:
|
|
|
|
assert.Equals(t, typ, acme.AuthzLink)
|
|
|
|
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
|
|
|
assert.True(t, abs)
|
|
|
|
assert.Equals(t, in, []string{ch.AuthzID})
|
|
|
|
ret = fmt.Sprintf("https://ca.smallstep.com/acme/authz/%s", ch.AuthzID)
|
|
|
|
case 1:
|
|
|
|
assert.Equals(t, typ, acme.ChallengeLink)
|
|
|
|
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
|
|
|
assert.True(t, abs)
|
|
|
|
assert.Equals(t, in, []string{ch.ID})
|
|
|
|
ret = url
|
|
|
|
}
|
|
|
|
count++
|
|
|
|
return ret
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
ch: ch,
|
|
|
|
}
|
|
|
|
},
|
2020-02-14 21:17:52 +00:00
|
|
|
"ok/retry-after": func(t *testing.T) test {
|
|
|
|
key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
acc := &acme.Account{ID: "accID", Key: key}
|
|
|
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
|
|
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
|
|
|
// TODO: Add correct key such that challenge object is already "active"
|
|
|
|
chiCtxInactive := chi.NewRouteContext()
|
|
|
|
chiCtxInactive.URLParams.Add("chID", "chID")
|
|
|
|
//chiCtxInactive.URLParams.Add("Active", "true")
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtxInactive)
|
|
|
|
ch := ch()
|
|
|
|
ch.Retry.Active = true
|
|
|
|
chJSON, err := json.Marshal(ch)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: chJSON})
|
|
|
|
return test{
|
|
|
|
auth: &mockAcmeAuthority{
|
|
|
|
validateChallenge: func(p provisioner.Interface, accID, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
|
|
|
|
assert.Equals(t, p, prov)
|
|
|
|
assert.Equals(t, accID, acc.ID)
|
|
|
|
assert.Equals(t, id, ch.ID)
|
|
|
|
assert.Equals(t, jwk.KeyID, key.KeyID)
|
|
|
|
return &ch, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
ctx: ctx,
|
|
|
|
statusCode: 200,
|
|
|
|
ch: ch,
|
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
for name, run := range tests {
|
|
|
|
tc := run(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
h := New(tc.auth).(*Handler)
|
|
|
|
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)
|
|
|
|
|
|
|
|
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
|
|
|
var ae acme.AError
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
|
|
|
prob := tc.problem.ToACME()
|
|
|
|
|
|
|
|
assert.Equals(t, ae.Type, prob.Type)
|
|
|
|
assert.Equals(t, ae.Detail, prob.Detail)
|
|
|
|
assert.Equals(t, ae.Identifier, prob.Identifier)
|
|
|
|
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
2020-02-19 01:48:53 +00:00
|
|
|
} else if res.StatusCode >= 200 {
|
2019-05-27 00:41:10 +00:00
|
|
|
expB, err := json.Marshal(tc.ch)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, bytes.TrimSpace(body), expB)
|
|
|
|
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<https://ca.smallstep.com/acme/authz/%s>;rel=\"up\"", tc.ch.AuthzID)})
|
|
|
|
assert.Equals(t, res.Header["Location"], []string{url})
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
2020-02-19 01:48:53 +00:00
|
|
|
} else if res.StatusCode >= 100 {
|
2020-02-14 21:17:52 +00:00
|
|
|
expB, err := json.Marshal(tc.ch)
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, bytes.TrimSpace(body), expB)
|
2020-02-19 01:48:53 +00:00
|
|
|
assert.True(t, res.Header["Retry-After"] != nil)
|
|
|
|
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|