diff options
author | Mohammed Al Sahaf <[email protected]> | 2024-02-08 11:42:03 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2024-02-08 11:42:03 +0300 |
commit | e1aa862e6a951d4dc9c0e89b6d42f7fea1c5aba7 (patch) | |
tree | f8896f0a097019bed12b8a7ba64fea8bdb31b5e3 /caddytest | |
parent | 8c2a72ad07578ceba74ce04a9cdc45f1c6e2faeb (diff) | |
download | caddy-e1aa862e6a951d4dc9c0e89b6d42f7fea1c5aba7.tar.gz caddy-e1aa862e6a951d4dc9c0e89b6d42f7fea1c5aba7.zip |
acmeserver: support specifying the allowed challenge types (#5794)
* acmeserver: support specifying the allowed challenge types
* add caddyfile adapt tests
* introduce basic acme_server test
* skip acme test on unsuitable environments
* skip integration tests of ACME
* documentation
* add negative-scenario test for mismatched allowed challenges
* a bit more docs
* fix tests for ACME challenges
* appease the linter
* skip ACME tests on s390x
* enable ACME challenge tests on all machines
* Apply suggestions from code review
Co-authored-by: Matt Holt <[email protected]>
---------
Co-authored-by: Matt Holt <[email protected]>
Diffstat (limited to 'caddytest')
4 files changed, 454 insertions, 0 deletions
diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go new file mode 100644 index 000000000..00a3a6c4e --- /dev/null +++ b/caddytest/integration/acme_test.go @@ -0,0 +1,261 @@ +package integration + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddytest" + "github.com/mholt/acmez" + "github.com/mholt/acmez/acme" + smallstepacme "github.com/smallstep/certificates/acme" + "go.uber.org/zap" +) + +const acmeChallengePort = 8080 + +// Test the basic functionality of Caddy's ACME server +func TestACMEServerWithDefaults(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + if err != nil { + t.Error(err) + return + } + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { + acme_server + } + `, "caddyfile") + + datadir := caddy.AppDataDir() + rootCertsGlob := filepath.Join(datadir, "pki", "authorities", "local", "*.crt") + matches, err := filepath.Glob(rootCertsGlob) + if err != nil { + t.Errorf("could not find root certs: %s", err) + return + } + certPool := x509.NewCertPool() + for _, m := range matches { + certPem, err := os.ReadFile(m) + if err != nil { + t.Errorf("reading cert file '%s' error: %s", m, err) + return + } + if !certPool.AppendCertsFromPEM(certPem) { + t.Errorf("failed to append the cert: %s", m) + return + } + } + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + }, + }, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:[email protected]"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + + certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + if err != nil { + t.Errorf("obtaining certificate: %v", err) + return + } + + // ACME servers should usually give you the entire certificate chain + // in PEM format, and sometimes even alternate chains! It's up to you + // which one(s) to store and use, but whatever you do, be sure to + // store the certificate and key somewhere safe and secure, i.e. don't + // lose them! + for _, cert := range certs { + t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM) + } +} + +func TestACMEServerWithMismatchedChallenges(t *testing.T) { + ctx := context.Background() + logger := caddy.Log().Named("acmez") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { + acme_server { + challenges tls-alpn-01 + } + } + `, "caddyfile") + + datadir := caddy.AppDataDir() + rootCertsGlob := filepath.Join(datadir, "pki", "authorities", "local", "*.crt") + matches, err := filepath.Glob(rootCertsGlob) + if err != nil { + t.Errorf("could not find root certs: %s", err) + return + } + certPool := x509.NewCertPool() + for _, m := range matches { + certPem, err := os.ReadFile(m) + if err != nil { + t.Errorf("reading cert file '%s' error: %s", m, err) + return + } + if !certPool.AppendCertsFromPEM(certPem) { + t.Errorf("failed to append the cert: %s", m) + return + } + } + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + }, + }, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:[email protected]"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + + certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + if len(certs) > 0 { + t.Errorf("expected '0' certificates, but received '%d'", len(certs)) + } + if err == nil { + t.Error("expected errors, but received none") + } + const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])" + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error()) + } +} + +// naiveHTTPSolver is a no-op acmez.Solver for example purposes only. +type naiveHTTPSolver struct { + srv *http.Server + logger *zap.Logger +} + +func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error { + smallstepacme.InsecurePortHTTP01 = acmeChallengePort + s.srv = &http.Server{ + Addr: fmt.Sprintf("localhost:%d", acmeChallengePort), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte(challenge.KeyAuthorization)) + r.Close = true + s.logger.Info("served key authentication", + zap.String("identifier", challenge.Identifier.Value), + zap.String("challenge", "http-01"), + zap.String("remote", r.RemoteAddr), + ) + } + }), + } + l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort)) + if err != nil { + return err + } + s.logger.Info("present challenge", zap.Any("challenge", challenge)) + go s.srv.Serve(l) + return nil +} + +func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error { + smallstepacme.InsecurePortHTTP01 = 0 + s.logger.Info("cleanup", zap.Any("challenge", challenge)) + if s.srv != nil { + s.srv.Close() + } + return nil +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.txt b/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.txt new file mode 100644 index 000000000..2a7a51492 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.txt @@ -0,0 +1,65 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges dns-01 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "challenges": [ + "dns-01" + ], + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.txt b/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.txt new file mode 100644 index 000000000..26d345047 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.txt @@ -0,0 +1,62 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.txt b/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.txt new file mode 100644 index 000000000..7fe3ca663 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.txt @@ -0,0 +1,66 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges dns-01 http-01 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "challenges": [ + "dns-01", + "http-01" + ], + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} |