From e27c6c529b5f72c43841295befec16b0e01b420d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 3 Nov 2022 16:58:25 -0700 Subject: [PATCH] Add support for custom acme ports This change adds the flags --acme-http-port, --acme-tls-port, that combined with --insecure can be used to set custom ports for ACME http-01 and tls-alpn-01 challenges. These flags should only be used for testing purposes. Fixes #1015 --- acme/challenge.go | 27 +++++++++- acme/challenge_test.go | 115 ++++++++++++++++++++++++++++++++++++++++- commands/app.go | 31 +++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 84b3f83a..1a45a252 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -44,6 +44,18 @@ const ( DEVICEATTEST01 ChallengeType = "device-attest-01" ) +var ( + // InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it + // defaults to 80. + InsecurePortHTTP01 int + + // InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not + // set it defaults to 443. + // + // This variable can be used for testing purposes. + InsecurePortTLSALPN01 int +) + // Challenge represents an ACME response Challenge type. type Challenge struct { ID string `json:"-"` @@ -93,6 +105,12 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} + // Append insecure port if set. + // Only used for testing purposes. + if InsecurePortHTTP01 != 0 { + u.Host += ":" + strconv.Itoa(InsecurePortHTTP01) + } + vc := MustClientFromContext(ctx) resp, err := vc.Get(u.String()) if err != nil { @@ -165,7 +183,14 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate } - hostPort := net.JoinHostPort(ch.Value, "443") + var hostPort string + + // Allow to change TLS port for testing purposes. + if port := InsecurePortTLSALPN01; port == 0 { + hostPort = net.JoinHostPort(ch.Value, "443") + } else { + hostPort = net.JoinHostPort(ch.Value, strconv.Itoa(port)) + } vc := MustClientFromContext(ctx) conn, err := vc.TLSDial("tcp", hostPort, config) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 0027f5b1..1aa9f6ab 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -24,6 +24,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strconv" "strings" "testing" "time" @@ -370,6 +371,47 @@ func TestChallenge_Validate(t *testing.T) { }, } }, + "ok/http-01-insecure": func(t *testing.T) test { + t.Cleanup(func() { + InsecurePortHTTP01 = 0 + }) + + ch := &Challenge{ + ID: "chID", + Status: StatusPending, + Type: "http-01", + Token: "token", + Value: "zap.internal", + } + + InsecurePortHTTP01 = 8080 + + return test{ + ch: ch, + vc: &mockClient{ + get: func(url string) (*http.Response, error) { + return nil, errors.New("force") + }, + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equals(t, updch.ID, ch.ID) + assert.Equals(t, updch.Token, ch.Token) + assert.Equals(t, updch.Type, ch.Type) + assert.Equals(t, updch.Status, ch.Status) + assert.Equals(t, updch.Value, ch.Value) + + err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal:8080/.well-known/acme-challenge/%s: force", ch.Token) + assert.HasPrefix(t, updch.Error.Err.Error(), err.Err.Error()) + assert.Equals(t, updch.Error.Type, err.Type) + assert.Equals(t, updch.Error.Detail, err.Detail) + assert.Equals(t, updch.Error.Status, err.Status) + assert.Equals(t, updch.Error.Detail, err.Detail) + return nil + }, + }, + } + }, "fail/dns-01": func(t *testing.T) test { ch := &Challenge{ ID: "chID", @@ -501,6 +543,72 @@ func TestChallenge_Validate(t *testing.T) { srv, tlsDial := newTestTLSALPNServer(cert) srv.Start() + return test{ + ch: ch, + vc: &mockClient{ + tlsDial: tlsDial, + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equals(t, updch.ID, ch.ID) + assert.Equals(t, updch.Token, ch.Token) + assert.Equals(t, updch.Status, ch.Status) + assert.Equals(t, updch.Type, ch.Type) + assert.Equals(t, updch.Value, ch.Value) + assert.Equals(t, updch.Error, nil) + return nil + }, + }, + srv: srv, + jwk: jwk, + } + }, + "ok/tls-alpn-01-insecure": func(t *testing.T) test { + t.Cleanup(func() { + InsecurePortTLSALPN01 = 0 + }) + + ch := &Challenge{ + ID: "chID", + Token: "token", + Type: "tls-alpn-01", + Status: StatusPending, + Value: "zap.internal", + } + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + + expKeyAuth, err := KeyAuthorization(ch.Token, jwk) + assert.FatalError(t, err) + expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth)) + + cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value) + assert.FatalError(t, err) + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { + t.Fatalf("failed to listen on a port: %v", err) + } + } + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatalf("failed to split host port: %v", err) + } + + // Use an insecure port + InsecurePortTLSALPN01, err = strconv.Atoi(port) + if err != nil { + t.Fatalf("failed to convert port to int: %v", err) + } + + srv, tlsDial := newTestTLSALPNServer(cert, func(srv *httptest.Server) { + srv.Listener.Close() + srv.Listener = l + }) + srv.Start() + return test{ ch: ch, vc: &mockClient{ @@ -1248,7 +1356,7 @@ func TestDNS01Validate(t *testing.T) { type tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) -func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tlsDialer) { +func newTestTLSALPNServer(validationCert *tls.Certificate, opts ...func(*httptest.Server)) (*httptest.Server, tlsDialer) { srv := httptest.NewUnstartedServer(http.NewServeMux()) srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ @@ -1273,6 +1381,11 @@ func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tl }, } + // Apply options + for _, fn := range opts { + fn(srv) + } + srv.Listener = tls.NewListener(srv.Listener, srv.TLS) //srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush diff --git a/commands/app.go b/commands/app.go index 7a0b2637..f624843a 100644 --- a/commands/app.go +++ b/commands/app.go @@ -12,6 +12,7 @@ import ( "unicode" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" @@ -71,6 +72,19 @@ certificate issuer private key used in the RA mode.`, Usage: "The name of the authority's context.", EnvVar: "STEP_CA_CONTEXT", }, + cli.IntFlag{ + Name: "acme-http-port", + Usage: `The port used on http-01 challenges. It can be changed for testing purposes. +Requires **--insecure** flag.`, + }, + cli.IntFlag{ + Name: "acme-tls-port", + Usage: `The port used on tls-alpn-01 challenges. It can be changed for testing purposes. +Requires **--insecure** flag.`, + }, + cli.BoolFlag{ + Name: "insecure", + }, }, } @@ -88,6 +102,23 @@ func appAction(ctx *cli.Context) error { return errs.TooManyArguments(ctx) } + // Allow custom ACME ports with insecure + if acmePort := ctx.Int("acme-http-port"); acmePort != 0 { + if ctx.Bool("insecure") { + acme.InsecurePortHTTP01 = acmePort + } else { + return fmt.Errorf("flag '--acme-http-port' requires the '--insecure' flag") + } + } + if acmePort := ctx.Int("acme-tls-port"); acmePort != 0 { + if ctx.Bool("insecure") { + acme.InsecurePortTLSALPN01 = acmePort + } else { + return fmt.Errorf("flag '--acme-tls-port' requires the '--insecure' flag") + } + } + + // Allow custom contexts. if caCtx := ctx.String("context"); caCtx != "" { if err := step.Contexts().SetCurrent(caCtx); err != nil { return err