-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
8c2a72a
commit e1aa862
Showing
7 changed files
with
549 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
65 changes: 65 additions & 0 deletions
65
caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
caddytest/integration/caddyfile_adapt/acme_server_default_challenges.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.