From 7846696fbb6972794afd5a5a681972da3de32512 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 29 Jan 2020 11:58:47 -0800 Subject: [PATCH 01/15] Fix return sign options on ssh sign. --- authority/authorize.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index bda59520..2bf7223b 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -100,8 +100,8 @@ func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner. if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { return nil, errs.NotImplemented("authority.Authorize; ssh certificate flows are not enabled", opts...) } - _, err := a.authorizeSSHSign(ctx, token) - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) + signOpts, err := a.authorizeSSHSign(ctx, token) + return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) case provisioner.SSHRenewMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { return nil, errs.NotImplemented("authority.Authorize; ssh certificate flows are not enabled", opts...) From 752bfeeccdb7a5076512ad08185c5e3237bd8d10 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 30 Jan 2020 10:59:28 -0800 Subject: [PATCH 02/15] Update cli dependency. --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e3437631..004c50d7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 - github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6 + github.com/smallstep/cli v0.14.0-rc.3 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 diff --git a/go.sum b/go.sum index 3cc1d957..b37f253c 100644 --- a/go.sum +++ b/go.sum @@ -420,6 +420,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191218224459-1fa35491ea07/go. github.com/smallstep/certificates v0.14.0-rc.1.0.20200110185849-085ae821636e/go.mod h1:weY9Os8g0yPfyxd+Zy1CTAwCb7YMqg/u5XnEagBN5Rk= github.com/smallstep/certificates v0.14.0-rc.1.0.20200111012147-3ce267cdd6b7/go.mod h1:jljUh6mTvHOAqvIUvbD2L3Q/aqSpTI6HzJiNFQkj1Hc= github.com/smallstep/certificates v0.14.0-rc.1.0.20200128212940-432ed0090f3d/go.mod h1:lWKe0ZOg45lNWtByxh82fOfzXwx93S0TeWzTCOjc19k= +github.com/smallstep/certificates v0.14.0-rc.2.0.20200129195847-7846696fbb69/go.mod h1:vZjJp4hweYVx+rBouWEVOf3KlH2Yilxo/50dsj7y8aY= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/certinfo v1.0.0/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= @@ -448,6 +449,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819 h1:mcYdBrClU github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819/go.mod h1:SUBVVdOk5XI7yllSupRYHzN5y4MBo89X27CN4P0d+Jw= github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6 h1:Dh+Au3z44aSzZ+nNEr+9MAdenSqTjtFVrlxlzdoXBNs= github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6/go.mod h1:50kmsPMAiR9XD0jHZYY19fkSSD3mKF9ztQjgtTLefjU= +github.com/smallstep/cli v0.14.0-rc.3 h1:IphfrTtJHR2tpFiiBUKfPpO7SCGvfca72PbYJq8k1kU= +github.com/smallstep/cli v0.14.0-rc.3/go.mod h1:5kg85FrLTaQE0JgV3IZAxuVRS7G5qvV0hxOh0u/H6IE= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From c255274572e6912695d741d44b0109e63d2adede Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 1 Feb 2020 17:35:41 -0800 Subject: [PATCH 03/15] Should be returning status code 400 for ACME Account Not Found. Issue #173 --- acme/api/account.go | 2 +- acme/api/account_test.go | 10 +++++----- acme/api/handler_test.go | 12 ++++++------ acme/api/middleware_test.go | 2 +- acme/api/order_test.go | 12 ++++++------ acme/errors.go | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index fb43d4f9..bb6c92d6 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -96,7 +96,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { acc, err := accountFromContext(r) if err != nil { acmeErr, ok := err.(*acme.Error) - if !ok || acmeErr.Status != http.StatusNotFound { + if !ok || acmeErr.Status != http.StatusBadRequest { // Something went wrong ... api.WriteError(w, err) return diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 193088f2..a3ebf55c 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -202,7 +202,7 @@ func TestHandlerGetOrdersByAccount(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -212,7 +212,7 @@ func TestHandlerGetOrdersByAccount(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -378,7 +378,7 @@ func TestHandlerNewAccount(t *testing.T) { ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -569,7 +569,7 @@ func TestHandlerGetUpdateAccount(t *testing.T) { "fail/no-account": func(t *testing.T) test { return test{ ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -578,7 +578,7 @@ func TestHandlerGetUpdateAccount(t *testing.T) { ctx = context.WithValue(ctx, accContextKey, nil) return test{ ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 3757882f..ebafbbb8 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -372,7 +372,7 @@ func TestHandlerGetAuthz(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -382,7 +382,7 @@ func TestHandlerGetAuthz(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -504,7 +504,7 @@ func TestHandlerGetCertificate(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -513,7 +513,7 @@ func TestHandlerGetCertificate(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -623,7 +623,7 @@ func TestHandlerGetChallenge(t *testing.T) { "fail/no-account": func(t *testing.T) test { return test{ ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -632,7 +632,7 @@ func TestHandlerGetChallenge(t *testing.T) { ctx = context.WithValue(ctx, accContextKey, nil) return test{ ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index f8aa322c..e617e5bd 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -842,7 +842,7 @@ func TestHandlerLookupJWK(t *testing.T) { }, }, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 68bd4f46..0931d832 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -205,7 +205,7 @@ func TestHandlerGetOrder(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -215,7 +215,7 @@ func TestHandlerGetOrder(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -343,7 +343,7 @@ func TestHandlerNewOrder(t *testing.T) { "fail/no-account": func(t *testing.T) test { return test{ ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -352,7 +352,7 @@ func TestHandlerNewOrder(t *testing.T) { ctx = context.WithValue(ctx, accContextKey, nil) return test{ ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -597,7 +597,7 @@ func TestHandlerFinalizeOrder(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, @@ -607,7 +607,7 @@ func TestHandlerFinalizeOrder(t *testing.T) { return test{ auth: &mockAcmeAuthority{}, ctx: ctx, - statusCode: 404, + statusCode: 400, problem: acme.AccountDoesNotExistErr(nil), } }, diff --git a/acme/errors.go b/acme/errors.go index 9facac5f..dd6f4aff 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -9,7 +9,7 @@ func AccountDoesNotExistErr(err error) *Error { return &Error{ Type: accountDoesNotExistErr, Detail: "Account does not exist", - Status: 404, + Status: 400, Err: err, } } From bb30372d434ceaa47dd549736dc9e3d0781d2f68 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 4 Feb 2020 21:37:52 +0100 Subject: [PATCH 04/15] Its 2020! --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9ea39e4f..38d7895a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Smallstep Labs, Inc. +Copyright (c) 2020 Smallstep Labs, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4b473732d98df2471ae2476392fda700bb4df83a Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 09:50:22 -0500 Subject: [PATCH 05/15] Add support for TLS-ALPN-01 challenge. --- .gitignore | 1 + README.md | 2 +- acme/authority.go | 7 + acme/authority_test.go | 26 +- acme/authz.go | 16 +- acme/authz_test.go | 37 +- acme/challenge.go | 156 +++++++++ acme/challenge_test.go | 769 ++++++++++++++++++++++++++++++++++++++++- acme/order_test.go | 98 ++++-- go.mod | 1 + 10 files changed, 1072 insertions(+), 41 deletions(-) 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 From 9052da66a3cf141cfc53019963f5524b29d426e6 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 14:42:56 -0500 Subject: [PATCH 06/15] Fix linter, tidy go.mod file. --- ca/identity/client.go | 2 +- ca/provisioner.go | 1 + go.mod | 1 - templates/values.go | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ca/identity/client.go b/ca/identity/client.go index d615a019..7daafacd 100644 --- a/ca/identity/client.go +++ b/ca/identity/client.go @@ -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) { diff --git a/ca/provisioner.go b/ca/provisioner.go index 3f86c068..1b7067e0 100644 --- a/ca/provisioner.go +++ b/ca/provisioner.go @@ -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 { diff --git a/go.mod b/go.mod index 5704c45d..004c50d7 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ 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 diff --git a/templates/values.go b/templates/values.go index 505b3b87..ba03267b 100644 --- a/templates/values.go +++ b/templates/values.go @@ -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 From b8208ec401d52908dcd436512ba6c2db675a3323 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 15:14:08 -0500 Subject: [PATCH 07/15] Add test case for failed came-tls/1 protocol negotiation. --- acme/challenge_test.go | 43 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 8564824d..bfcfb603 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -23,8 +23,6 @@ import ( "testing" "time" - "golang.org/x/crypto/acme" - "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/db" @@ -1352,6 +1350,45 @@ func TestTLSALPN01Validate(t *testing.T) { 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) @@ -1563,7 +1600,7 @@ func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tl srv.TLS = &tls.Config{ GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == acme.ALPNProto { + if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == "acme-tls/1" { return validationCert, nil } return nil, nil From 6b5a2b17b51cb2641b1c464b58d7a737788da211 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 15:25:27 -0500 Subject: [PATCH 08/15] Add challenge unmarshal test cases. --- acme/challenge_test.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index bfcfb603..40466454 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -433,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) @@ -446,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) + _tlsALPNCh, ok := dnsCh.(*dns01Challenge) + assert.Fatal(t, ok) + _tlsALPNCh.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) From 6843408d42e477ce356632e05968ccb062d544e8 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 19:26:18 -0500 Subject: [PATCH 09/15] Reject obsolete id-pe-acmeIdentifier. --- acme/challenge.go | 17 ++++++- acme/challenge_test.go | 100 +++++++++++++++++++---------------------- 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index d55f42a6..986b94c4 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -439,6 +439,7 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val 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 { @@ -447,8 +448,7 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val hashedKeyAuth := sha256.Sum256([]byte(keyAuth)) for _, ext := range leafCert.Extensions { - if idPeAcmeIdentifier.Equal(ext.Id) || idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) { - + if idPeAcmeIdentifier.Equal(ext.Id) { if !ext.Critical { if err = tc.storeError(db, RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ @@ -490,6 +490,19 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val } 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, diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 40466454..4d97d79d 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -1463,6 +1463,52 @@ func TestTLSALPN01Validate(t *testing.T) { 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/with-new-oid": func(t *testing.T) test { ch, err := newTLSALPNCh() assert.FatalError(t, err) @@ -1525,60 +1571,6 @@ func TestTLSALPN01Validate(t *testing.T) { 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) { From 157686e338b1afd50e9917937bffa6540aec546e Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Fri, 7 Feb 2020 19:57:29 -0500 Subject: [PATCH 10/15] Tiny finishes. --- acme/challenge.go | 3 --- acme/challenge_test.go | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 986b94c4..1d1cf50b 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -406,9 +406,6 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val 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 { diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 4d97d79d..d079bf8f 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -1509,7 +1509,7 @@ func TestTLSALPN01Validate(t *testing.T) { res: ch, } }, - "ok/with-new-oid": func(t *testing.T) test { + "ok": func(t *testing.T) test { ch, err := newTLSALPNCh() assert.FatalError(t, err) _ch, ok := ch.(*tlsALPN01Challenge) From 5d208432a5c3f7ebfb0fe3e60698824a7cee7db6 Mon Sep 17 00:00:00 2001 From: mkontani Date: Sat, 8 Feb 2020 20:31:49 +0900 Subject: [PATCH 11/15] Fix help command indication --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 806d1775..456d5b14 100644 --- a/README.md +++ b/README.md @@ -342,8 +342,8 @@ Documentation can be found in a handful of different places: 1. The [docs](./docs/README.md) sub-repo has an index of documentation and tutorials. -2. On the command line with `step ca help xxx` where `xxx` is the subcommand -you are interested in. Ex: `step help ca provisioners list`. +2. On the command line with `step help ca xxx` where `xxx` is the subcommand +you are interested in. Ex: `step help ca provisioner list`. 3. On the web at https://smallstep.com/docs/certificates. From f2b95647f3645e745395a79cb1f600741fb56ab5 Mon Sep 17 00:00:00 2001 From: Sebastian Tiedtke Date: Mon, 10 Feb 2020 09:55:21 -0800 Subject: [PATCH 12/15] Use date range in copyright --- cmd/step-ca/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 468f7084..3f345092 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -116,7 +116,7 @@ $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt '''` app.Flags = append(app.Flags, commands.AppCommand.Flags...) app.Flags = append(app.Flags, cli.HelpFlag) - app.Copyright = "(c) 2019 Smallstep Labs, Inc." + app.Copyright = "(c) 2018-2020 Smallstep Labs, Inc." // All non-successful output should be written to stderr app.Writer = os.Stdout From 200cfd24334d343984963e83c41e9eafd60b3b07 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Mon, 10 Feb 2020 14:50:13 -0500 Subject: [PATCH 13/15] Add test for missing TLS certificates in response. --- acme/challenge_test.go | 28 ++++++++++++------- acme/mock_conn.go | 61 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 acme/mock_conn.go diff --git a/acme/challenge_test.go b/acme/challenge_test.go index d079bf8f..844c8385 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -14,8 +14,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "io/ioutil" - "log" "math/big" "net" "net/http" @@ -1086,21 +1086,19 @@ func TestTLSALPN01Validate(t *testing.T) { 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())) + 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) - srv, tlsDial := newTestTLSALPNServer(nil) - srv.Start() - return test{ - srv: srv, - ch: ch, + ch: ch, vo: validateOptions{ - tlsDial: tlsDial, + 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) { @@ -1630,13 +1628,25 @@ func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tl } srv.Listener = tls.NewListener(srv.Listener, srv.TLS) - srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush + //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 { diff --git a/acme/mock_conn.go b/acme/mock_conn.go new file mode 100644 index 00000000..590fbed1 --- /dev/null +++ b/acme/mock_conn.go @@ -0,0 +1,61 @@ +package acme + +/* +type Conn interface { + // Read reads data from the connection. + // Read can be made to time out and return an Error with Timeout() == true + // after a fixed time limit; see SetDeadline and SetReadDeadline. + Read(b []byte) (n int, err error) + + // Write writes data to the connection. + // Write can be made to time out and return an Error with Timeout() == true + // after a fixed time limit; see SetDeadline and SetWriteDeadline. + Write(b []byte) (n int, err error) + + // Close closes the connection. + // Any blocked Read or Write operations will be unblocked and return errors. + Close() error + + // LocalAddr returns the local network address. + LocalAddr() Addr + + // RemoteAddr returns the remote network address. + RemoteAddr() Addr + + // SetDeadline sets the read and write deadlines associated + // with the connection. It is equivalent to calling both + // SetReadDeadline and SetWriteDeadline. + // + // A deadline is an absolute time after which I/O operations + // fail with a timeout (see type Error) instead of + // blocking. The deadline applies to all future and pending + // I/O, not just the immediately following call to Read or + // Write. After a deadline has been exceeded, the connection + // can be refreshed by setting a deadline in the future. + // + // An idle timeout can be implemented by repeatedly extending + // the deadline after successful Read or Write calls. + // + // A zero value for t means I/O operations will not time out. + // + // Note that if a TCP connection has keep-alive turned on, + // which is the default unless overridden by Dialer.KeepAlive + // or ListenConfig.KeepAlive, then a keep-alive failure may + // also return a timeout error. On Unix systems a keep-alive + // failure on I/O can be detected using + // errors.Is(err, syscall.ETIMEDOUT). + SetDeadline(t time.Time) error + + // SetReadDeadline sets the deadline for future Read calls + // and any currently-blocked Read call. + // A zero value for t means Read will not time out. + SetReadDeadline(t time.Time) error + + // SetWriteDeadline sets the deadline for future Write calls + // and any currently-blocked Write call. + // Even if write times out, it may return n > 0, indicating that + // some of the data was successfully written. + // A zero value for t means Write will not time out. + SetWriteDeadline(t time.Time) error +} +*/ From 10bc548c6ec6c84f5d85ad243b93f049243c9c58 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Mon, 10 Feb 2020 14:58:16 -0500 Subject: [PATCH 14/15] Remove leftover file. --- acme/mock_conn.go | 61 ----------------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 acme/mock_conn.go diff --git a/acme/mock_conn.go b/acme/mock_conn.go deleted file mode 100644 index 590fbed1..00000000 --- a/acme/mock_conn.go +++ /dev/null @@ -1,61 +0,0 @@ -package acme - -/* -type Conn interface { - // Read reads data from the connection. - // Read can be made to time out and return an Error with Timeout() == true - // after a fixed time limit; see SetDeadline and SetReadDeadline. - Read(b []byte) (n int, err error) - - // Write writes data to the connection. - // Write can be made to time out and return an Error with Timeout() == true - // after a fixed time limit; see SetDeadline and SetWriteDeadline. - Write(b []byte) (n int, err error) - - // Close closes the connection. - // Any blocked Read or Write operations will be unblocked and return errors. - Close() error - - // LocalAddr returns the local network address. - LocalAddr() Addr - - // RemoteAddr returns the remote network address. - RemoteAddr() Addr - - // SetDeadline sets the read and write deadlines associated - // with the connection. It is equivalent to calling both - // SetReadDeadline and SetWriteDeadline. - // - // A deadline is an absolute time after which I/O operations - // fail with a timeout (see type Error) instead of - // blocking. The deadline applies to all future and pending - // I/O, not just the immediately following call to Read or - // Write. After a deadline has been exceeded, the connection - // can be refreshed by setting a deadline in the future. - // - // An idle timeout can be implemented by repeatedly extending - // the deadline after successful Read or Write calls. - // - // A zero value for t means I/O operations will not time out. - // - // Note that if a TCP connection has keep-alive turned on, - // which is the default unless overridden by Dialer.KeepAlive - // or ListenConfig.KeepAlive, then a keep-alive failure may - // also return a timeout error. On Unix systems a keep-alive - // failure on I/O can be detected using - // errors.Is(err, syscall.ETIMEDOUT). - SetDeadline(t time.Time) error - - // SetReadDeadline sets the deadline for future Read calls - // and any currently-blocked Read call. - // A zero value for t means Read will not time out. - SetReadDeadline(t time.Time) error - - // SetWriteDeadline sets the deadline for future Write calls - // and any currently-blocked Write call. - // Even if write times out, it may return n > 0, indicating that - // some of the data was successfully written. - // A zero value for t means Write will not time out. - SetWriteDeadline(t time.Time) error -} -*/ From cb46a8b74195178a4238be54f5bd8f81d0634eef Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Tue, 11 Feb 2020 09:57:28 -0500 Subject: [PATCH 15/15] Small test fixes. --- acme/challenge_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 844c8385..b2b249cb 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -462,9 +462,9 @@ func TestChallengeUnmarshal(t *testing.T) { "fail/unexpected-type-dns": func(t *testing.T) test { dnsCh, err := newDNSCh() assert.FatalError(t, err) - _tlsALPNCh, ok := dnsCh.(*dns01Challenge) + _dnsCh, ok := dnsCh.(*dns01Challenge) assert.Fatal(t, ok) - _tlsALPNCh.baseChallenge.Type = "foo" + _dnsCh.baseChallenge.Type = "foo" b, err := json.Marshal(dnsCh) assert.FatalError(t, err) return test{ @@ -492,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)