aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAziz Rmadi <[email protected]>2024-03-05 15:55:37 -0600
committerGitHub <[email protected]>2024-03-05 14:55:37 -0700
commit3ae07a73dc057c3a12486b78872c5e1391ec7cc9 (patch)
tree4904adfcb5da3d9d1dcdca65cb7af6d8e2ea83a2
parente473ae6803a95a8e85ba867d1fa1d205d98b73d8 (diff)
downloadcaddy-3ae07a73dc057c3a12486b78872c5e1391ec7cc9.tar.gz
caddy-3ae07a73dc057c3a12486b78872c5e1391ec7cc9.zip
caddytls: clientauth: leaf verifier: make trusted leaf certs source pluggable (#6050)
* Made trusted leaf certificates pluggable into the tls.client_auth.leaf module * Added leaf loaders modules: file, folder, pem aand storage * Cleaned implementation of leaf cert loader modules * Added tests for leaf certs file and folder loaders * cmd: fix the output of the `Usage` section (#6138) * core: OnExit hooks (#6128) * core: OnExit callbacks * core: Process-global OnExit callbacks * ci: bump golangci/golangci-lint-action from 3 to 4 (#6141) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Added more leaf certificate loaders tests and cleaned up code * Modified leaf cert loaders json field names and cleaned up storage loader comment * Update modules/caddytls/leaffileloader.go * Update LeafStorageLoader certificates field name * Upgraded protobuf version --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Mohammed Al Sahaf <[email protected]> Co-authored-by: Matt Holt <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
-rw-r--r--caddytest/integration/leafcertloaders_test.go67
-rw-r--r--caddytest/leafcert.pem15
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--modules/caddytls/connpolicy.go37
-rw-r--r--modules/caddytls/leaffileloader.go99
-rw-r--r--modules/caddytls/leaffileloader_test.go38
-rw-r--r--modules/caddytls/leaffolderloader.go97
-rw-r--r--modules/caddytls/leaffolderloader_test.go37
-rw-r--r--modules/caddytls/leafpemloader.go76
-rw-r--r--modules/caddytls/leafpemloader_test.go54
-rw-r--r--modules/caddytls/leafstorageloader.go129
12 files changed, 649 insertions, 4 deletions
diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go
new file mode 100644
index 000000000..592c3f869
--- /dev/null
+++ b/caddytest/integration/leafcertloaders_test.go
@@ -0,0 +1,67 @@
+package integration
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy/v2/caddytest"
+)
+
+func TestLeafCertLoaders(t *testing.T) {
+ tester := caddytest.NewTester(t)
+ tester.InitServer(`
+ {
+ "admin": {
+ "listen": "localhost:2999"
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "localhost"
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "tls_connection_policies": [
+ {
+ "client_authentication": {
+ "verifiers": [
+ {
+ "verifier": "leaf",
+ "leaf_certs_loaders": [
+ {
+ "loader": "file",
+ "files": ["../leafcert.pem"]
+ },
+ {
+ "loader": "folder",
+ "folders": ["../"]
+ },
+ {
+ "loader": "storage"
+ },
+ {
+ "loader": "pem"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }`, "json")
+}
diff --git a/caddytest/leafcert.pem b/caddytest/leafcert.pem
new file mode 100644
index 000000000..03febfd3a
--- /dev/null
+++ b/caddytest/leafcert.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
+MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC
+VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx
+NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD
+TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu
+ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j
+V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj
+gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA
+FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE
+CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS
+BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE
+BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju
+Wm7DCfrPNGVwFWUQOmsPue9rZBgO
+-----END CERTIFICATE-----
diff --git a/go.mod b/go.mod
index 0d02c2890..8760d8351 100644
--- a/go.mod
+++ b/go.mod
@@ -148,7 +148,7 @@ require (
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/grpc v1.60.1 // indirect
- google.golang.org/protobuf v1.31.0 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
howett.net/plist v1.0.0 // indirect
)
diff --git a/go.sum b/go.sum
index b4e081a3e..1c7d91d07 100644
--- a/go.sum
+++ b/go.sum
@@ -855,6 +855,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index 20b781274..49c7add49 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -651,7 +651,7 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
}
trustedLeafCerts = append(trustedLeafCerts, clientCert)
}
- clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts})
+ clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{trustedLeafCerts: trustedLeafCerts})
}
// if a custom verification function already exists, wrap it
@@ -715,7 +715,8 @@ func setDefaultTLSParams(cfg *tls.Config) {
// LeafCertClientAuth verifies the client's leaf certificate.
type LeafCertClientAuth struct {
- TrustedLeafCerts []*x509.Certificate
+ LeafCertificateLoadersRaw []json.RawMessage `json:"leaf_certs_loaders,omitempty" caddy:"namespace=tls.leaf_cert_loader inline_key=loader"`
+ trustedLeafCerts []*x509.Certificate
}
// CaddyModule returns the Caddy module information.
@@ -726,6 +727,30 @@ func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo {
}
}
+func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error {
+ if l.LeafCertificateLoadersRaw == nil {
+ return nil
+ }
+ val, err := ctx.LoadModule(l, "LeafCertificateLoadersRaw")
+ if err != nil {
+ return fmt.Errorf("could not parse leaf certificates loaders: %s", err.Error())
+ }
+ trustedLeafCertloaders := []LeafCertificateLoader{}
+ for _, loader := range val.([]any) {
+ trustedLeafCertloaders = append(trustedLeafCertloaders, loader.(LeafCertificateLoader))
+ }
+ trustedLeafCertificates := []*x509.Certificate{}
+ for _, loader := range trustedLeafCertloaders {
+ certs, err := loader.LoadLeafCertificates()
+ if err != nil {
+ return fmt.Errorf("could not load leaf certificates: %s", err.Error())
+ }
+ trustedLeafCertificates = append(trustedLeafCertificates, certs...)
+ }
+ l.trustedLeafCerts = trustedLeafCertificates
+ return nil
+}
+
func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
@@ -736,7 +761,7 @@ func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x5
return fmt.Errorf("can't parse the given certificate: %s", err.Error())
}
- for _, trustedLeafCert := range l.TrustedLeafCerts {
+ for _, trustedLeafCert := range l.trustedLeafCerts {
if remoteLeafCert.Equal(trustedLeafCert) {
return nil
}
@@ -765,6 +790,12 @@ type ConnectionMatcher interface {
Match(*tls.ClientHelloInfo) bool
}
+// LeafCertificateLoader is a type that loads the trusted leaf certificates
+// for the tls.leaf_cert_loader modules
+type LeafCertificateLoader interface {
+ LoadLeafCertificates() ([]*x509.Certificate, error)
+}
+
// ClientCertificateVerifier is a type which verifies client certificates.
// It is called during verifyPeerCertificate in the TLS handshake.
type ClientCertificateVerifier interface {
diff --git a/modules/caddytls/leaffileloader.go b/modules/caddytls/leaffileloader.go
new file mode 100644
index 000000000..1d3f3a3e5
--- /dev/null
+++ b/modules/caddytls/leaffileloader.go
@@ -0,0 +1,99 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddytls
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "os"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(LeafFileLoader{})
+}
+
+// LeafFileLoader loads leaf certificates from disk.
+type LeafFileLoader struct {
+ Files []string `json:"files,omitempty"`
+}
+
+// Provision implements caddy.Provisioner.
+func (fl *LeafFileLoader) Provision(ctx caddy.Context) error {
+ repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+ if !ok {
+ repl = caddy.NewReplacer()
+ }
+ for k, path := range fl.Files {
+ fl.Files[k] = repl.ReplaceKnown(path, "")
+ }
+ return nil
+}
+
+// CaddyModule returns the Caddy module information.
+func (LeafFileLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.leaf_cert_loader.file",
+ New: func() caddy.Module { return new(LeafFileLoader) },
+ }
+}
+
+// LoadLeafCertificates returns the certificates to be loaded by fl.
+func (fl LeafFileLoader) LoadLeafCertificates() ([]*x509.Certificate, error) {
+ certificates := make([]*x509.Certificate, 0, len(fl.Files))
+ for _, path := range fl.Files {
+ ders, err := convertPEMFilesToDERBytes(path)
+ if err != nil {
+ return nil, err
+ }
+ certs, err := x509.ParseCertificates(ders)
+ if err != nil {
+ return nil, err
+ }
+ certificates = append(certificates, certs...)
+ }
+ return certificates, nil
+}
+
+func convertPEMFilesToDERBytes(filename string) ([]byte, error) {
+ certDataPEM, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ var ders []byte
+ // while block is not nil, we have more certificates in the file
+ for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) {
+ if block.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename)
+ }
+ ders = append(
+ ders,
+ block.Bytes...,
+ )
+ }
+ // if we decoded nothing, return an error
+ if len(ders) == 0 {
+ return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename)
+ }
+ return ders, nil
+}
+
+// Interface guard
+var (
+ _ LeafCertificateLoader = (*LeafFileLoader)(nil)
+ _ caddy.Provisioner = (*LeafFileLoader)(nil)
+)
diff --git a/modules/caddytls/leaffileloader_test.go b/modules/caddytls/leaffileloader_test.go
new file mode 100644
index 000000000..940ed78bd
--- /dev/null
+++ b/modules/caddytls/leaffileloader_test.go
@@ -0,0 +1,38 @@
+package caddytls
+
+import (
+ "context"
+ "encoding/pem"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func TestLeafFileLoader(t *testing.T) {
+ fl := LeafFileLoader{Files: []string{"../../caddytest/leafcert.pem"}}
+ fl.Provision(caddy.Context{Context: context.Background()})
+
+ out, err := fl.LoadLeafCertificates()
+ if err != nil {
+ t.Errorf("Leaf certs file loading test failed: %v", err)
+ }
+ if len(out) != 1 {
+ t.Errorf("Error loading leaf cert in memory struct")
+ return
+ }
+ pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw})
+
+ pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem")
+ if err != nil {
+ t.Errorf("Unable to read the example certificate from the file")
+ }
+
+ // Remove /r because windows.
+ pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n")
+
+ if string(pemBytes) != pemFileString {
+ t.Errorf("Leaf Certificate File Loader: Failed to load the correct certificate")
+ }
+}
diff --git a/modules/caddytls/leaffolderloader.go b/modules/caddytls/leaffolderloader.go
new file mode 100644
index 000000000..5c7b06e76
--- /dev/null
+++ b/modules/caddytls/leaffolderloader.go
@@ -0,0 +1,97 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddytls
+
+import (
+ "crypto/x509"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(LeafFolderLoader{})
+}
+
+// LeafFolderLoader loads certificates and their associated keys from disk
+// by recursively walking the specified directories, looking for PEM
+// files which contain both a certificate and a key.
+type LeafFolderLoader struct {
+ Folders []string `json:"folders,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (LeafFolderLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.leaf_cert_loader.folder",
+ New: func() caddy.Module { return new(LeafFolderLoader) },
+ }
+}
+
+// Provision implements caddy.Provisioner.
+func (fl *LeafFolderLoader) Provision(ctx caddy.Context) error {
+ repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+ if !ok {
+ repl = caddy.NewReplacer()
+ }
+ for k, path := range fl.Folders {
+ fl.Folders[k] = repl.ReplaceKnown(path, "")
+ }
+ return nil
+}
+
+// LoadLeafCertificates loads all the leaf certificates in the directories
+// listed in fl from all files ending with .pem.
+func (fl LeafFolderLoader) LoadLeafCertificates() ([]*x509.Certificate, error) {
+ var certs []*x509.Certificate
+ for _, dir := range fl.Folders {
+ err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
+ if err != nil {
+ return fmt.Errorf("unable to traverse into path: %s", fpath)
+ }
+ if info.IsDir() {
+ return nil
+ }
+ if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
+ return nil
+ }
+
+ certData, err := convertPEMFilesToDERBytes(fpath)
+ if err != nil {
+ return err
+ }
+ cert, err := x509.ParseCertificate(certData)
+ if err != nil {
+ return fmt.Errorf("%s: %w", fpath, err)
+ }
+
+ certs = append(certs, cert)
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ return certs, nil
+}
+
+var (
+ _ LeafCertificateLoader = (*LeafFolderLoader)(nil)
+ _ caddy.Provisioner = (*LeafFolderLoader)(nil)
+)
diff --git a/modules/caddytls/leaffolderloader_test.go b/modules/caddytls/leaffolderloader_test.go
new file mode 100644
index 000000000..35fecba89
--- /dev/null
+++ b/modules/caddytls/leaffolderloader_test.go
@@ -0,0 +1,37 @@
+package caddytls
+
+import (
+ "context"
+ "encoding/pem"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func TestLeafFolderLoader(t *testing.T) {
+ fl := LeafFolderLoader{Folders: []string{"../../caddytest"}}
+ fl.Provision(caddy.Context{Context: context.Background()})
+
+ out, err := fl.LoadLeafCertificates()
+ if err != nil {
+ t.Errorf("Leaf certs folder loading test failed: %v", err)
+ }
+ if len(out) != 1 {
+ t.Errorf("Error loading leaf cert in memory struct")
+ return
+ }
+ pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw})
+ pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem")
+ if err != nil {
+ t.Errorf("Unable to read the example certificate from the file")
+ }
+
+ // Remove /r because windows.
+ pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n")
+
+ if string(pemBytes) != pemFileString {
+ t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate")
+ }
+}
diff --git a/modules/caddytls/leafpemloader.go b/modules/caddytls/leafpemloader.go
new file mode 100644
index 000000000..28467ccf2
--- /dev/null
+++ b/modules/caddytls/leafpemloader.go
@@ -0,0 +1,76 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddytls
+
+import (
+ "crypto/x509"
+ "fmt"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(LeafPEMLoader{})
+}
+
+// LeafPEMLoader loads leaf certificates by
+// decoding their PEM blocks directly. This has the advantage
+// of not needing to store them on disk at all.
+type LeafPEMLoader struct {
+ Certificates []string `json:"certificates,omitempty"`
+}
+
+// Provision implements caddy.Provisioner.
+func (pl *LeafPEMLoader) Provision(ctx caddy.Context) error {
+ repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+ if !ok {
+ repl = caddy.NewReplacer()
+ }
+ for i, cert := range pl.Certificates {
+ pl.Certificates[i] = repl.ReplaceKnown(cert, "")
+ }
+ return nil
+}
+
+// CaddyModule returns the Caddy module information.
+func (LeafPEMLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.leaf_cert_loader.pem",
+ New: func() caddy.Module { return new(LeafPEMLoader) },
+ }
+}
+
+// LoadLeafCertificates returns the certificates contained in pl.
+func (pl LeafPEMLoader) LoadLeafCertificates() ([]*x509.Certificate, error) {
+ certs := make([]*x509.Certificate, 0, len(pl.Certificates))
+ for i, cert := range pl.Certificates {
+ derBytes, err := convertPEMToDER([]byte(cert))
+ if err != nil {
+ return nil, fmt.Errorf("PEM leaf certificate loader, cert %d: %v", i, err)
+ }
+ cert, err := x509.ParseCertificate(derBytes)
+ if err != nil {
+ return nil, fmt.Errorf("PEM cert %d: %v", i, err)
+ }
+ certs = append(certs, cert)
+ }
+ return certs, nil
+}
+
+// Interface guard
+var (
+ _ LeafCertificateLoader = (*LeafPEMLoader)(nil)
+ _ caddy.Provisioner = (*LeafPEMLoader)(nil)
+)
diff --git a/modules/caddytls/leafpemloader_test.go b/modules/caddytls/leafpemloader_test.go
new file mode 100644
index 000000000..04a9efd25
--- /dev/null
+++ b/modules/caddytls/leafpemloader_test.go
@@ -0,0 +1,54 @@
+package caddytls
+
+import (
+ "context"
+ "encoding/pem"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func TestLeafPEMLoader(t *testing.T) {
+ pl := LeafPEMLoader{Certificates: []string{`
+-----BEGIN CERTIFICATE-----
+MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
+MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC
+VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx
+NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD
+TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu
+ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j
+V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj
+gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA
+FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE
+CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS
+BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE
+BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju
+Wm7DCfrPNGVwFWUQOmsPue9rZBgO
+-----END CERTIFICATE-----
+`}}
+ pl.Provision(caddy.Context{Context: context.Background()})
+
+ out, err := pl.LoadLeafCertificates()
+ if err != nil {
+ t.Errorf("Leaf certs pem loading test failed: %v", err)
+ }
+ if len(out) != 1 {
+ t.Errorf("Error loading leaf cert in memory struct")
+ return
+ }
+ pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw})
+
+ pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem")
+ if err != nil {
+ t.Errorf("Unable to read the example certificate from the file")
+ }
+
+ // Remove /r because windows.
+ pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n")
+
+ if string(pemBytes) != pemFileString {
+ t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate")
+ }
+}
diff --git a/modules/caddytls/leafstorageloader.go b/modules/caddytls/leafstorageloader.go
new file mode 100644
index 000000000..0215c8af2
--- /dev/null
+++ b/modules/caddytls/leafstorageloader.go
@@ -0,0 +1,129 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddytls
+
+import (
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+
+ "github.com/caddyserver/certmagic"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(LeafStorageLoader{})
+}
+
+// LeafStorageLoader loads leaf certificates from the
+// globally configured storage module.
+type LeafStorageLoader struct {
+ // A list of certificate file names to be loaded from storage.
+ Certificates []string `json:"certificates,omitempty"`
+
+ // The storage module where the trusted leaf certificates are stored. Absent
+ // explicit storage implies the use of Caddy default storage.
+ StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
+
+ // Reference to the globally configured storage module.
+ storage certmagic.Storage
+
+ ctx caddy.Context
+}
+
+// CaddyModule returns the Caddy module information.
+func (LeafStorageLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.leaf_cert_loader.storage",
+ New: func() caddy.Module { return new(LeafStorageLoader) },
+ }
+}
+
+// Provision loads the storage module for sl.
+func (sl *LeafStorageLoader) Provision(ctx caddy.Context) error {
+ if sl.StorageRaw != nil {
+ val, err := ctx.LoadModule(sl, "StorageRaw")
+ if err != nil {
+ return fmt.Errorf("loading storage module: %v", err)
+ }
+ cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return fmt.Errorf("creating storage configuration: %v", err)
+ }
+ sl.storage = cmStorage
+ }
+ if sl.storage == nil {
+ sl.storage = ctx.Storage()
+ }
+ sl.ctx = ctx
+
+ repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+ if !ok {
+ repl = caddy.NewReplacer()
+ }
+ for k, path := range sl.Certificates {
+ sl.Certificates[k] = repl.ReplaceKnown(path, "")
+ }
+ return nil
+}
+
+// LoadLeafCertificates returns the certificates to be loaded by sl.
+func (sl LeafStorageLoader) LoadLeafCertificates() ([]*x509.Certificate, error) {
+ certificates := make([]*x509.Certificate, 0, len(sl.Certificates))
+ for _, path := range sl.Certificates {
+ certData, err := sl.storage.Load(sl.ctx, path)
+ if err != nil {
+ return nil, err
+ }
+
+ ders, err := convertPEMToDER(certData)
+ if err != nil {
+ return nil, err
+ }
+ certs, err := x509.ParseCertificates(ders)
+ if err != nil {
+ return nil, err
+ }
+ certificates = append(certificates, certs...)
+ }
+ return certificates, nil
+}
+
+func convertPEMToDER(pemData []byte) ([]byte, error) {
+ var ders []byte
+ // while block is not nil, we have more certificates in the file
+ for block, rest := pem.Decode(pemData); block != nil; block, rest = pem.Decode(rest) {
+ if block.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data")
+ }
+ ders = append(
+ ders,
+ block.Bytes...,
+ )
+ }
+ // if we decoded nothing, return an error
+ if len(ders) == 0 {
+ return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data")
+ }
+ return ders, nil
+}
+
+// Interface guard
+var (
+ _ LeafCertificateLoader = (*LeafStorageLoader)(nil)
+ _ caddy.Provisioner = (*LeafStorageLoader)(nil)
+)