forked from TrueCloudLab/certificates
2025 lines
55 KiB
Go
2025 lines
55 KiB
Go
package acme
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/assert"
|
|
"github.com/smallstep/certificates/db"
|
|
"github.com/smallstep/cli/jose"
|
|
"github.com/smallstep/nosql"
|
|
"github.com/smallstep/nosql/database"
|
|
)
|
|
|
|
var testOps = ChallengeOptions{
|
|
AccountID: "accID",
|
|
AuthzID: "authzID",
|
|
Identifier: Identifier{
|
|
Type: "", // will get set correctly depending on the "new.." method.
|
|
Value: "zap.internal",
|
|
},
|
|
}
|
|
|
|
func newDNSCh() (challenge, error) {
|
|
mockdb := &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
}
|
|
return newDNS01Challenge(mockdb, testOps)
|
|
}
|
|
|
|
func newTLSALPNCh() (challenge, error) {
|
|
mockdb := &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
}
|
|
return newTLSALPN01Challenge(mockdb, testOps)
|
|
}
|
|
|
|
func newHTTPCh() (challenge, error) {
|
|
mockdb := &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
}
|
|
return newHTTP01Challenge(mockdb, testOps)
|
|
}
|
|
|
|
func TestNewHTTP01Challenge(t *testing.T) {
|
|
ops := ChallengeOptions{
|
|
AccountID: "accID",
|
|
AuthzID: "authzID",
|
|
Identifier: Identifier{
|
|
Type: "http",
|
|
Value: "zap.internal",
|
|
},
|
|
}
|
|
type test struct {
|
|
ops ChallengeOptions
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]test{
|
|
"fail/store-error": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return nil, false, errors.New("force")
|
|
},
|
|
},
|
|
err: ServerInternalErr(errors.New("error saving acme challenge: force")),
|
|
},
|
|
"ok": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ch, err := newHTTP01Challenge(tc.db, tc.ops)
|
|
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, ch.getAccountID(), ops.AccountID)
|
|
assert.Equals(t, ch.getAuthzID(), ops.AuthzID)
|
|
assert.Equals(t, ch.getType(), "http-01")
|
|
assert.Equals(t, ch.getValue(), "zap.internal")
|
|
assert.Equals(t, ch.getStatus(), StatusPending)
|
|
|
|
assert.True(t, ch.getValidated().IsZero())
|
|
assert.True(t, ch.getCreated().Before(time.Now().UTC().Add(time.Minute)))
|
|
assert.True(t, ch.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
|
|
|
|
assert.True(t, ch.getID() != "")
|
|
assert.True(t, ch.getToken() != "")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewTLSALPN01Challenge(t *testing.T) {
|
|
ops := ChallengeOptions{
|
|
AccountID: "accID",
|
|
AuthzID: "authzID",
|
|
Identifier: Identifier{
|
|
Type: "http",
|
|
Value: "zap.internal",
|
|
},
|
|
}
|
|
type test struct {
|
|
ops ChallengeOptions
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]test{
|
|
"fail/store-error": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return nil, false, errors.New("force")
|
|
},
|
|
},
|
|
err: ServerInternalErr(errors.New("error saving acme challenge: force")),
|
|
},
|
|
"ok": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ch, err := newTLSALPN01Challenge(tc.db, tc.ops)
|
|
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, ch.getAccountID(), ops.AccountID)
|
|
assert.Equals(t, ch.getAuthzID(), ops.AuthzID)
|
|
assert.Equals(t, ch.getType(), "tls-alpn-01")
|
|
assert.Equals(t, ch.getValue(), "zap.internal")
|
|
assert.Equals(t, ch.getStatus(), StatusPending)
|
|
|
|
assert.True(t, ch.getValidated().IsZero())
|
|
assert.True(t, ch.getCreated().Before(time.Now().UTC().Add(time.Minute)))
|
|
assert.True(t, ch.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
|
|
|
|
assert.True(t, ch.getID() != "")
|
|
assert.True(t, ch.getToken() != "")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewDNS01Challenge(t *testing.T) {
|
|
ops := ChallengeOptions{
|
|
AccountID: "accID",
|
|
AuthzID: "authzID",
|
|
Identifier: Identifier{
|
|
Type: "dns",
|
|
Value: "zap.internal",
|
|
},
|
|
}
|
|
type test struct {
|
|
ops ChallengeOptions
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]test{
|
|
"fail/store-error": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return nil, false, errors.New("force")
|
|
},
|
|
},
|
|
err: ServerInternalErr(errors.New("error saving acme challenge: force")),
|
|
},
|
|
"ok": {
|
|
ops: ops,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
return []byte("foo"), true, nil
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ch, err := newDNS01Challenge(tc.db, tc.ops)
|
|
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, ch.getAccountID(), ops.AccountID)
|
|
assert.Equals(t, ch.getAuthzID(), ops.AuthzID)
|
|
assert.Equals(t, ch.getType(), "dns-01")
|
|
assert.Equals(t, ch.getValue(), "zap.internal")
|
|
assert.Equals(t, ch.getStatus(), StatusPending)
|
|
|
|
assert.True(t, ch.getValidated().IsZero())
|
|
assert.True(t, ch.getCreated().Before(time.Now().UTC().Add(time.Minute)))
|
|
assert.True(t, ch.getCreated().After(time.Now().UTC().Add(-1*time.Minute)))
|
|
|
|
assert.True(t, ch.getID() != "")
|
|
assert.True(t, ch.getToken() != "")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChallengeToACME_Valid(t *testing.T) {
|
|
dir := newDirectory("ca.smallstep.com", "acme")
|
|
|
|
n := clock.Now()
|
|
|
|
fns := []func() (challenge, error){
|
|
newDNSCh,
|
|
newHTTPCh,
|
|
newTLSALPNCh,
|
|
}
|
|
chs := make([]challenge, 3)
|
|
|
|
for i, f := range fns {
|
|
ch, err := f()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Validated = n
|
|
chs[i] = b.morph()
|
|
}
|
|
|
|
prov := newProv()
|
|
provName := url.PathEscape(prov.GetName())
|
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
|
ctx := context.WithValue(context.Background(), ProvisionerContextKey, prov)
|
|
ctx = context.WithValue(ctx, BaseURLContextKey, baseURL)
|
|
tests := map[string]challenge{
|
|
"dns": chs[0],
|
|
"http": chs[1],
|
|
"tls-alpn": chs[2],
|
|
}
|
|
|
|
for name, ch := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ach, err := ch.toACME(ctx, dir)
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, ach.Type, ch.getType())
|
|
assert.Equals(t, ach.Status, ch.getStatus())
|
|
assert.Equals(t, ach.Token, ch.getToken())
|
|
assert.Equals(t, ach.URL,
|
|
fmt.Sprintf("%s/acme/%s/challenge/%s",
|
|
baseURL.String(), provName, ch.getID()))
|
|
assert.Equals(t, ach.ID, ch.getID())
|
|
assert.Equals(t, ach.AuthzID, ch.getAuthzID())
|
|
|
|
v, err := time.Parse(time.RFC3339, ach.Validated)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, v, ch.getValidated())
|
|
|
|
assert.Equals(t, ach.RetryAfter, "")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChallengeToACME_Retry(t *testing.T) {
|
|
dir := newDirectory("example.com", "acme")
|
|
|
|
n := clock.Now()
|
|
|
|
fns := []func() (challenge, error){
|
|
newDNSCh,
|
|
newHTTPCh,
|
|
newTLSALPNCh,
|
|
}
|
|
states := []*Retry{
|
|
nil,
|
|
{NextAttempt: n.Format(time.RFC3339)},
|
|
}
|
|
chs := make([]challenge, len(fns)*len(states))
|
|
|
|
for i, s := range states {
|
|
for j, f := range fns {
|
|
ch, err := f()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = "processing"
|
|
b.Retry = s
|
|
chs[j+i*len(fns)] = b.morph()
|
|
}
|
|
}
|
|
|
|
prov := newProv()
|
|
provName := url.PathEscape(prov.GetName())
|
|
baseURL := &url.URL{Scheme: "https", Host: "example.com"}
|
|
ctx := context.WithValue(context.Background(), ProvisionerContextKey, prov)
|
|
ctx = context.WithValue(ctx, BaseURLContextKey, baseURL)
|
|
|
|
tests := map[string]challenge{
|
|
"dns_no-retry": chs[0+0*len(fns)],
|
|
"http_no-retry": chs[1+0*len(fns)],
|
|
"tls-alpn_no-retry": chs[2+0*len(fns)],
|
|
"dns_retry": chs[0+1*len(fns)],
|
|
"http_retry": chs[1+1*len(fns)],
|
|
"tls_alpn_retry": chs[2+1*len(fns)],
|
|
}
|
|
for name, ch := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ach, err := ch.toACME(ctx, dir)
|
|
assert.FatalError(t, err)
|
|
|
|
assert.Equals(t, ach.Type, ch.getType())
|
|
assert.Equals(t, ach.Status, ch.getStatus())
|
|
assert.Equals(t, ach.Token, ch.getToken())
|
|
assert.Equals(t, ach.URL,
|
|
fmt.Sprintf("%s/acme/%s/challenge/%s",
|
|
baseURL.String(), provName, ch.getID()))
|
|
assert.Equals(t, ach.ID, ch.getID())
|
|
assert.Equals(t, ach.AuthzID, ch.getAuthzID())
|
|
|
|
assert.Equals(t, ach.Validated, "")
|
|
if ch.getRetry() != nil {
|
|
assert.Equals(t, ach.RetryAfter, ch.getRetry().NextAttempt)
|
|
} else {
|
|
assert.Equals(t, ach.RetryAfter, "")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChallengeSave(t *testing.T) {
|
|
type test struct {
|
|
ch challenge
|
|
old challenge
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/old-nil/swap-error": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
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 saving acme challenge: force")),
|
|
}
|
|
},
|
|
"fail/old-nil/swap-false": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
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 saving acme challenge; acme challenge has changed since last read")),
|
|
}
|
|
},
|
|
"ok/old-nil": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(httpCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
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, challengeTable)
|
|
assert.Equals(t, []byte(httpCh.getID()), key)
|
|
return []byte("foo"), true, nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"ok/old-not-nil": func(t *testing.T) test {
|
|
oldHTTPCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
|
|
oldb, err := json.Marshal(oldHTTPCh)
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(httpCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
old: oldHTTPCh,
|
|
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, challengeTable)
|
|
assert.Equals(t, []byte(httpCh.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.ch.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 TestChallengeClone(t *testing.T) {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
|
|
clone := ch.clone()
|
|
|
|
assert.Equals(t, clone.getID(), ch.getID())
|
|
assert.Equals(t, clone.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, clone.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, clone.getStatus(), ch.getStatus())
|
|
assert.Equals(t, clone.getToken(), ch.getToken())
|
|
assert.Equals(t, clone.getCreated(), ch.getCreated())
|
|
assert.Equals(t, clone.getValidated(), ch.getValidated())
|
|
|
|
clone.Status = StatusValid
|
|
|
|
assert.NotEquals(t, clone.getStatus(), ch.getStatus())
|
|
}
|
|
|
|
func TestChallengeUnmarshal(t *testing.T) {
|
|
type test struct {
|
|
ch challenge
|
|
chb []byte
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/nil": func(t *testing.T) test {
|
|
return test{
|
|
chb: nil,
|
|
err: ServerInternalErr(errors.New("error unmarshaling challenge type: unexpected end of JSON input")),
|
|
}
|
|
},
|
|
"fail/unexpected-type-http": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
_httpCh, ok := httpCh.(*http01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_httpCh.baseChallenge.Type = "foo"
|
|
b, err := json.Marshal(httpCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
chb: b,
|
|
err: ServerInternalErr(errors.New("unexpected challenge type foo")),
|
|
}
|
|
},
|
|
"fail/unexpected-type-alpn": func(t *testing.T) test {
|
|
tlsALPNCh, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
_tlsALPNCh, ok := tlsALPNCh.(*tlsALPN01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_tlsALPNCh.baseChallenge.Type = "foo"
|
|
b, err := json.Marshal(tlsALPNCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
chb: b,
|
|
err: ServerInternalErr(errors.New("unexpected challenge type foo")),
|
|
}
|
|
},
|
|
"fail/unexpected-type-dns": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
_dnsCh, ok := dnsCh.(*dns01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_dnsCh.baseChallenge.Type = "foo"
|
|
b, err := json.Marshal(dnsCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
chb: b,
|
|
err: ServerInternalErr(errors.New("unexpected challenge type foo")),
|
|
}
|
|
},
|
|
"ok/dns": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(dnsCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: dnsCh,
|
|
chb: b,
|
|
}
|
|
},
|
|
"ok/http": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(httpCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
chb: b,
|
|
}
|
|
},
|
|
"ok/alpn": func(t *testing.T) test {
|
|
tlsALPNCh, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(tlsALPNCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: tlsALPNCh,
|
|
chb: b,
|
|
}
|
|
},
|
|
"ok/err": func(t *testing.T) test {
|
|
httpCh, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
_httpCh, ok := httpCh.(*http01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_httpCh.baseChallenge.Error = ServerInternalErr(errors.New("force")).ToACME()
|
|
b, err := json.Marshal(httpCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: httpCh,
|
|
chb: b,
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if ch, err := unmarshalChallenge(tc.chb); 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.ch.getID(), ch.getID())
|
|
assert.Equals(t, tc.ch.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, tc.ch.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, tc.ch.getStatus(), ch.getStatus())
|
|
assert.Equals(t, tc.ch.getToken(), ch.getToken())
|
|
assert.Equals(t, tc.ch.getCreated(), ch.getCreated())
|
|
assert.Equals(t, tc.ch.getValidated(), ch.getValidated())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestGetChallenge(t *testing.T) {
|
|
type test struct {
|
|
id string
|
|
db nosql.DB
|
|
ch challenge
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/not-found": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: dnsCh,
|
|
id: dnsCh.getID(),
|
|
db: &db.MockNoSQLDB{
|
|
MGet: func(bucket, key []byte) ([]byte, error) {
|
|
return nil, database.ErrNotFound
|
|
},
|
|
},
|
|
err: MalformedErr(errors.Errorf("challenge %s not found: not found", dnsCh.getID())),
|
|
}
|
|
},
|
|
"fail/db-error": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: dnsCh,
|
|
id: dnsCh.getID(),
|
|
db: &db.MockNoSQLDB{
|
|
MGet: func(bucket, key []byte) ([]byte, error) {
|
|
return nil, errors.New("force")
|
|
},
|
|
},
|
|
err: ServerInternalErr(errors.Errorf("error loading challenge %s: force", dnsCh.getID())),
|
|
}
|
|
},
|
|
"fail/unmarshal-error": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
_dnsCh, ok := dnsCh.(*dns01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_dnsCh.baseChallenge.Type = "foo"
|
|
b, err := json.Marshal(dnsCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: dnsCh,
|
|
id: dnsCh.getID(),
|
|
db: &db.MockNoSQLDB{
|
|
MGet: func(bucket, key []byte) ([]byte, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(dnsCh.getID()))
|
|
return b, nil
|
|
},
|
|
},
|
|
err: ServerInternalErr(errors.New("unexpected challenge type foo")),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
dnsCh, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b, err := json.Marshal(dnsCh)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
ch: dnsCh,
|
|
id: dnsCh.getID(),
|
|
db: &db.MockNoSQLDB{
|
|
MGet: func(bucket, key []byte) ([]byte, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(dnsCh.getID()))
|
|
return b, nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if ch, err := getChallenge(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.ch.getID(), ch.getID())
|
|
assert.Equals(t, tc.ch.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, tc.ch.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, tc.ch.getStatus(), ch.getStatus())
|
|
assert.Equals(t, tc.ch.getToken(), ch.getToken())
|
|
assert.Equals(t, tc.ch.getCreated(), ch.getCreated())
|
|
assert.Equals(t, tc.ch.getValidated(), ch.getValidated())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKeyAuthorization(t *testing.T) {
|
|
type test struct {
|
|
token string
|
|
jwk *jose.JSONWebKey
|
|
exp string
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/jwk-thumbprint-error": func(t *testing.T) test {
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
jwk.Key = "foo"
|
|
return test{
|
|
token: "1234",
|
|
jwk: jwk,
|
|
err: ServerInternalErr(errors.Errorf("error generating JWK thumbprint: square/go-jose: unknown key type 'string'")),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
token := "1234"
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
thumbprint, err := jwk.Thumbprint(crypto.SHA256)
|
|
assert.FatalError(t, err)
|
|
encPrint := base64.RawURLEncoding.EncodeToString(thumbprint)
|
|
return test{
|
|
token: token,
|
|
jwk: jwk,
|
|
exp: fmt.Sprintf("%s.%s", token, encPrint),
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if ka, err := KeyAuthorization(tc.token, tc.jwk); 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.exp, ka)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type errReader int
|
|
|
|
func (errReader) Read(p []byte) (n int, err error) {
|
|
return 0, errors.New("force")
|
|
}
|
|
func (errReader) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func TestHTTP01Validate(t *testing.T) {
|
|
type test struct {
|
|
vo validateOptions
|
|
ch challenge
|
|
res challenge
|
|
jwk *jose.JSONWebKey
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
|
|
"valid/status-noop": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusValid
|
|
ch = b.morph()
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
|
|
"invalid/status-noop": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusInvalid
|
|
ch = b.morph()
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
|
|
"error/status-pending": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusPending
|
|
e := errors.New("pending challenges must first be moved to the processing state")
|
|
return test{
|
|
ch: b.morph(),
|
|
err: ServerInternalErr(e),
|
|
}
|
|
},
|
|
|
|
"error/status-unknown": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = "unknown"
|
|
e := errors.New("unknown challenge state: unknown")
|
|
return test{
|
|
ch: b.morph(),
|
|
err: ServerInternalErr(e),
|
|
}
|
|
},
|
|
|
|
"ok/http-get-error": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
rch := ch.clone()
|
|
geterr := errors.New("force")
|
|
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", ch.getValue(), ch.getToken())
|
|
e := errors.Wrapf(geterr, "error doing http GET for url %s", url)
|
|
rch.Error = ConnectionErr(e).ToACME()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return nil, geterr
|
|
},
|
|
},
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"processing/http-get->=400": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
rch := ch.clone()
|
|
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", ch.getValue(), ch.getToken())
|
|
e := errors.Errorf("error doing http GET for url %s with status code %d", url, http.StatusBadRequest)
|
|
rch.Error = ConnectionErr(e).ToACME()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return &http.Response{
|
|
Body: ioutil.NopCloser(bytes.NewBufferString("")),
|
|
StatusCode: http.StatusBadRequest,
|
|
}, nil
|
|
},
|
|
},
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"processing/read-body-error": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
rch := ch.clone()
|
|
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", ch.getValue(), ch.getToken())
|
|
e := errors.Wrapf(errors.New("force"), "error reading response body for url %s", url)
|
|
rch.Error = ServerInternalErr(e).ToACME()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return &http.Response{
|
|
Body: errReader(0),
|
|
}, nil
|
|
},
|
|
},
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"error/key-authorization-gen": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
jwk.Key = "foo"
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return &http.Response{
|
|
Body: ioutil.NopCloser(bytes.NewBufferString("foo")),
|
|
}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
err: ServerInternalErr(errors.New("error generating JWK thumbprint: square/go-jose: unknown key type 'string'")),
|
|
}
|
|
},
|
|
|
|
"invalid/key-auth-mismatch": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
|
|
b = ch.clone()
|
|
e := errors.Errorf("keyAuthorization does not match; expected %s, but got foo", expKeyAuth)
|
|
b.Error = IncorrectResponseErr(e).ToACME()
|
|
b.Retry = nil
|
|
b.Status = StatusInvalid
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return &http.Response{
|
|
Body: ioutil.NopCloser(bytes.NewBufferString("foo")),
|
|
}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"valid/normal-http-get": func(t *testing.T) test {
|
|
ch, err := newHTTPCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
|
|
b = ch.clone()
|
|
b.Validated = clock.Now()
|
|
b.Status = StatusValid
|
|
b.Error = nil
|
|
b.Retry = nil
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
httpGet: func(url string) (*http.Response, error) {
|
|
return &http.Response{
|
|
Body: ioutil.NopCloser(bytes.NewBufferString(expKeyAuth)),
|
|
}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
}
|
|
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if ch, err := tc.ch.validate(tc.jwk, tc.vo); 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.res.getID(), ch.getID())
|
|
assert.Equals(t, tc.res.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, tc.res.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, tc.res.getStatus(), ch.getStatus())
|
|
assert.Equals(t, tc.res.getToken(), ch.getToken())
|
|
assert.Equals(t, tc.res.getCreated(), ch.getCreated())
|
|
if tc.res.getValidated() != ch.getValidated() {
|
|
now := clock.Now()
|
|
window := now.Sub(tc.res.getValidated())
|
|
assert.True(t, now.Sub(ch.getValidated()) <= window,
|
|
"validated timestamp should come before now but after test case setup")
|
|
} else {
|
|
assert.Equals(t, tc.res.getValidated(), ch.getValidated())
|
|
}
|
|
assert.Equals(t, tc.res.getError(), ch.getError())
|
|
assert.Equals(t, tc.res.getRetry(), ch.getRetry())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01Validate(t *testing.T) {
|
|
type test struct {
|
|
srv *httptest.Server
|
|
vo validateOptions
|
|
ch challenge
|
|
res challenge
|
|
jwk *jose.JSONWebKey
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"ok/status-already-valid": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
_ch, ok := ch.(*tlsALPN01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_ch.baseChallenge.Status = StatusValid
|
|
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/status-already-invalid": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
_ch, ok := ch.(*tlsALPN01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_ch.baseChallenge.Status = StatusInvalid
|
|
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/tls-dial-error": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := ConnectionErr(errors.Errorf("error doing TLS dial for %v:443: force", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
|
return nil, errors.New("force")
|
|
},
|
|
},
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, newval, newb)
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/timeout": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := ConnectionErr(errors.Errorf("error doing TLS dial for %v:443: tls: DialWithDialer timed out", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(nil)
|
|
// srv.Start() - do not start server to cause timeout
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/no-certificates": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.Errorf("tls-alpn-01 challenge for %v resulted in no certificates", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
|
return tls.Client(&noopConn{}, config), nil
|
|
},
|
|
},
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/no-names": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single DNS name, %v", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true)
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/too-many-names": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single DNS name, %v", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.getValue(), "other.internal")
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/wrong-name": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single DNS name, %v", ch.getValue()))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, "other.internal")
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/no-extension": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.New("incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension"))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
cert, err := newTLSALPNValidationCert(nil, false, true, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/extension-not-critical": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.New("incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical"))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, false, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/extension-malformed": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.New("incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value"))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
cert, err := newTLSALPNValidationCert([]byte{1, 2, 3}, false, true, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/no-protocol": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.New("cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge"))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
srv := httptest.NewTLSServer(nil)
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
|
return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config)
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/mismatched-token": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
incorrectTokenHash := sha256.Sum256([]byte("mismatched"))
|
|
|
|
expErr := RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
|
"expected acmeValidationV1 extension value %s for this challenge but got %s",
|
|
hex.EncodeToString(expKeyAuthHash[:]), hex.EncodeToString(incorrectTokenHash[:])))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
cert, err := newTLSALPNValidationCert(incorrectTokenHash[:], false, true, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok/obsolete-oid": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expErr := RejectedIdentifierErr(errors.New("incorrect certificate for tls-alpn-01 challenge: " +
|
|
"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension"))
|
|
baseClone := ch.clone()
|
|
baseClone.Error = expErr.ToACME()
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
newb, err := json.Marshal(newCh)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], true, true, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: tlsDial,
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
assert.Equals(t, string(newval), string(newb))
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: ch,
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
ch, err := newTLSALPNCh()
|
|
assert.FatalError(t, err)
|
|
_ch, ok := ch.(*tlsALPN01Challenge)
|
|
assert.Fatal(t, ok)
|
|
_ch.baseChallenge.Error = MalformedErr(nil).ToACME()
|
|
oldb, err := json.Marshal(ch)
|
|
assert.FatalError(t, err)
|
|
|
|
baseClone := ch.clone()
|
|
baseClone.Status = StatusValid
|
|
baseClone.Error = nil
|
|
newCh := &tlsALPN01Challenge{baseClone}
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
|
|
|
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.getValue())
|
|
assert.FatalError(t, err)
|
|
|
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
|
srv.Start()
|
|
|
|
return test{
|
|
srv: srv,
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
tlsDial: func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) {
|
|
assert.Equals(t, network, "tcp")
|
|
assert.Equals(t, addr, net.JoinHostPort(newCh.getValue(), "443"))
|
|
assert.Equals(t, config.NextProtos, []string{"acme-tls/1"})
|
|
assert.Equals(t, config.ServerName, newCh.getValue())
|
|
assert.True(t, config.InsecureSkipVerify)
|
|
|
|
return tlsDial(network, addr, config)
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
db: &db.MockNoSQLDB{
|
|
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
|
assert.Equals(t, bucket, challengeTable)
|
|
assert.Equals(t, key, []byte(ch.getID()))
|
|
assert.Equals(t, old, oldb)
|
|
|
|
alpnCh, err := unmarshalChallenge(newval)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, alpnCh.getStatus(), StatusValid)
|
|
assert.True(t, alpnCh.getValidated().Before(time.Now().UTC().Add(time.Minute)))
|
|
assert.True(t, alpnCh.getValidated().After(time.Now().UTC().Add(-1*time.Second)))
|
|
|
|
baseClone.Validated = alpnCh.getValidated()
|
|
|
|
return nil, true, nil
|
|
},
|
|
},
|
|
res: newCh,
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
|
|
if tc.srv != nil {
|
|
defer tc.srv.Close()
|
|
}
|
|
|
|
if ch, err := tc.ch.validate(tc.jwk, tc.vo); 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.res.getID(), ch.getID())
|
|
assert.Equals(t, tc.res.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, tc.res.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, tc.res.getStatus(), ch.getStatus())
|
|
assert.Equals(t, tc.res.getToken(), ch.getToken())
|
|
assert.Equals(t, tc.res.getCreated(), ch.getCreated())
|
|
assert.Equals(t, tc.res.getValidated(), ch.getValidated())
|
|
assert.Equals(t, tc.res.getError(), ch.getError())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tlsDialer) {
|
|
srv := httptest.NewUnstartedServer(http.NewServeMux())
|
|
|
|
srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
|
|
"acme-tls/1": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
|
// no-op
|
|
},
|
|
"http/1.1": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
|
panic("unexpected http/1.1 next proto")
|
|
},
|
|
}
|
|
|
|
srv.TLS = &tls.Config{
|
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == "acme-tls/1" {
|
|
return validationCert, nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
NextProtos: []string{
|
|
"acme-tls/1",
|
|
"http/1.1",
|
|
},
|
|
}
|
|
|
|
srv.Listener = tls.NewListener(srv.Listener, srv.TLS)
|
|
//srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush
|
|
|
|
return srv, func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) {
|
|
return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config)
|
|
}
|
|
}
|
|
|
|
// noopConn is a mock net.Conn that does nothing.
|
|
type noopConn struct{}
|
|
|
|
func (c *noopConn) Read(_ []byte) (n int, err error) { return 0, io.EOF }
|
|
func (c *noopConn) Write(_ []byte) (n int, err error) { return 0, io.EOF }
|
|
func (c *noopConn) Close() error { return nil }
|
|
func (c *noopConn) LocalAddr() net.Addr { return &net.IPAddr{IP: net.IPv4zero, Zone: ""} }
|
|
func (c *noopConn) RemoteAddr() net.Addr { return &net.IPAddr{IP: net.IPv4zero, Zone: ""} }
|
|
func (c *noopConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *noopConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *noopConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
func newTLSALPNValidationCert(keyAuthHash []byte, obsoleteOID, critical bool, names ...string) (*tls.Certificate, error) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certTemplate := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1337),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"Test"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(0, 0, 1),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
DNSNames: names,
|
|
}
|
|
|
|
if keyAuthHash != nil {
|
|
oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
|
if obsoleteOID {
|
|
oid = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
|
|
}
|
|
|
|
keyAuthHashEnc, _ := asn1.Marshal(keyAuthHash[:])
|
|
|
|
certTemplate.ExtraExtensions = []pkix.Extension{
|
|
{
|
|
Id: oid,
|
|
Critical: critical,
|
|
Value: keyAuthHashEnc,
|
|
},
|
|
}
|
|
}
|
|
|
|
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, privateKey.Public(), privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tls.Certificate{
|
|
PrivateKey: privateKey,
|
|
Certificate: [][]byte{cert},
|
|
}, nil
|
|
}
|
|
|
|
func TestDNS01Validate(t *testing.T) {
|
|
type test struct {
|
|
vo validateOptions
|
|
ch challenge
|
|
res challenge
|
|
jwk *jose.JSONWebKey
|
|
db nosql.DB
|
|
err *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
|
|
"valid/status-noop": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusValid
|
|
ch = b.morph()
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
|
|
"invalid/status-noop": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusInvalid
|
|
ch = b.morph()
|
|
return test{
|
|
ch: ch,
|
|
res: ch,
|
|
}
|
|
},
|
|
|
|
"error/status-pending": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusPending
|
|
e := errors.New("pending challenges must first be moved to the processing state")
|
|
return test{
|
|
ch: b.morph(),
|
|
err: ServerInternalErr(e),
|
|
}
|
|
},
|
|
|
|
"error/status-unknown": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = "unknown"
|
|
e := errors.New("unknown challenge state: unknown")
|
|
return test{
|
|
ch: b.morph(),
|
|
err: ServerInternalErr(e),
|
|
}
|
|
},
|
|
|
|
"processing/lookup-txt-error": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
b = ch.clone()
|
|
e := errors.Errorf("error looking up TXT records for domain %s: force", ch.getValue())
|
|
b.Error = DNSErr(e).ToACME()
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
return nil, errors.New("force")
|
|
},
|
|
},
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"fail/key-authorization-gen-error": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
jwk.Key = "foo"
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
return []string{"foo", "bar"}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
err: ServerInternalErr(errors.New("error generating JWK thumbprint: square/go-jose: unknown key type 'string'")),
|
|
}
|
|
},
|
|
|
|
"invalid/key-auth-mismatch": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
|
|
e := errors.Errorf("keyAuthorization does not match; "+
|
|
"expected %s, but got %s", expKeyAuth, []string{"foo", "bar"})
|
|
b = ch.clone()
|
|
b.Status = StatusInvalid
|
|
b.Error = IncorrectResponseErr(e).ToACME()
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
return []string{"foo", "bar"}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"processing/empty-list": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
e := errors.New("no TXT record found at '_acme-challenge.zap.internal'")
|
|
b = ch.clone()
|
|
b.Error = DNSErr(e).ToACME()
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
return []string{}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"valid/lookup-txt-normal": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
h := sha256.Sum256([]byte(expKeyAuth))
|
|
expected := base64.RawURLEncoding.EncodeToString(h[:])
|
|
|
|
b = ch.clone()
|
|
b.Validated = clock.Now()
|
|
b.Status = StatusValid
|
|
b.Error = nil
|
|
b.Retry = nil
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
return []string{"foo", expected}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
|
|
"valid/lookup-txt-wildcard": func(t *testing.T) test {
|
|
ch, err := newDNSCh()
|
|
assert.FatalError(t, err)
|
|
b := ch.clone()
|
|
b.Status = StatusProcessing
|
|
b.Value = "*.zap.internal"
|
|
ch = b.morph()
|
|
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
|
|
expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk)
|
|
assert.FatalError(t, err)
|
|
h := sha256.Sum256([]byte(expKeyAuth))
|
|
expected := base64.RawURLEncoding.EncodeToString(h[:])
|
|
|
|
b = ch.clone()
|
|
b.Status = StatusValid
|
|
b.Validated = clock.Now()
|
|
b.Error = nil
|
|
b.Retry = nil
|
|
rch := b.morph()
|
|
|
|
return test{
|
|
ch: ch,
|
|
vo: validateOptions{
|
|
lookupTxt: func(url string) ([]string, error) {
|
|
assert.Equals(t, url, "_acme-challenge.zap.internal")
|
|
return []string{"foo", expected}, nil
|
|
},
|
|
},
|
|
jwk: jwk,
|
|
res: rch,
|
|
}
|
|
},
|
|
}
|
|
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if ch, err := tc.ch.validate(tc.jwk, tc.vo); 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.res.getID(), ch.getID())
|
|
assert.Equals(t, tc.res.getAccountID(), ch.getAccountID())
|
|
assert.Equals(t, tc.res.getAuthzID(), ch.getAuthzID())
|
|
assert.Equals(t, tc.res.getStatus(), ch.getStatus())
|
|
assert.Equals(t, tc.res.getToken(), ch.getToken())
|
|
assert.Equals(t, tc.res.getCreated(), ch.getCreated())
|
|
if tc.res.getValidated() != ch.getValidated() {
|
|
now := clock.Now()
|
|
window := now.Sub(tc.res.getValidated())
|
|
assert.True(t, now.Sub(ch.getValidated()) <= window,
|
|
"validated timestamp should come before now but after test case setup")
|
|
} else {
|
|
assert.Equals(t, tc.res.getValidated(), ch.getValidated())
|
|
}
|
|
assert.Equals(t, tc.res.getError(), ch.getError())
|
|
assert.Equals(t, tc.res.getRetry(), ch.getRetry())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|