diff --git a/.gitignore b/.gitignore index ed2ab99d..7cba0d08 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ coverage.txt vendor output +.idea diff --git a/README.md b/README.md index 806d1775..2d1d197b 100644 --- a/README.md +++ b/README.md @@ -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)) diff --git a/acme/authority.go b/acme/authority.go index 286a7218..fe51ea9b 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -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") diff --git a/acme/authority_test.go b/acme/authority_test.go index f3c47966..525a61b9 100644 --- a/acme/authority_test.go +++ b/acme/authority_test.go @@ -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) } diff --git a/acme/authz.go b/acme/authz.go index 27e98051..cdcb15e5 100644 --- a/acme/authz.go +++ b/acme/authz.go @@ -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 { diff --git a/acme/authz_test.go b/acme/authz_test.go index 96213e4f..05e3c40b 100644 --- a/acme/authz_test.go +++ b/acme/authz_test.go @@ -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 }, diff --git a/acme/challenge.go b/acme/challenge.go index f0180f64..d55f42a6 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -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,148 @@ 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 { + // note: it does not seem to be possible to trigger this path, as the Go TLS client will return a dial error + // when no certificate is served + + 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} + + 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) || idPeAcmeIdentifierV1Obsolete.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 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 diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 720321e5..8564824d 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -3,15 +3,28 @@ 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/ioutil" + "log" + "math/big" + "net" "net/http" + "net/http/httptest" "testing" "time" + "golang.org/x/crypto/acme" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/db" @@ -38,6 +51,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 +133,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 +269,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) { @@ -866,6 +955,680 @@ 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 := ConnectionErr(errors.Errorf("error doing TLS dial for %v:443: remote error: tls: internal error", 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() + + 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-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/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/with-new-oid": 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, + } + }, + "ok/with-obsolete-oid": 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[:], 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) + + 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.ALPNProto { + 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) + } +} + +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 diff --git a/acme/order_test.go b/acme/order_test.go index 77c21e24..b0453754 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -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") } diff --git a/go.mod b/go.mod index 004c50d7..5704c45d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 + github.com/davecgh/go-spew v1.1.1 github.com/go-chi/chi v4.0.2+incompatible github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.8.1