diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go new file mode 100644 index 00000000..592c3f86 --- /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 00000000..03febfd3 --- /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 0d02c289..8760d835 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 b4e081a3..1c7d91d0 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 20b78127..49c7add4 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 00000000..1d3f3a3e --- /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 00000000..940ed78b --- /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 00000000..5c7b06e7 --- /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 00000000..35fecba8 --- /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 00000000..28467ccf --- /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 00000000..04a9efd2 --- /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 00000000..0215c8af --- /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) +)