Skip to content

Commit e1aa862

Browse files
mohammed90mholt
andauthored
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]>
1 parent 8c2a72a commit e1aa862

File tree

7 files changed

+549
-4
lines changed

7 files changed

+549
-4
lines changed

caddytest/integration/acme_test.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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:[email protected]"},
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:[email protected]"},
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+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
pki {
3+
ca custom-ca {
4+
name "Custom CA"
5+
}
6+
}
7+
}
8+
9+
acme.example.com {
10+
acme_server {
11+
ca custom-ca
12+
challenges dns-01
13+
}
14+
}
15+
----------
16+
{
17+
"apps": {
18+
"http": {
19+
"servers": {
20+
"srv0": {
21+
"listen": [
22+
":443"
23+
],
24+
"routes": [
25+
{
26+
"match": [
27+
{
28+
"host": [
29+
"acme.example.com"
30+
]
31+
}
32+
],
33+
"handle": [
34+
{
35+
"handler": "subroute",
36+
"routes": [
37+
{
38+
"handle": [
39+
{
40+
"ca": "custom-ca",
41+
"challenges": [
42+
"dns-01"
43+
],
44+
"handler": "acme_server"
45+
}
46+
]
47+
}
48+
]
49+
}
50+
],
51+
"terminal": true
52+
}
53+
]
54+
}
55+
}
56+
},
57+
"pki": {
58+
"certificate_authorities": {
59+
"custom-ca": {
60+
"name": "Custom CA"
61+
}
62+
}
63+
}
64+
}
65+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
pki {
3+
ca custom-ca {
4+
name "Custom CA"
5+
}
6+
}
7+
}
8+
9+
acme.example.com {
10+
acme_server {
11+
ca custom-ca
12+
challenges
13+
}
14+
}
15+
----------
16+
{
17+
"apps": {
18+
"http": {
19+
"servers": {
20+
"srv0": {
21+
"listen": [
22+
":443"
23+
],
24+
"routes": [
25+
{
26+
"match": [
27+
{
28+
"host": [
29+
"acme.example.com"
30+
]
31+
}
32+
],
33+
"handle": [
34+
{
35+
"handler": "subroute",
36+
"routes": [
37+
{
38+
"handle": [
39+
{
40+
"ca": "custom-ca",
41+
"handler": "acme_server"
42+
}
43+
]
44+
}
45+
]
46+
}
47+
],
48+
"terminal": true
49+
}
50+
]
51+
}
52+
}
53+
},
54+
"pki": {
55+
"certificate_authorities": {
56+
"custom-ca": {
57+
"name": "Custom CA"
58+
}
59+
}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)