Skip to content

Commit

Permalink
acmeserver: support specifying the allowed challenge types (#5794)
Browse files Browse the repository at this point in the history
* 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
mohammed90 and mholt authored Feb 8, 2024
1 parent 8c2a72a commit e1aa862
Show file tree
Hide file tree
Showing 7 changed files with 549 additions and 4 deletions.
261 changes: 261 additions & 0 deletions caddytest/integration/acme_test.go
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
}
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"
}
}
}
}
}
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"
}
}
}
}
}
Loading

0 comments on commit e1aa862

Please sign in to comment.