summaryrefslogtreecommitdiffhomepage
path: root/caddytest
diff options
context:
space:
mode:
authorMohammed Al Sahaf <[email protected]>2024-02-08 11:42:03 +0300
committerGitHub <[email protected]>2024-02-08 11:42:03 +0300
commite1aa862e6a951d4dc9c0e89b6d42f7fea1c5aba7 (patch)
treef8896f0a097019bed12b8a7ba64fea8bdb31b5e3 /caddytest
parent8c2a72ad07578ceba74ce04a9cdc45f1c6e2faeb (diff)
downloadcaddy-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')
-rw-r--r--caddytest/integration/acme_test.go261
-rw-r--r--caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.txt65
-rw-r--r--caddytest/integration/caddyfile_adapt/acme_server_default_challenges.txt62
-rw-r--r--caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.txt66
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"
+ }
+ }
+ }
+ }
+}