Repository: caddyserver/caddy (language: go)

Issue:
Providing Trusted Leaf Certificates to the Leaf Certificate Verifier
The [leaf-certificate verifier](https://caddyserver.com/docs/json/apps/http/servers/tls_connection_policies/client_authentication/verifiers/leaf/) in client authentication does not have a way to provide the trusted leaf certificate material without relying no the deprecated field [`trusted_leaf_certs`](https://caddyserver.com/docs/json/apps/http/servers/tls_connection_policies/client_authentication/#trusted_leaf_certs), which was not meant to be the proxy forever and only left around as a bridge for existing users and not to be used after the introduction of the feature in #4389. At the moment, there's no way to configure the `"verifier": "leaf"`directly with new behavior only.

We should provide a way to provide the trusted certificates to the `tls.client_auth.leaf` module.


Discussion / hints:
@mohammed90 I can take a look at this, unless you want to leave it for someone doing their first contribution. 
Go for it!
Cool. I have a couple of questions though to make sure I understand this right. 
 
1.  Currently, what happens when we include the `{ "verifier" : "leaf"}` in our config without using the deprecated `trusted_leaf_certs` field? I am assuming it doesn't do anything? 

2. How do you envision this being implemented? Should it be a sub-directive of the verifier directive? 
> * Currently, what happens when we include the `{ "verifier" : "leaf"}` in our config without using the deprecated `trusted_leaf_certs` field? I am assuming it doesn't do anything?

Per the code here:
https://github.com/caddyserver/caddy/blob/4181c79a8130a59c40c76437e15265452422ccb1/modules/caddytls/connpolicy.go#L561-L567

It'll return an error in the last line

> * How do you envision this being implemented? Should it be a sub-directive of the verifier directive?

Start by figuring out the JSON first, not the Caddyfile. Consider that the source of the certificates can be from various sources, so it should be pluggable.

Fix patch:
diff --git a/go.mod b/go.mod
index 0d02c289040..8760d835112 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 b4e081a3e74..1c7d91d07f5 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 20b781274d2..49c7add49d3 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 00000000000..1d3f3a3e59f
--- /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/leaffolderloader.go b/modules/caddytls/leaffolderloader.go
new file mode 100644
index 00000000000..5c7b06e7681
--- /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/leafpemloader.go b/modules/caddytls/leafpemloader.go
new file mode 100644
index 00000000000..28467ccf2c7
--- /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/leafstorageloader.go b/modules/caddytls/leafstorageloader.go
new file mode 100644
index 00000000000..0215c8af2a6
--- /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)
+)


Test patch:
diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go
new file mode 100644
index 00000000000..592c3f8690c
--- /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 00000000000..03febfd3ae1
--- /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/modules/caddytls/leaffileloader_test.go b/modules/caddytls/leaffileloader_test.go
new file mode 100644
index 00000000000..940ed78bd70
--- /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_test.go b/modules/caddytls/leaffolderloader_test.go
new file mode 100644
index 00000000000..35fecba89b3
--- /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_test.go b/modules/caddytls/leafpemloader_test.go
new file mode 100644
index 00000000000..04a9efd2531
--- /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")
+	}
+}