1
+package integration
2
+
3
+import (
4
+ "context"
5
+ "crypto/ecdsa"
6
+ "crypto/elliptic"
7
+ "crypto/rand"
8
+ "crypto/tls"
9
+ "crypto/x509"
10
+ "fmt"
11
+ "net"
12
+ "net/http"
13
+ "os"
14
+ "path/filepath"
15
+ "strings"
16
+ "testing"
17
+
18
+ "github.com/caddyserver/caddy/v2"
19
+ "github.com/caddyserver/caddy/v2/caddytest"
20
+ "github.com/mholt/acmez"
21
+ "github.com/mholt/acmez/acme"
22
+ smallstepacme "github.com/smallstep/certificates/acme"
23
+ "go.uber.org/zap"
24
+)
25
+
26
+const acmeChallengePort = 8080
27
+
28
+// Test the basic functionality of Caddy's ACME server
29
+func TestACMEServerWithDefaults(t *testing.T) {
30
+ ctx := context.Background()
31
+ logger, err := zap.NewDevelopment()
32
+ if err != nil {
33
+ t.Error(err)
34
+ return
35
+ }
36
+
37
+ tester := caddytest.NewTester(t)
38
+ tester.InitServer(`
39
+ {
40
+ skip_install_trust
41
+ admin localhost:2999
42
+ http_port 9080
43
+ https_port 9443
44
+ local_certs
45
+ }
46
+ acme.localhost {
47
+ acme_server
48
+ }
49
+ `, "caddyfile")
50
+
51
+ datadir := caddy.AppDataDir()
52
+ rootCertsGlob := filepath.Join(datadir, "pki", "authorities", "local", "*.crt")
53
+ matches, err := filepath.Glob(rootCertsGlob)
54
+ if err != nil {
55
+ t.Errorf("could not find root certs: %s", err)
56
+ return
57
+ }
58
+ certPool := x509.NewCertPool()
59
+ for _, m := range matches {
60
+ certPem, err := os.ReadFile(m)
61
+ if err != nil {
62
+ t.Errorf("reading cert file '%s' error: %s", m, err)
63
+ return
64
+ }
65
+ if !certPool.AppendCertsFromPEM(certPem) {
66
+ t.Errorf("failed to append the cert: %s", m)
67
+ return
68
+ }
69
+ }
70
+
71
+ client := acmez.Client{
72
+ Client: &acme.Client{
73
+ Directory: "https://acme.localhost:9443/acme/local/directory",
74
+ HTTPClient: &http.Client{
75
+ Transport: &http.Transport{
76
+ TLSClientConfig: &tls.Config{
77
+ RootCAs: certPool,
78
+ },
79
+ },
80
+ },
81
+ Logger: logger,
82
+ },
83
+ ChallengeSolvers: map[string]acmez.Solver{
84
+ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
85
+ },
86
+ }
87
+
88
+ accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
89
+ if err != nil {
90
+ t.Errorf("generating account key: %v", err)
91
+ }
92
+ account := acme.Account{
93
+ Contact: []string{"mailto:you@example.com"},
94
+ TermsOfServiceAgreed: true,
95
+ PrivateKey: accountPrivateKey,
96
+ }
97
+ account, err = client.NewAccount(ctx, account)
98
+ if err != nil {
99
+ t.Errorf("new account: %v", err)
100
+ return
101
+ }
102
+
103
+ // Every certificate needs a key.
104
+ certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
105
+ if err != nil {
106
+ t.Errorf("generating certificate key: %v", err)
107
+ return
108
+ }
109
+
110
+ certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
111
+ if err != nil {
112
+ t.Errorf("obtaining certificate: %v", err)
113
+ return
114
+ }
115
+
116
+ // ACME servers should usually give you the entire certificate chain
117
+ // in PEM format, and sometimes even alternate chains! It's up to you
118
+ // which one(s) to store and use, but whatever you do, be sure to
119
+ // store the certificate and key somewhere safe and secure, i.e. don't
120
+ // lose them!
121
+ for _, cert := range certs {
122
+ t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
123
+ }
124
+}
125
+
126
+func TestACMEServerWithMismatchedChallenges(t *testing.T) {
127
+ ctx := context.Background()
128
+ logger := caddy.Log().Named("acmez")
129
+
130
+ tester := caddytest.NewTester(t)
131
+ tester.InitServer(`
132
+ {
133
+ skip_install_trust
134
+ admin localhost:2999
135
+ http_port 9080
136
+ https_port 9443
137
+ local_certs
138
+ }
139
+ acme.localhost {
140
+ acme_server {
141
+ challenges tls-alpn-01
142
+ }
143
+ }
144
+ `, "caddyfile")
145
+
146
+ datadir := caddy.AppDataDir()
147
+ rootCertsGlob := filepath.Join(datadir, "pki", "authorities", "local", "*.crt")
148
+ matches, err := filepath.Glob(rootCertsGlob)
149
+ if err != nil {
150
+ t.Errorf("could not find root certs: %s", err)
151
+ return
152
+ }
153
+ certPool := x509.NewCertPool()
154
+ for _, m := range matches {
155
+ certPem, err := os.ReadFile(m)
156
+ if err != nil {
157
+ t.Errorf("reading cert file '%s' error: %s", m, err)
158
+ return
159
+ }
160
+ if !certPool.AppendCertsFromPEM(certPem) {
161
+ t.Errorf("failed to append the cert: %s", m)
162
+ return
163
+ }
164
+ }
165
+
166
+ client := acmez.Client{
167
+ Client: &acme.Client{
168
+ Directory: "https://acme.localhost:9443/acme/local/directory",
169
+ HTTPClient: &http.Client{
170
+ Transport: &http.Transport{
171
+ TLSClientConfig: &tls.Config{
172
+ RootCAs: certPool,
173
+ },
174
+ },
175
+ },
176
+ Logger: logger,
177
+ },
178
+ ChallengeSolvers: map[string]acmez.Solver{
179
+ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
180
+ },
181
+ }
182
+
183
+ accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
184
+ if err != nil {
185
+ t.Errorf("generating account key: %v", err)
186
+ }
187
+ account := acme.Account{
188
+ Contact: []string{"mailto:you@example.com"},
189
+ TermsOfServiceAgreed: true,
190
+ PrivateKey: accountPrivateKey,
191
+ }
192
+ account, err = client.NewAccount(ctx, account)
193
+ if err != nil {
194
+ t.Errorf("new account: %v", err)
195
+ return
196
+ }
197
+
198
+ // Every certificate needs a key.
199
+ certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
200
+ if err != nil {
201
+ t.Errorf("generating certificate key: %v", err)
202
+ return
203
+ }
204
+
205
+ certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
206
+ if len(certs) > 0 {
207
+ t.Errorf("expected '0' certificates, but received '%d'", len(certs))
208
+ }
209
+ if err == nil {
210
+ t.Error("expected errors, but received none")
211
+ }
212
+ const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])"
213
+ if !strings.Contains(err.Error(), expectedErrMsg) {
214
+ t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error())
215
+ }
216
+}
217
+
218
+// naiveHTTPSolver is a no-op acmez.Solver for example purposes only.
219
+type naiveHTTPSolver struct {
220
+ srv *http.Server
221
+ logger *zap.Logger
222
+}
223
+
224
+func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
225
+ smallstepacme.InsecurePortHTTP01 = acmeChallengePort
226
+ s.srv = &http.Server{
227
+ Addr: fmt.Sprintf("localhost:%d", acmeChallengePort),
228
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
229
+ host, _, err := net.SplitHostPort(r.Host)
230
+ if err != nil {
231
+ host = r.Host
232
+ }
233
+ if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) {
234
+ w.Header().Add("Content-Type", "text/plain")
235
+ w.Write([]byte(challenge.KeyAuthorization))
236
+ r.Close = true
237
+ s.logger.Info("served key authentication",
238
+ zap.String("identifier", challenge.Identifier.Value),
239
+ zap.String("challenge", "http-01"),
240
+ zap.String("remote", r.RemoteAddr),
241
+ )
242
+ }
243
+ }),
244
+ }
245
+ l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort))
246
+ if err != nil {
247
+ return err
248
+ }
249
+ s.logger.Info("present challenge", zap.Any("challenge", challenge))
250
+ go s.srv.Serve(l)
251
+ return nil
252
+}
253
+
254
+func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
255
+ smallstepacme.InsecurePortHTTP01 = 0
256
+ s.logger.Info("cleanup", zap.Any("challenge", challenge))
257
+ if s.srv != nil {
258
+ s.srv.Close()
259
+ }
260
+ return nil
261
+}