Merge pull request #180 from ibrt/master

Support for ALPN-01 Challenge
This commit is contained in:
Max 2020-02-11 10:53:14 -08:00 committed by GitHub
commit 2c747fc11f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1160 additions and 43 deletions

1
.gitignore vendored
View file

@ -18,3 +18,4 @@
coverage.txt
vendor
output
.idea

View file

@ -50,7 +50,7 @@ It's super easy to get started and to operate `step-ca` thanks to [streamlined i
### [Your own private ACME Server](https://smallstep.com/blog/private-acme-server/)
- Issue certificates using ACMEv2 ([RFC8555](https://tools.ietf.org/html/rfc8555)), **the protocol used by Let's Encrypt**
- Great for [using ACME in development & pre-production](https://smallstep.com/blog/private-acme-server/#local-development-pre-production)
- Supports the `http-01` and `dns-01` ACME challenge types
- Supports the `http-01`, `tls-alpn-01`, and `dns-01` ACME challenge types
- Works with any compliant ACME client including [certbot](https://smallstep.com/blog/private-acme-server/#certbot-uploads-acme-certbot-png-certbot-example), [acme.sh](https://smallstep.com/blog/private-acme-server/#acme-sh-uploads-acme-acme-sh-png-acme-sh-example), [Caddy](https://smallstep.com/blog/private-acme-server/#caddy-uploads-acme-caddy-png-caddy-example), and [traefik](https://smallstep.com/blog/private-acme-server/#traefik-uploads-acme-traefik-png-traefik-example)
- Get certificates programmatically (e.g., in [Go](https://smallstep.com/blog/private-acme-server/#golang-uploads-acme-golang-png-go-example), [Python](https://smallstep.com/blog/private-acme-server/#python-uploads-acme-python-png-python-example), [Node.js](https://smallstep.com/blog/private-acme-server/#node-js-uploads-acme-node-js-png-node-js-example))

View file

@ -2,6 +2,7 @@ package acme
import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"net"
@ -265,9 +266,15 @@ func (a *Authority) ValidateChallenge(p provisioner.Interface, accID, chID strin
client := http.Client{
Timeout: time.Duration(30 * time.Second),
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
}
ch, err = ch.validate(a.db, jwk, validateOptions{
httpGet: client.Get,
lookupTxt: net.LookupTXT,
tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
return tls.DialWithDialer(dialer, network, addr, config)
},
})
if err != nil {
return nil, Wrap(err, "error attempting challenge validation")

View file

@ -730,7 +730,7 @@ func TestAuthorityGetAuthz(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
var ch1B, ch2B = &[]byte{}, &[]byte{}
var ch1B, ch2B, ch3B = &[]byte{}, &[]byte{}, &[]byte{}
count := 0
mockdb := &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
@ -739,6 +739,8 @@ func TestAuthorityGetAuthz(t *testing.T) {
*ch1B = newval
case 1:
*ch2B = newval
case 2:
*ch3B = newval
}
count++
return nil, true, nil
@ -758,6 +760,8 @@ func TestAuthorityGetAuthz(t *testing.T) {
assert.FatalError(t, err)
ch2, err := unmarshalChallenge(*ch2B)
assert.FatalError(t, err)
ch3, err := unmarshalChallenge(*ch3B)
assert.FatalError(t, err)
count = 0
mockdb = &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
@ -771,6 +775,10 @@ func TestAuthorityGetAuthz(t *testing.T) {
assert.Equals(t, bucket, challengeTable)
assert.Equals(t, key, []byte(ch2.getID()))
ret = *ch2B
case 2:
assert.Equals(t, bucket, challengeTable)
assert.Equals(t, key, []byte(ch3.getID()))
ret = *ch3B
}
count++
return ret, nil
@ -796,6 +804,10 @@ func TestAuthorityGetAuthz(t *testing.T) {
assert.Equals(t, bucket, challengeTable)
assert.Equals(t, key, []byte(ch2.getID()))
ret = *ch2B
case 3:
assert.Equals(t, bucket, challengeTable)
assert.Equals(t, key, []byte(ch3.getID()))
ret = *ch3B
}
count++
return ret, nil
@ -876,21 +888,25 @@ func TestAuthorityNewOrder(t *testing.T) {
case 1:
assert.Equals(t, bucket, challengeTable)
case 2:
assert.Equals(t, bucket, authzTable)
case 3:
assert.Equals(t, bucket, challengeTable)
case 3:
assert.Equals(t, bucket, authzTable)
case 4:
assert.Equals(t, bucket, challengeTable)
case 5:
assert.Equals(t, bucket, authzTable)
assert.Equals(t, bucket, challengeTable)
case 6:
assert.Equals(t, bucket, challengeTable)
case 7:
assert.Equals(t, bucket, authzTable)
case 8:
assert.Equals(t, bucket, orderTable)
var o order
assert.FatalError(t, json.Unmarshal(newval, &o))
*acmeO, err = o.toACME(nil, dir, prov)
assert.FatalError(t, err)
*accID = o.AccountID
case 7:
case 9:
assert.Equals(t, bucket, ordersByAccountIDTable)
assert.Equals(t, string(key), *accID)
}

View file

@ -294,7 +294,7 @@ func newDNSAuthz(db nosql.DB, accID string, identifier Identifier) (authz, error
ba.Challenges = []string{}
if !ba.Wildcard {
// http challenges are only permitted if the DNS is not a wildcard dns.
// http and alpn challenges are only permitted if the DNS is not a wildcard dns.
ch1, err := newHTTP01Challenge(db, ChallengeOptions{
AccountID: accID,
AuthzID: ba.ID,
@ -303,15 +303,25 @@ func newDNSAuthz(db nosql.DB, accID string, identifier Identifier) (authz, error
return nil, Wrap(err, "error creating http challenge")
}
ba.Challenges = append(ba.Challenges, ch1.getID())
ch2, err := newTLSALPN01Challenge(db, ChallengeOptions{
AccountID: accID,
AuthzID: ba.ID,
Identifier: ba.Identifier,
})
if err != nil {
return nil, Wrap(err, "error creating alpn challenge")
}
ba.Challenges = append(ba.Challenges, ch2.getID())
}
ch2, err := newDNS01Challenge(db, ChallengeOptions{
ch3, err := newDNS01Challenge(db, ChallengeOptions{
AccountID: accID,
AuthzID: ba.ID,
Identifier: identifier})
if err != nil {
return nil, Wrap(err, "error creating dns challenge")
}
ba.Challenges = append(ba.Challenges, ch2.getID())
ba.Challenges = append(ba.Challenges, ch3.getID())
da := &dnsAuthz{ba}
if err := da.save(db, nil); err != nil {

View file

@ -173,7 +173,7 @@ func TestNewAuthz(t *testing.T) {
err: ServerInternalErr(errors.New("error creating http challenge: error saving acme challenge: force")),
}
},
"fail/new-dns-chall-error": func(t *testing.T) test {
"fail/new-tls-alpn-chall-error": func(t *testing.T) test {
count := 0
return test{
iden: iden,
@ -186,6 +186,22 @@ func TestNewAuthz(t *testing.T) {
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")),
}
},
@ -195,7 +211,7 @@ func TestNewAuthz(t *testing.T) {
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 2 {
if count == 3 {
return nil, false, errors.New("force")
}
count++
@ -212,7 +228,7 @@ func TestNewAuthz(t *testing.T) {
iden: iden,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count == 2 {
if count == 3 {
assert.Equals(t, bucket, authzTable)
assert.Equals(t, old, nil)
@ -690,7 +706,8 @@ func TestAuthzUpdateStatus(t *testing.T) {
},
"ok/valid": func(t *testing.T) test {
var (
ch2 challenge
ch3 challenge
ch2Bytes = &([]byte{})
ch1Bytes = &([]byte{})
err error
)
@ -701,7 +718,9 @@ func TestAuthzUpdateStatus(t *testing.T) {
if count == 0 {
*ch1Bytes = newval
} else if count == 1 {
ch2, err = unmarshalChallenge(newval)
*ch2Bytes = newval
} else if count == 2 {
ch3, err = unmarshalChallenge(newval)
assert.FatalError(t, err)
}
count++
@ -717,10 +736,10 @@ func TestAuthzUpdateStatus(t *testing.T) {
assert.Fatal(t, ok)
_az.baseAuthz.Error = MalformedErr(nil)
_ch, ok := ch2.(*dns01Challenge)
_ch, ok := ch3.(*dns01Challenge)
assert.Fatal(t, ok)
_ch.baseChallenge.Status = StatusValid
chb, err := json.Marshal(ch2)
chb, err := json.Marshal(ch3)
clone := az.clone()
clone.Status = StatusValid
@ -736,6 +755,10 @@ func TestAuthzUpdateStatus(t *testing.T) {
count++
return *ch1Bytes, nil
}
if count == 1 {
count++
return *ch2Bytes, nil
}
count++
return chb, nil
},

View file

@ -3,10 +3,15 @@ package acme
import (
"crypto"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
@ -51,10 +56,12 @@ func (c *Challenge) GetAuthzID() string {
type httpGetter func(string) (*http.Response, error)
type lookupTxt func(string) ([]string, error)
type tlsDialer func(network, addr string, config *tls.Config) (*tls.Conn, error)
type validateOptions struct {
httpGet httpGetter
lookupTxt lookupTxt
tlsDial tlsDialer
}
// challenge is the interface ACME challenege types must implement.
@ -258,6 +265,13 @@ func unmarshalChallenge(data []byte) (challenge, error) {
"challenge type into http01Challenge"))
}
return &http01Challenge{&bc}, nil
case "tls-alpn-01":
var bc baseChallenge
if err := json.Unmarshal(data, &bc); err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling "+
"challenge type into tlsALPN01Challenge"))
}
return &tlsALPN01Challenge{&bc}, nil
default:
return nil, ServerInternalErr(errors.Errorf("unexpected challenge type %s", getType.Type))
}
@ -344,6 +358,158 @@ func (hc *http01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo valida
return upd, nil
}
type tlsALPN01Challenge struct {
*baseChallenge
}
// newTLSALPN01Challenge returns a new acme tls-alpn-01 challenge.
func newTLSALPN01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) {
bc, err := newBaseChallenge(ops.AccountID, ops.AuthzID)
if err != nil {
return nil, err
}
bc.Type = "tls-alpn-01"
bc.Value = ops.Identifier.Value
hc := &tlsALPN01Challenge{bc}
if err := hc.save(db, nil); err != nil {
return nil, err
}
return hc, nil
}
func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
// If already valid or invalid then return without performing validation.
if tc.getStatus() == StatusValid || tc.getStatus() == StatusInvalid {
return tc, nil
}
config := &tls.Config{
NextProtos: []string{"acme-tls/1"},
ServerName: tc.Value,
InsecureSkipVerify: true, // we expect a self-signed challenge certificate
}
hostPort := net.JoinHostPort(tc.Value, "443")
conn, err := vo.tlsDial("tcp", hostPort, config)
if err != nil {
if err = tc.storeError(db,
ConnectionErr(errors.Wrapf(err, "error doing TLS dial for %s", hostPort))); err != nil {
return nil, err
}
return tc, nil
}
defer conn.Close()
cs := conn.ConnectionState()
certs := cs.PeerCertificates
if len(certs) == 0 {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("%s challenge for %s resulted in no certificates",
tc.Type, tc.Value))); err != nil {
return nil, err
}
return tc, nil
}
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != "acme-tls/1" {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("cannot negotiate ALPN acme-tls/1 protocol for "+
"tls-alpn-01 challenge"))); err != nil {
return nil, err
}
return tc, nil
}
leafCert := certs[0]
if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], tc.Value) {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"leaf certificate must contain a single DNS name, %v", tc.Value))); err != nil {
return nil, err
}
return tc, nil
}
idPeAcmeIdentifier := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
idPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
foundIDPeAcmeIdentifierV1Obsolete := false
keyAuth, err := KeyAuthorization(tc.Token, jwk)
if err != nil {
return nil, err
}
hashedKeyAuth := sha256.Sum256([]byte(keyAuth))
for _, ext := range leafCert.Extensions {
if idPeAcmeIdentifier.Equal(ext.Id) {
if !ext.Critical {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"acmeValidationV1 extension not critical"))); err != nil {
return nil, err
}
return tc, nil
}
var extValue []byte
rest, err := asn1.Unmarshal(ext.Value, &extValue)
if err != nil || len(rest) > 0 || len(hashedKeyAuth) != len(extValue) {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"malformed acmeValidationV1 extension value"))); err != nil {
return nil, err
}
return tc, nil
}
if subtle.ConstantTimeCompare(hashedKeyAuth[:], extValue) != 1 {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"expected acmeValidationV1 extension value %s for this challenge but got %s",
hex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue)))); err != nil {
return nil, err
}
return tc, nil
}
upd := &tlsALPN01Challenge{tc.baseChallenge.clone()}
upd.Status = StatusValid
upd.Error = nil
upd.Validated = clock.Now()
if err := upd.save(db, tc); err != nil {
return nil, err
}
return upd, nil
}
if idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) {
foundIDPeAcmeIdentifierV1Obsolete = true
}
}
if foundIDPeAcmeIdentifierV1Obsolete {
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension"))); err != nil {
return nil, err
}
return tc, nil
}
if err = tc.storeError(db,
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
"missing acmeValidationV1 extension"))); err != nil {
return nil, err
}
return tc, nil
}
// dns01Challenge represents an dns-01 acme challenge.
type dns01Challenge struct {
*baseChallenge

View file

@ -3,12 +3,23 @@ package acme
import (
"bytes"
"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"
"testing"
"time"
@ -38,6 +49,15 @@ func newDNSCh() (challenge, error) {
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) {
@ -111,6 +131,70 @@ func TestNewHTTP01Challenge(t *testing.T) {
}
}
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",
@ -183,13 +267,16 @@ func TestChallengeToACME(t *testing.T) {
_httpCh, ok := httpCh.(*http01Challenge)
assert.Fatal(t, ok)
_httpCh.baseChallenge.Validated = clock.Now()
dnsCh, err := newDNSCh()
assert.FatalError(t, err)
tlsALPNCh, err := newTLSALPNCh()
assert.FatalError(t, err)
prov := newProv()
tests := map[string]challenge{
"dns": dnsCh,
"http": httpCh,
"dns": dnsCh,
"http": httpCh,
"tls-alpn": tlsALPNCh,
}
for name, ch := range tests {
t.Run(name, func(t *testing.T) {
@ -346,7 +433,7 @@ func TestChallengeUnmarshal(t *testing.T) {
err: ServerInternalErr(errors.New("error unmarshaling challenge type: unexpected end of JSON input")),
}
},
"fail/unexpected-type": func(t *testing.T) test {
"fail/unexpected-type-http": func(t *testing.T) test {
httpCh, err := newHTTPCh()
assert.FatalError(t, err)
_httpCh, ok := httpCh.(*http01Challenge)
@ -359,6 +446,32 @@ func TestChallengeUnmarshal(t *testing.T) {
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)
@ -379,6 +492,16 @@ func TestChallengeUnmarshal(t *testing.T) {
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)
@ -866,6 +989,721 @@ func TestHTTP01Validate(t *testing.T) {
}
}
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.db, 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

View file

@ -325,7 +325,7 @@ func TestNewOrder(t *testing.T) {
ops: defaultOrderOps(),
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count >= 6 {
if count >= 8 {
return nil, false, errors.New("force")
}
count++
@ -342,7 +342,7 @@ func TestNewOrder(t *testing.T) {
ops: ops,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count >= 7 {
if count >= 9 {
return nil, false, errors.New("force")
}
count++
@ -357,7 +357,7 @@ func TestNewOrder(t *testing.T) {
},
"fail/save-orderIDs-error": func(t *testing.T) test {
count := 0
oids := []string{"1", "2"}
oids := []string{"1", "2", "3"}
oidsB, err := json.Marshal(oids)
assert.FatalError(t, err)
var (
@ -369,11 +369,11 @@ func TestNewOrder(t *testing.T) {
ops: ops,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count >= 7 {
if count >= 9 {
assert.Equals(t, bucket, ordersByAccountIDTable)
assert.Equals(t, key, []byte(ops.AccountID))
return nil, false, errors.New("force")
} else if count == 6 {
} else if count == 8 {
*oid = string(key)
}
count++
@ -393,7 +393,7 @@ func TestNewOrder(t *testing.T) {
},
"ok": func(t *testing.T) test {
count := 0
oids := []string{"1", "2"}
oids := []string{"1", "2", "3"}
oidsB, err := json.Marshal(oids)
assert.FatalError(t, err)
authzs := &([]string{})
@ -406,18 +406,18 @@ func TestNewOrder(t *testing.T) {
ops: ops,
db: &db.MockNoSQLDB{
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
if count >= 7 {
if count >= 9 {
assert.Equals(t, bucket, ordersByAccountIDTable)
assert.Equals(t, key, []byte(ops.AccountID))
assert.Equals(t, old, oidsB)
newB, err := json.Marshal(append(oids, *oid))
assert.FatalError(t, err)
assert.Equals(t, newval, newB)
} else if count == 6 {
} else if count == 8 {
*oid = string(key)
} else if count == 5 {
} else if count == 7 {
*authzs = append(*authzs, string(key))
} else if count == 2 {
} else if count == 3 {
*authzs = []string{string(key)}
}
count++
@ -649,29 +649,37 @@ func TestOrderUpdateStatus(t *testing.T) {
assert.FatalError(t, err)
az2, err := newAz()
assert.FatalError(t, err)
az3, err := newAz()
assert.FatalError(t, err)
ch1, err := newHTTPCh()
assert.FatalError(t, err)
ch2, err := newDNSCh()
ch2, err := newTLSALPNCh()
assert.FatalError(t, err)
ch3, err := newDNSCh()
assert.FatalError(t, err)
ch1b, err := json.Marshal(ch1)
assert.FatalError(t, err)
ch2b, err := json.Marshal(ch2)
assert.FatalError(t, err)
ch3b, err := json.Marshal(ch3)
assert.FatalError(t, err)
o, err := newO()
assert.FatalError(t, err)
o.Authorizations = []string{az1.getID(), az2.getID()}
o.Authorizations = []string{az1.getID(), az2.getID(), az3.getID()}
_az2, ok := az2.(*dnsAuthz)
_az3, ok := az3.(*dnsAuthz)
assert.Fatal(t, ok)
_az2.baseAuthz.Status = StatusValid
_az3.baseAuthz.Status = StatusValid
b1, err := json.Marshal(az1)
assert.FatalError(t, err)
b2, err := json.Marshal(az2)
assert.FatalError(t, err)
b3, err := json.Marshal(az3)
assert.FatalError(t, err)
count := 0
return test{
@ -688,7 +696,17 @@ func TestOrderUpdateStatus(t *testing.T) {
case 2:
ret = ch2b
case 3:
ret = ch3b
case 4:
ret = b2
case 5:
ret = ch1b
case 6:
ret = ch2b
case 7:
ret = ch3b
case 8:
ret = b3
default:
return nil, errors.New("unexpected count")
}
@ -706,29 +724,37 @@ func TestOrderUpdateStatus(t *testing.T) {
assert.FatalError(t, err)
az2, err := newAz()
assert.FatalError(t, err)
az3, err := newAz()
assert.FatalError(t, err)
ch1, err := newHTTPCh()
assert.FatalError(t, err)
ch2, err := newDNSCh()
ch2, err := newTLSALPNCh()
assert.FatalError(t, err)
ch3, err := newDNSCh()
assert.FatalError(t, err)
ch1b, err := json.Marshal(ch1)
assert.FatalError(t, err)
ch2b, err := json.Marshal(ch2)
assert.FatalError(t, err)
ch3b, err := json.Marshal(ch3)
assert.FatalError(t, err)
o, err := newO()
assert.FatalError(t, err)
o.Authorizations = []string{az1.getID(), az2.getID()}
o.Authorizations = []string{az1.getID(), az2.getID(), az3.getID()}
_az2, ok := az2.(*dnsAuthz)
_az3, ok := az3.(*dnsAuthz)
assert.Fatal(t, ok)
_az2.baseAuthz.Status = StatusInvalid
_az3.baseAuthz.Status = StatusInvalid
b1, err := json.Marshal(az1)
assert.FatalError(t, err)
b2, err := json.Marshal(az2)
assert.FatalError(t, err)
b3, err := json.Marshal(az3)
assert.FatalError(t, err)
_o := *o
clone := &_o
@ -749,7 +775,17 @@ func TestOrderUpdateStatus(t *testing.T) {
case 2:
ret = ch2b
case 3:
ret = ch3b
case 4:
ret = b2
case 5:
ret = ch1b
case 6:
ret = ch2b
case 7:
ret = ch3b
case 8:
ret = b3
default:
return nil, errors.New("unexpected count")
}
@ -846,29 +882,37 @@ func TestOrderFinalize(t *testing.T) {
assert.FatalError(t, err)
az2, err := newAz()
assert.FatalError(t, err)
az3, err := newAz()
assert.FatalError(t, err)
ch1, err := newHTTPCh()
assert.FatalError(t, err)
ch2, err := newDNSCh()
ch2, err := newTLSALPNCh()
assert.FatalError(t, err)
ch3, err := newDNSCh()
assert.FatalError(t, err)
ch1b, err := json.Marshal(ch1)
assert.FatalError(t, err)
ch2b, err := json.Marshal(ch2)
assert.FatalError(t, err)
ch3b, err := json.Marshal(ch3)
assert.FatalError(t, err)
o, err := newO()
assert.FatalError(t, err)
o.Authorizations = []string{az1.getID(), az2.getID()}
o.Authorizations = []string{az1.getID(), az2.getID(), az3.getID()}
_az2, ok := az2.(*dnsAuthz)
_az3, ok := az3.(*dnsAuthz)
assert.Fatal(t, ok)
_az2.baseAuthz.Status = StatusValid
_az3.baseAuthz.Status = StatusValid
b1, err := json.Marshal(az1)
assert.FatalError(t, err)
b2, err := json.Marshal(az2)
assert.FatalError(t, err)
b3, err := json.Marshal(az3)
assert.FatalError(t, err)
count := 0
return test{
@ -885,7 +929,17 @@ func TestOrderFinalize(t *testing.T) {
case 2:
ret = ch2b
case 3:
ret = ch3b
case 4:
ret = b2
case 5:
ret = ch1b
case 6:
ret = ch2b
case 7:
ret = ch3b
case 8:
ret = b3
default:
return nil, errors.New("unexpected count")
}

View file

@ -23,7 +23,7 @@ func (c *Client) ResolveReference(ref *url.URL) *url.URL {
return c.CaURL.ResolveReference(ref)
}
// LoadStepClient configures an http.Client with the root in
// LoadClient configures an http.Client with the root in
// $STEPPATH/config/defaults.json and the identity defined in
// $STEPPATH/config/identity.json
func LoadClient() (*Client, error) {

View file

@ -118,6 +118,7 @@ func (p *Provisioner) Token(subject string, sans ...string) (string, error) {
return tok.SignedString(p.jwk.Algorithm, p.jwk.Key)
}
// SSHToken generates a SSH token.
func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (string, error) {
jwtID, err := randutil.Hex(64)
if err != nil {

View file

@ -9,6 +9,7 @@ type Step struct {
SSH StepSSH
}
// StepSSH holds SSH-related values for the CA.
type StepSSH struct {
HostKey ssh.PublicKey
UserKey ssh.PublicKey