Skip to content

Commit 931656b

Browse files
authored
acmeserver: add policy field to define allow/deny rules (#5796)
* acmeserver: support specifying the allowed challenge types * add caddyfile adapt tests * acmeserver: add `policy` field to define allow/deny rules * allow `omitempty` to work * add caddyfile support for `policy` * remove "uri domain" policy * fmt the files * add docs * do not support `CommonName`; the field is deprecated * r/DNSDomains/Domains/g * Caddyfile docs * add tests * move `Policy` to top of file
1 parent da6a569 commit 931656b

File tree

6 files changed

+506
-66
lines changed

6 files changed

+506
-66
lines changed

caddytest/integration/acme_test.go

+8-63
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import (
55
"crypto/ecdsa"
66
"crypto/elliptic"
77
"crypto/rand"
8-
"crypto/tls"
9-
"crypto/x509"
108
"fmt"
119
"net"
1210
"net/http"
13-
"os"
14-
"path/filepath"
1511
"strings"
1612
"testing"
1713

@@ -48,37 +44,11 @@ func TestACMEServerWithDefaults(t *testing.T) {
4844
}
4945
`, "caddyfile")
5046

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-
7147
client := acmez.Client{
7248
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,
49+
Directory: "https://acme.localhost:9443/acme/local/directory",
50+
HTTPClient: tester.Client,
51+
Logger: logger,
8252
},
8353
ChallengeSolvers: map[string]acmez.Solver{
8454
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@@ -143,37 +113,11 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
143113
}
144114
`, "caddyfile")
145115

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-
166116
client := acmez.Client{
167117
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,
118+
Directory: "https://acme.localhost:9443/acme/local/directory",
119+
HTTPClient: tester.Client,
120+
Logger: logger,
177121
},
178122
ChallengeSolvers: map[string]acmez.Solver{
179123
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@@ -224,12 +168,13 @@ type naiveHTTPSolver struct {
224168
func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
225169
smallstepacme.InsecurePortHTTP01 = acmeChallengePort
226170
s.srv = &http.Server{
227-
Addr: fmt.Sprintf("localhost:%d", acmeChallengePort),
171+
Addr: fmt.Sprintf(":%d", acmeChallengePort),
228172
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
229173
host, _, err := net.SplitHostPort(r.Host)
230174
if err != nil {
231175
host = r.Host
232176
}
177+
s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path))
233178
if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) {
234179
w.Header().Add("Content-Type", "text/plain")
235180
w.Write([]byte(challenge.KeyAuthorization))

caddytest/integration/acmeserver_test.go

+176
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package integration
22

33
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"strings"
49
"testing"
510

611
"github.com/caddyserver/caddy/v2/caddytest"
12+
"github.com/mholt/acmez"
13+
"github.com/mholt/acmez/acme"
14+
"go.uber.org/zap"
715
)
816

917
func TestACMEServerDirectory(t *testing.T) {
@@ -31,3 +39,171 @@ func TestACMEServerDirectory(t *testing.T) {
3139
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
3240
`)
3341
}
42+
43+
func TestACMEServerAllowPolicy(t *testing.T) {
44+
tester := caddytest.NewTester(t)
45+
tester.InitServer(`
46+
{
47+
skip_install_trust
48+
local_certs
49+
admin localhost:2999
50+
http_port 9080
51+
https_port 9443
52+
pki {
53+
ca local {
54+
name "Caddy Local Authority"
55+
}
56+
}
57+
}
58+
acme.localhost {
59+
acme_server {
60+
challenges http-01
61+
allow {
62+
domains localhost
63+
}
64+
}
65+
}
66+
`, "caddyfile")
67+
68+
ctx := context.Background()
69+
logger, err := zap.NewDevelopment()
70+
if err != nil {
71+
t.Error(err)
72+
return
73+
}
74+
75+
client := acmez.Client{
76+
Client: &acme.Client{
77+
Directory: "https://acme.localhost:9443/acme/local/directory",
78+
HTTPClient: tester.Client,
79+
Logger: logger,
80+
},
81+
ChallengeSolvers: map[string]acmez.Solver{
82+
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
83+
},
84+
}
85+
86+
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
87+
if err != nil {
88+
t.Errorf("generating account key: %v", err)
89+
}
90+
account := acme.Account{
91+
Contact: []string{"mailto:[email protected]"},
92+
TermsOfServiceAgreed: true,
93+
PrivateKey: accountPrivateKey,
94+
}
95+
account, err = client.NewAccount(ctx, account)
96+
if err != nil {
97+
t.Errorf("new account: %v", err)
98+
return
99+
}
100+
101+
// Every certificate needs a key.
102+
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
103+
if err != nil {
104+
t.Errorf("generating certificate key: %v", err)
105+
return
106+
}
107+
{
108+
certs, err := client.ObtainCertificate(
109+
ctx,
110+
account,
111+
certPrivateKey,
112+
[]string{"localhost"},
113+
)
114+
if err != nil {
115+
t.Errorf("obtaining certificate for allowed domain: %v", err)
116+
return
117+
}
118+
119+
// ACME servers should usually give you the entire certificate chain
120+
// in PEM format, and sometimes even alternate chains! It's up to you
121+
// which one(s) to store and use, but whatever you do, be sure to
122+
// store the certificate and key somewhere safe and secure, i.e. don't
123+
// lose them!
124+
for _, cert := range certs {
125+
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
126+
}
127+
}
128+
{
129+
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
130+
if err == nil {
131+
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
132+
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
133+
t.Logf("unexpected error: %v", err)
134+
}
135+
}
136+
}
137+
138+
func TestACMEServerDenyPolicy(t *testing.T) {
139+
tester := caddytest.NewTester(t)
140+
tester.InitServer(`
141+
{
142+
skip_install_trust
143+
local_certs
144+
admin localhost:2999
145+
http_port 9080
146+
https_port 9443
147+
pki {
148+
ca local {
149+
name "Caddy Local Authority"
150+
}
151+
}
152+
}
153+
acme.localhost {
154+
acme_server {
155+
deny {
156+
domains deny.localhost
157+
}
158+
}
159+
}
160+
`, "caddyfile")
161+
162+
ctx := context.Background()
163+
logger, err := zap.NewDevelopment()
164+
if err != nil {
165+
t.Error(err)
166+
return
167+
}
168+
169+
client := acmez.Client{
170+
Client: &acme.Client{
171+
Directory: "https://acme.localhost:9443/acme/local/directory",
172+
HTTPClient: tester.Client,
173+
Logger: logger,
174+
},
175+
ChallengeSolvers: map[string]acmez.Solver{
176+
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
177+
},
178+
}
179+
180+
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
181+
if err != nil {
182+
t.Errorf("generating account key: %v", err)
183+
}
184+
account := acme.Account{
185+
Contact: []string{"mailto:[email protected]"},
186+
TermsOfServiceAgreed: true,
187+
PrivateKey: accountPrivateKey,
188+
}
189+
account, err = client.NewAccount(ctx, account)
190+
if err != nil {
191+
t.Errorf("new account: %v", err)
192+
return
193+
}
194+
195+
// Every certificate needs a key.
196+
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
197+
if err != nil {
198+
t.Errorf("generating certificate key: %v", err)
199+
return
200+
}
201+
{
202+
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
203+
if err == nil {
204+
t.Errorf("obtaining certificate for 'deny.localhost' domain")
205+
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
206+
t.Logf("unexpected error: %v", err)
207+
}
208+
}
209+
}

modules/caddypki/acmeserver/acmeserver.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ type Handler struct {
9696
// "http-01", "dns-01", "tls-alpn-01"
9797
Challenges ACMEChallenges `json:"challenges,omitempty" `
9898

99+
// The policy to use for issuing certificates
100+
Policy *Policy `json:"policy,omitempty"`
101+
99102
logger *zap.Logger
100103
resolvers []caddy.NetworkAddress
101104
ctx caddy.Context
@@ -165,7 +168,10 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
165168
&provisioner.ACME{
166169
Name: ash.CA,
167170
Challenges: ash.Challenges.toSmallstepType(),
168-
Type: provisioner.TypeACME.String(),
171+
Options: &provisioner.Options{
172+
X509: ash.Policy.normalizeRules(),
173+
},
174+
Type: provisioner.TypeACME.String(),
169175
Claims: &provisioner.Claims{
170176
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
171177
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365},

0 commit comments

Comments
 (0)