Skip to content

Commit

Permalink
acmeserver: add policy field to define allow/deny rules (#5796)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mohammed90 committed Feb 23, 2024
1 parent da6a569 commit 931656b
Show file tree
Hide file tree
Showing 6 changed files with 506 additions and 66 deletions.
71 changes: 8 additions & 63 deletions caddytest/integration/acme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -48,37 +44,11 @@ func TestACMEServerWithDefaults(t *testing.T) {
}
`, "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,
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
Expand Down Expand Up @@ -143,37 +113,11 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
}
`, "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,
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
Expand Down Expand Up @@ -224,12 +168,13 @@ type naiveHTTPSolver struct {
func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
smallstepacme.InsecurePortHTTP01 = acmeChallengePort
s.srv = &http.Server{
Addr: fmt.Sprintf("localhost:%d", acmeChallengePort),
Addr: fmt.Sprintf(":%d", acmeChallengePort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path))
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))
Expand Down
176 changes: 176 additions & 0 deletions caddytest/integration/acmeserver_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package integration

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"strings"
"testing"

"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)

func TestACMEServerDirectory(t *testing.T) {
Expand Down Expand Up @@ -31,3 +39,171 @@ func TestACMEServerDirectory(t *testing.T) {
`{"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"}
`)
}

func TestACMEServerAllowPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
challenges http-01
allow {
domains localhost
}
}
}
`, "caddyfile")

ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}

client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
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:you@example.com"},
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 for allowed domain: %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)
}
}
{
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}

func TestACMEServerDenyPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
deny {
domains deny.localhost
}
}
}
`, "caddyfile")

ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}

client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
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:you@example.com"},
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
}
{
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}
8 changes: 7 additions & 1 deletion modules/caddypki/acmeserver/acmeserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ type Handler struct {
// "http-01", "dns-01", "tls-alpn-01"
Challenges ACMEChallenges `json:"challenges,omitempty" `

// The policy to use for issuing certificates
Policy *Policy `json:"policy,omitempty"`

logger *zap.Logger
resolvers []caddy.NetworkAddress
ctx caddy.Context
Expand Down Expand Up @@ -165,7 +168,10 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
&provisioner.ACME{
Name: ash.CA,
Challenges: ash.Challenges.toSmallstepType(),
Type: provisioner.TypeACME.String(),
Options: &provisioner.Options{
X509: ash.Policy.normalizeRules(),
},
Type: provisioner.TypeACME.String(),
Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365},
Expand Down
Loading

0 comments on commit 931656b

Please sign in to comment.