certificates/acme/authz_test.go
2020-05-18 04:06:30 -07:00

836 lines
22 KiB
Go

package acme
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
"github.com/smallstep/nosql/database"
)
func newAz() (authz, error) {
mockdb := &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return []byte("foo"), true, nil
},
}
return newAuthz(mockdb, "1234", Identifier{
Type: "dns", Value: "acme.example.com",
})
}
func TestGetAuthz(t *testing.T) {
type test struct {
id string
db nosql.DB
az authz
err *Error
}
tests := map[string]func(t *testing.T) test{
"fail/not-found": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
return test{
az: az,
id: az.getID(),
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
return nil, database.ErrNotFound
},
},
err: MalformedErr(errors.Errorf("authz %s not found: not found", az.getID())),
}
},
"fail/db-error": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
return test{
az: az,
id: az.getID(),
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
return nil, errors.New("force")
},
},
err: ServerInternalErr(errors.Errorf("error loading authz %s: force", az.getID())),
}
},
"fail/unmarshal-error": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Identifier.Type = "foo"
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
az: az,
id: az.getID(),
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, bucket, authzTable)
assert.Equals(t, key, []byte(az.getID()))
return b, nil
},
},
err: ServerInternalErr(errors.New("unexpected authz type foo")),
}
},
"ok": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
az: az,
id: az.getID(),
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, bucket, authzTable)
assert.Equals(t, key, []byte(az.getID()))
return b, nil
},
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
if az, err := getAuthz(tc.db, tc.id); err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, tc.az.getID(), az.getID())
assert.Equals(t, tc.az.getAccountID(), az.getAccountID())
assert.Equals(t, tc.az.getStatus(), az.getStatus())
assert.Equals(t, tc.az.getIdentifier(), az.getIdentifier())
assert.Equals(t, tc.az.getCreated(), az.getCreated())
assert.Equals(t, tc.az.getExpiry(), az.getExpiry())
assert.Equals(t, tc.az.getChallenges(), az.getChallenges())
}
}
})
}
}
func TestAuthzClone(t *testing.T) {
az, err := newAz()
assert.FatalError(t, err)
clone := az.clone()
assert.Equals(t, clone.getID(), az.getID())
assert.Equals(t, clone.getAccountID(), az.getAccountID())
assert.Equals(t, clone.getStatus(), az.getStatus())
assert.Equals(t, clone.getIdentifier(), az.getIdentifier())
assert.Equals(t, clone.getExpiry(), az.getExpiry())
assert.Equals(t, clone.getCreated(), az.getCreated())
assert.Equals(t, clone.getChallenges(), az.getChallenges())
clone.Status = StatusValid
assert.NotEquals(t, clone.getStatus(), az.getStatus())
}
func TestNewAuthz(t *testing.T) {
iden := Identifier{
Type: "dns", Value: "acme.example.com",
}
accID := "1234"
type test struct {
iden Identifier
db nosql.DB
err *Error
resChs *([]string)
}
tests := map[string]func(t *testing.T) test{
"fail/unexpected-type": func(t *testing.T) test {
return test{
iden: Identifier{Type: "foo", Value: "acme.example.com"},
err: MalformedErr(errors.New("unexpected authz type foo")),
}
},
"fail/new-http-chall-error": func(t *testing.T) test {
return test{
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return nil, false, errors.New("force")
},
},
err: ServerInternalErr(errors.New("error creating http challenge: error saving acme challenge: force")),
}
},
"fail/new-tls-alpn-chall-error": func(t *testing.T) test {
count := 0
return test{
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 1 {
return nil, false, errors.New("force")
}
count++
return nil, true, nil
},
},
err: ServerInternalErr(errors.New("error creating alpn challenge: error saving acme challenge: force")),
}
},
"fail/new-dns-chall-error": func(t *testing.T) test {
count := 0
return test{
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 2 {
return nil, false, errors.New("force")
}
count++
return nil, true, nil
},
},
err: ServerInternalErr(errors.New("error creating dns challenge: error saving acme challenge: force")),
}
},
"fail/save-authz-error": func(t *testing.T) test {
count := 0
return test{
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 3 {
return nil, false, errors.New("force")
}
count++
return nil, true, nil
},
},
err: ServerInternalErr(errors.New("error storing authz: force")),
}
},
"ok": func(t *testing.T) test {
chs := &([]string{})
count := 0
return test{
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 3 {
assert.Equals(t, bucket, authzTable)
assert.Equals(t, old, nil)
az, err := unmarshalAuthz(newval)
assert.FatalError(t, err)
assert.Equals(t, az.getID(), string(key))
assert.Equals(t, az.getAccountID(), accID)
assert.Equals(t, az.getStatus(), StatusPending)
assert.Equals(t, az.getIdentifier(), iden)
assert.Equals(t, az.getWildcard(), false)
*chs = az.getChallenges()
assert.True(t, az.getCreated().Before(time.Now().UTC().Add(time.Minute)))
assert.True(t, az.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
expiry := az.getCreated().Add(defaultExpiryDuration)
assert.True(t, az.getExpiry().Before(expiry.Add(time.Minute)))
assert.True(t, az.getExpiry().After(expiry.Add(-1*time.Minute)))
}
count++
return nil, true, nil
},
},
resChs: chs,
}
},
"ok/wildcard": func(t *testing.T) test {
chs := &([]string{})
count := 0
_iden := Identifier{Type: "dns", Value: "*.acme.example.com"}
return test{
iden: _iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 1 {
assert.Equals(t, bucket, authzTable)
assert.Equals(t, old, nil)
az, err := unmarshalAuthz(newval)
assert.FatalError(t, err)
assert.Equals(t, az.getID(), string(key))
assert.Equals(t, az.getAccountID(), accID)
assert.Equals(t, az.getStatus(), StatusPending)
assert.Equals(t, az.getIdentifier(), iden)
assert.Equals(t, az.getWildcard(), true)
*chs = az.getChallenges()
// Verify that we only have 1 challenge instead of 2.
assert.True(t, len(*chs) == 1)
assert.True(t, az.getCreated().Before(time.Now().UTC().Add(time.Minute)))
assert.True(t, az.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
expiry := az.getCreated().Add(defaultExpiryDuration)
assert.True(t, az.getExpiry().Before(expiry.Add(time.Minute)))
assert.True(t, az.getExpiry().After(expiry.Add(-1*time.Minute)))
}
count++
return nil, true, nil
},
},
resChs: chs,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
az, err := newAuthz(tc.db, accID, tc.iden)
if err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, az.getAccountID(), accID)
assert.Equals(t, az.getType(), "dns")
assert.Equals(t, az.getStatus(), StatusPending)
assert.True(t, az.getCreated().Before(time.Now().UTC().Add(time.Minute)))
assert.True(t, az.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
expiry := az.getCreated().Add(defaultExpiryDuration)
assert.True(t, az.getExpiry().Before(expiry.Add(time.Minute)))
assert.True(t, az.getExpiry().After(expiry.Add(-1*time.Minute)))
assert.Equals(t, az.getChallenges(), *(tc.resChs))
if strings.HasPrefix(tc.iden.Value, "*.") {
assert.True(t, az.getWildcard())
assert.Equals(t, az.getIdentifier().Value, strings.TrimPrefix(tc.iden.Value, "*."))
} else {
assert.False(t, az.getWildcard())
assert.Equals(t, az.getIdentifier().Value, tc.iden.Value)
}
assert.True(t, az.getID() != "")
}
}
})
}
}
func TestAuthzToACME(t *testing.T) {
dir := newDirectory("ca.smallstep.com", "acme")
var (
ch1, ch2 challenge
ch1Bytes, ch2Bytes = &([]byte{}), &([]byte{})
err error
)
count := 0
mockdb := &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 0 {
*ch1Bytes = newval
ch1, err = unmarshalChallenge(newval)
assert.FatalError(t, err)
} else if count == 1 {
*ch2Bytes = newval
ch2, err = unmarshalChallenge(newval)
assert.FatalError(t, err)
}
count++
return []byte("foo"), true, nil
},
}
iden := Identifier{
Type: "dns", Value: "acme.example.com",
}
az, err := newAuthz(mockdb, "1234", iden)
assert.FatalError(t, err)
prov := newProv()
ctx := context.WithValue(context.Background(), ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, BaseURLContextKey, "https://test.ca.smallstep.com:8080")
type test struct {
db nosql.DB
err *Error
}
tests := map[string]func(t *testing.T) test{
"fail/getChallenge1-error": func(t *testing.T) test {
return test{
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
return nil, errors.New("force")
},
},
err: ServerInternalErr(errors.New("error loading challenge")),
}
},
"fail/getChallenge2-error": func(t *testing.T) test {
count := 0
return test{
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
if count == 1 {
return nil, errors.New("force")
}
count++
return *ch1Bytes, nil
},
},
err: ServerInternalErr(errors.New("error loading challenge")),
}
},
"ok": func(t *testing.T) test {
count := 0
return test{
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
if count == 0 {
count++
return *ch1Bytes, nil
}
return *ch2Bytes, nil
},
},
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
acmeAz, err := az.toACME(ctx, tc.db, dir)
if err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, acmeAz.ID, az.getID())
assert.Equals(t, acmeAz.Identifier, iden)
assert.Equals(t, acmeAz.Status, StatusPending)
acmeCh1, err := ch1.toACME(ctx, dir)
assert.FatalError(t, err)
acmeCh2, err := ch2.toACME(ctx, dir)
assert.FatalError(t, err)
assert.Equals(t, acmeAz.Challenges[0], acmeCh1)
assert.Equals(t, acmeAz.Challenges[1], acmeCh2)
expiry, err := time.Parse(time.RFC3339, acmeAz.Expires)
assert.FatalError(t, err)
assert.Equals(t, expiry.String(), az.getExpiry().String())
}
}
})
}
}
func TestAuthzSave(t *testing.T) {
type test struct {
az, old authz
db nosql.DB
err *Error
}
tests := map[string]func(t *testing.T) test{
"fail/old-nil/swap-error": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
return test{
az: az,
old: nil,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return nil, false, errors.New("force")
},
},
err: ServerInternalErr(errors.New("error storing authz: force")),
}
},
"fail/old-nil/swap-false": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
return test{
az: az,
old: nil,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return []byte("foo"), false, nil
},
},
err: ServerInternalErr(errors.New("error storing authz; value has changed since last read")),
}
},
"ok/old-nil": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
az: az,
old: nil,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
assert.Equals(t, old, nil)
assert.Equals(t, b, newval)
assert.Equals(t, bucket, authzTable)
assert.Equals(t, []byte(az.getID()), key)
return nil, true, nil
},
},
}
},
"ok/old-not-nil": func(t *testing.T) test {
oldAz, err := newAz()
assert.FatalError(t, err)
az, err := newAz()
assert.FatalError(t, err)
oldb, err := json.Marshal(oldAz)
assert.FatalError(t, err)
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
az: az,
old: oldAz,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
assert.Equals(t, old, oldb)
assert.Equals(t, b, newval)
assert.Equals(t, bucket, authzTable)
assert.Equals(t, []byte(az.getID()), key)
return []byte("foo"), true, nil
},
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
if err := tc.az.save(tc.db, tc.old); err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestAuthzUnmarshal(t *testing.T) {
type test struct {
az authz
azb []byte
err *Error
}
tests := map[string]func(t *testing.T) test{
"fail/nil": func(t *testing.T) test {
return test{
azb: nil,
err: ServerInternalErr(errors.New("error unmarshaling authz type: unexpected end of JSON input")),
}
},
"fail/unexpected-type": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Identifier.Type = "foo"
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
azb: b,
err: ServerInternalErr(errors.New("unexpected authz type foo")),
}
},
"ok/dns": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
b, err := json.Marshal(az)
assert.FatalError(t, err)
return test{
az: az,
azb: b,
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
if az, err := unmarshalAuthz(tc.azb); err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, tc.az.getID(), az.getID())
assert.Equals(t, tc.az.getAccountID(), az.getAccountID())
assert.Equals(t, tc.az.getStatus(), az.getStatus())
assert.Equals(t, tc.az.getCreated(), az.getCreated())
assert.Equals(t, tc.az.getExpiry(), az.getExpiry())
assert.Equals(t, tc.az.getWildcard(), az.getWildcard())
assert.Equals(t, tc.az.getChallenges(), az.getChallenges())
}
}
})
}
}
func TestAuthzUpdateStatus(t *testing.T) {
type test struct {
az, res authz
err *Error
db nosql.DB
}
tests := map[string]func(t *testing.T) test{
"fail/already-invalid": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Status = StatusInvalid
return test{
az: az,
res: az,
}
},
"fail/already-valid": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Status = StatusValid
return test{
az: az,
res: az,
}
},
"fail/unexpected-status": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Status = StatusReady
return test{
az: az,
res: az,
err: ServerInternalErr(errors.New("unrecognized authz status: ready")),
}
},
"fail/save-error": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Expires = time.Now().UTC().Add(-time.Minute)
return test{
az: az,
res: az,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return nil, false, errors.New("force")
},
},
err: ServerInternalErr(errors.New("error storing authz: force")),
}
},
"ok/expired": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Expires = time.Now().UTC().Add(-time.Minute)
clone := az.clone()
clone.Error = MalformedErr(errors.New("authz has expired"))
clone.Status = StatusInvalid
return test{
az: az,
res: clone.parent(),
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return nil, true, nil
},
},
}
},
"fail/get-challenge-error": func(t *testing.T) test {
az, err := newAz()
assert.FatalError(t, err)
return test{
az: az,
res: az,
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
return nil, errors.New("force")
},
},
err: ServerInternalErr(errors.New("error loading challenge")),
}
},
"ok/valid": func(t *testing.T) test {
var (
ch3 challenge
ch2Bytes = &([]byte{})
ch1Bytes = &([]byte{})
err error
)
count := 0
mockdb := &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 0 {
*ch1Bytes = newval
} else if count == 1 {
*ch2Bytes = newval
} else if count == 2 {
ch3, err = unmarshalChallenge(newval)
assert.FatalError(t, err)
}
count++
return nil, true, nil
},
}
iden := Identifier{
Type: "dns", Value: "acme.example.com",
}
az, err := newAuthz(mockdb, "1234", iden)
assert.FatalError(t, err)
_az, ok := az.(*dnsAuthz)
assert.Fatal(t, ok)
_az.baseAuthz.Error = MalformedErr(nil)
_ch, ok := ch3.(*dns01Challenge)
assert.Fatal(t, ok)
_ch.baseChallenge.Status = StatusValid
chb, err := json.Marshal(ch3)
clone := az.clone()
clone.Status = StatusValid
clone.Error = nil
count = 0
return test{
az: az,
res: clone.parent(),
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
if count == 0 {
count++
return *ch1Bytes, nil
}
if count == 1 {
count++
return *ch2Bytes, nil
}
count++
return chb, nil
},
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
return nil, true, nil
},
},
}
},
"ok/still-pending": func(t *testing.T) test {
var ch1Bytes, ch2Bytes = &([]byte{}), &([]byte{})
count := 0
mockdb := &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 0 {
*ch1Bytes = newval
} else if count == 1 {
*ch2Bytes = newval
}
count++
return nil, true, nil
},
}
iden := Identifier{
Type: "dns", Value: "acme.example.com",
}
az, err := newAuthz(mockdb, "1234", iden)
assert.FatalError(t, err)
count = 0
return test{
az: az,
res: az,
db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
if count == 0 {
count++
return *ch1Bytes, nil
}
count++
return *ch2Bytes, nil
},
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
az, err := tc.az.updateStatus(tc.db)
if err != nil {
if assert.NotNil(t, tc.err) {
ae, ok := err.(*Error)
assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type)
}
} else {
if assert.Nil(t, tc.err) {
expB, err := json.Marshal(tc.res)
assert.FatalError(t, err)
b, err := json.Marshal(az)
assert.FatalError(t, err)
assert.Equals(t, expB, b)
}
}
})
}
}