@marcel91200 / caddy Mirror · read-only / commits / d7834676
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 13:30 UTC

tls: add `system` and `combined` CA pool modules (#7406)

* feat: add system and combined CA pool modules * fix: combining pools using `CertificateProvider` * fix: lint issue * chore: caddyfiletests * doing it for first time, so not sure if its right. * fix: use `x509` native addCert * chore: explicit err handling * Apply suggestion from @mohammed90 --------- Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

Harsh Patel committed Apr 6, 2026 at 03:43 UTC d7834676aac1c9718ca78ac4bab421f261fa789e
5 files changed +764 -15
caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool.caddyfiletest new
+87
@@ -0,0 +1,87 @@
1 +localhost
2 +
3 +respond "hello from localhost"
4 +tls {
5 + client_auth {
6 + mode request
7 + trust_pool combined {
8 + source inline {
9 + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
10 + }
11 + source file {
12 + pem_file ../caddy.ca.cer
13 + }
14 + }
15 + }
16 +}
17 +----------
18 +{
19 + "apps": {
20 + "http": {
21 + "servers": {
22 + "srv0": {
23 + "listen": [
24 + ":443"
25 + ],
26 + "routes": [
27 + {
28 + "match": [
29 + {
30 + "host": [
31 + "localhost"
32 + ]
33 + }
34 + ],
35 + "handle": [
36 + {
37 + "handler": "subroute",
38 + "routes": [
39 + {
40 + "handle": [
41 + {
42 + "body": "hello from localhost",
43 + "handler": "static_response"
44 + }
45 + ]
46 + }
47 + ]
48 + }
49 + ],
50 + "terminal": true
51 + }
52 + ],
53 + "tls_connection_policies": [
54 + {
55 + "match": {
56 + "sni": [
57 + "localhost"
58 + ]
59 + },
60 + "client_authentication": {
61 + "ca": {
62 + "provider": "combined",
63 + "sources": [
64 + {
65 + "provider": "inline",
66 + "trusted_ca_certs": [
67 + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
68 + ]
69 + },
70 + {
71 + "pem_files": [
72 + "../caddy.ca.cer"
73 + ],
74 + "provider": "file"
75 + }
76 + ]
77 + },
78 + "mode": "request"
79 + }
80 + },
81 + {}
82 + ]
83 + }
84 + }
85 + }
86 + }
87 +}
caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool_pki.caddyfiletest new
+87
@@ -0,0 +1,87 @@
1 +localhost
2 +
3 +respond "hello from localhost"
4 +tls {
5 + client_auth {
6 + mode require_and_verify
7 + trust_pool combined {
8 + source inline {
9 + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
10 + }
11 + source pki_root {
12 + authority local
13 + }
14 + }
15 + }
16 +}
17 +----------
18 +{
19 + "apps": {
20 + "http": {
21 + "servers": {
22 + "srv0": {
23 + "listen": [
24 + ":443"
25 + ],
26 + "routes": [
27 + {
28 + "match": [
29 + {
30 + "host": [
31 + "localhost"
32 + ]
33 + }
34 + ],
35 + "handle": [
36 + {
37 + "handler": "subroute",
38 + "routes": [
39 + {
40 + "handle": [
41 + {
42 + "body": "hello from localhost",
43 + "handler": "static_response"
44 + }
45 + ]
46 + }
47 + ]
48 + }
49 + ],
50 + "terminal": true
51 + }
52 + ],
53 + "tls_connection_policies": [
54 + {
55 + "match": {
56 + "sni": [
57 + "localhost"
58 + ]
59 + },
60 + "client_authentication": {
61 + "ca": {
62 + "provider": "combined",
63 + "sources": [
64 + {
65 + "provider": "inline",
66 + "trusted_ca_certs": [
67 + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
68 + ]
69 + },
70 + {
71 + "authority": [
72 + "local"
73 + ],
74 + "provider": "pki_root"
75 + }
76 + ]
77 + },
78 + "mode": "require_and_verify"
79 + }
80 + },
81 + {}
82 + ]
83 + }
84 + }
85 + }
86 + }
87 +}
caddytest/integration/caddyfile_adapt/tls_client_auth_system_trust_pool.caddyfiletest new
+66
@@ -0,0 +1,66 @@
1 +localhost
2 +
3 +respond "hello from localhost"
4 +tls {
5 + client_auth {
6 + mode request
7 + trust_pool system
8 + }
9 +}
10 +----------
11 +{
12 + "apps": {
13 + "http": {
14 + "servers": {
15 + "srv0": {
16 + "listen": [
17 + ":443"
18 + ],
19 + "routes": [
20 + {
21 + "match": [
22 + {
23 + "host": [
24 + "localhost"
25 + ]
26 + }
27 + ],
28 + "handle": [
29 + {
30 + "handler": "subroute",
31 + "routes": [
32 + {
33 + "handle": [
34 + {
35 + "body": "hello from localhost",
36 + "handler": "static_response"
37 + }
38 + ]
39 + }
40 + ]
41 + }
42 + ],
43 + "terminal": true
44 + }
45 + ],
46 + "tls_connection_policies": [
47 + {
48 + "match": {
49 + "sni": [
50 + "localhost"
51 + ]
52 + },
53 + "client_authentication": {
54 + "ca": {
55 + "provider": "system"
56 + },
57 + "mode": "request"
58 + }
59 + },
60 + {}
61 + ]
62 + }
63 + }
64 + }
65 + }
66 +}
modules/caddytls/capools.go
+307 -15
@@ -4,6 +4,7 @@ import (
4 "crypto/tls"
5 "crypto/x509"
6 "encoding/json"
7 + "encoding/pem"
8 "errors"
9 "fmt"
10 "io"
@@ -27,6 +28,8 @@ func init() {
28 caddy.RegisterModule(PKIIntermediateCAPool{})
29 caddy.RegisterModule(StoragePool{})
30 caddy.RegisterModule(HTTPCertPool{})
31 + caddy.RegisterModule(SystemCAPool{})
32 + caddy.RegisterModule(CombinedCAPool{})
33 }
34
35 // The interface to be implemented by all guest modules part of
@@ -35,6 +38,12 @@ type CA interface {
38 CertPool() *x509.CertPool
39 }
40
41 +// CertificateProvider is an optional interface that CA pool sources
42 +// can implement to expose their underlying certificates for combining.
43 +type CertificateProvider interface {
44 + Certificates() []*x509.Certificate
45 +}
46 +
47 // InlineCAPool is a certificate authority pool provider coming from
48 // a DER-encoded certificates in the config
49 type InlineCAPool struct {
@@ -44,7 +53,8 @@ type InlineCAPool struct {
53 // these CAs will be rejected.
54 TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
55
47 - pool *x509.CertPool
56 + pool *x509.CertPool
57 + certs []*x509.Certificate
58 }
59
60 // CaddyModule implements caddy.Module.
@@ -60,14 +70,17 @@ func (icp InlineCAPool) CaddyModule() caddy.ModuleInfo {
70 // Provision implements caddy.Provisioner.
71 func (icp *InlineCAPool) Provision(ctx caddy.Context) error {
72 caPool := x509.NewCertPool()
73 + var certs []*x509.Certificate
74 for i, clientCAString := range icp.TrustedCACerts {
75 clientCA, err := decodeBase64DERCert(clientCAString)
76 if err != nil {
77 return fmt.Errorf("parsing certificate at index %d: %v", i, err)
78 }
79 caPool.AddCert(clientCA)
80 + certs = append(certs, clientCA)
81 }
82 icp.pool = caPool
83 + icp.certs = certs
84
85 return nil
86 }
@@ -103,6 +116,11 @@ func (icp InlineCAPool) CertPool() *x509.CertPool {
116 return icp.pool
117 }
118
119 +// Certificates implements CertificateProvider.
120 +func (icp InlineCAPool) Certificates() []*x509.Certificate {
121 + return icp.certs
122 +}
123 +
124 // FileCAPool generates trusted root certificates pool from the designated DER and PEM file
125 type FileCAPool struct {
126 // TrustedCACertPEMFiles is a list of PEM file names
@@ -111,7 +129,8 @@ type FileCAPool struct {
129 // these CA certificates will be rejected.
130 TrustedCACertPEMFiles []string `json:"pem_files,omitempty"`
131
114 - pool *x509.CertPool
132 + pool *x509.CertPool
133 + certs []*x509.Certificate
134 }
135
136 // CaddyModule implements caddy.Module.
@@ -127,14 +146,32 @@ func (FileCAPool) CaddyModule() caddy.ModuleInfo {
146 // Loads and decodes the DER and pem files to generate the certificate pool
147 func (f *FileCAPool) Provision(ctx caddy.Context) error {
148 caPool := x509.NewCertPool()
149 + var certs []*x509.Certificate
150 for _, pemFile := range f.TrustedCACertPEMFiles {
151 pemContents, err := os.ReadFile(pemFile)
152 if err != nil {
153 return fmt.Errorf("reading %s: %v", pemFile, err)
154 }
135 - caPool.AppendCertsFromPEM(pemContents)
155 + // Parse PEM to extract certificates
156 + for len(pemContents) > 0 {
157 + var block *pem.Block
158 + block, pemContents = pem.Decode(pemContents)
159 + if block == nil {
160 + break
161 + }
162 + if block.Type != "CERTIFICATE" {
163 + continue
164 + }
165 + cert, err := x509.ParseCertificate(block.Bytes)
166 + if err != nil {
167 + return fmt.Errorf("parsing certificate in %s: %v", pemFile, err)
168 + }
169 + caPool.AddCert(cert)
170 + certs = append(certs, cert)
171 + }
172 }
173 f.pool = caPool
174 + f.certs = certs
175 return nil
176 }
177
@@ -166,13 +203,19 @@ func (f FileCAPool) CertPool() *x509.CertPool {
203 return f.pool
204 }
205
206 +// Certificates implements CertificateProvider.
207 +func (f FileCAPool) Certificates() []*x509.Certificate {
208 + return f.certs
209 +}
210 +
211 // PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app
212 type PKIRootCAPool struct {
213 // List of the Authority names that are configured in the `pki` app whose root certificates are trusted
214 Authority []string `json:"authority,omitempty"`
215
174 - ca []*caddypki.CA
175 - pool *x509.CertPool
216 + ca []*caddypki.CA
217 + pool *x509.CertPool
218 + certs []*x509.Certificate
219 }
220
221 // CaddyModule implements caddy.Module.
@@ -201,10 +244,17 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error {
244 }
245
246 caPool := x509.NewCertPool()
247 + var certs []*x509.Certificate
248 for _, ca := range p.ca {
205 - caPool.AddCert(ca.RootCertificate())
249 + rootCert := ca.RootCertificate()
250 + if rootCert == nil {
251 + return fmt.Errorf("CA %s has no root certificate", ca.ID)
252 + }
253 + caPool.AddCert(rootCert)
254 + certs = append(certs, rootCert)
255 }
256 p.pool = caPool
257 + p.certs = certs
258
259 return nil
260 }
@@ -238,13 +288,19 @@ func (p PKIRootCAPool) CertPool() *x509.CertPool {
288 return p.pool
289 }
290
291 +// Certificates implements CertificateProvider.
292 +func (p PKIRootCAPool) Certificates() []*x509.Certificate {
293 + return p.certs
294 +}
295 +
296 // PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app
297 type PKIIntermediateCAPool struct {
298 // List of the Authority names that are configured in the `pki` app whose intermediate certificates are trusted
299 Authority []string `json:"authority,omitempty"`
300
246 - ca []*caddypki.CA
247 - pool *x509.CertPool
301 + ca []*caddypki.CA
302 + pool *x509.CertPool
303 + certs []*x509.Certificate
304 }
305
306 // CaddyModule implements caddy.Module.
@@ -273,12 +329,18 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
329 }
330
331 caPool := x509.NewCertPool()
332 + var certs []*x509.Certificate
333 for _, ca := range p.ca {
334 for _, c := range ca.IntermediateCertificateChain() {
335 + if c == nil {
336 + return fmt.Errorf("CA %s has a nil certificate in its intermediate chain", ca.ID)
337 + }
338 caPool.AddCert(c)
339 + certs = append(certs, c)
340 }
341 }
342 p.pool = caPool
343 + p.certs = certs
344 return nil
345 }
346
@@ -311,6 +373,11 @@ func (p PKIIntermediateCAPool) CertPool() *x509.CertPool {
373 return p.pool
374 }
375
376 +// Certificates implements CertificateProvider.
377 +func (p PKIIntermediateCAPool) Certificates() []*x509.Certificate {
378 + return p.certs
379 +}
380 +
381 // StoragePool extracts the trusted certificates root from Caddy storage
382 type StoragePool struct {
383 // The storage module where the trusted root certificates are stored. Absent
@@ -322,6 +389,7 @@ type StoragePool struct {
389
390 storage certmagic.Storage
391 pool *x509.CertPool
392 + certs []*x509.Certificate
393 }
394
395 // CaddyModule implements caddy.Module.
@@ -354,16 +422,33 @@ func (ca *StoragePool) Provision(ctx caddy.Context) error {
422 return fmt.Errorf("no PEM keys specified")
423 }
424 caPool := x509.NewCertPool()
425 + var certs []*x509.Certificate
426 for _, caID := range ca.PEMKeys {
427 bs, err := ca.storage.Load(ctx, caID)
428 if err != nil {
429 return fmt.Errorf("error loading cert '%s' from storage: %s", caID, err)
430 }
362 - if !caPool.AppendCertsFromPEM(bs) {
363 - return fmt.Errorf("failed to add certificate '%s' to pool", caID)
431 + // Parse PEM to extract certificates
432 + pemData := bs
433 + for len(pemData) > 0 {
434 + var block *pem.Block
435 + block, pemData = pem.Decode(pemData)
436 + if block == nil {
437 + break
438 + }
439 + if block.Type != "CERTIFICATE" {
440 + continue
441 + }
442 + cert, err := x509.ParseCertificate(block.Bytes)
443 + if err != nil {
444 + return fmt.Errorf("parsing certificate '%s': %v", caID, err)
445 + }
446 + caPool.AddCert(cert)
447 + certs = append(certs, cert)
448 }
449 }
450 ca.pool = caPool
451 + ca.certs = certs
452
453 return nil
454 }
@@ -413,9 +498,13 @@ func (p StoragePool) CertPool() *x509.CertPool {
498 return p.pool
499 }
500
501 +// Certificates implements CertificateProvider.
502 +func (p StoragePool) Certificates() []*x509.Certificate {
503 + return p.certs
504 +}
505 +
506 // TLSConfig holds configuration related to the TLS configuration for the
507 // transport/client.
418 -// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
508 type TLSConfig struct {
509 // Provides the guest module that provides the trusted certificate authority (CA) certificates
510 CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
@@ -500,7 +589,6 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error {
589
590 // MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
591 // If there is no custom TLS configuration, a nil config may be returned.
503 -// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
592 func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
593 repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
594 if !ok || repl == nil {
@@ -554,7 +642,8 @@ type HTTPCertPool struct {
642 // Customize the TLS connection knobs to used during the HTTP call
643 TLS *TLSConfig `json:"tls,omitempty"`
644
557 - pool *x509.CertPool
645 + pool *x509.CertPool
646 + certs []*x509.Certificate
647 }
648
649 // CaddyModule implements caddy.Module.
@@ -570,6 +659,7 @@ func (HTTPCertPool) CaddyModule() caddy.ModuleInfo {
659 // Provision implements caddy.Provisioner.
660 func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
661 caPool := x509.NewCertPool()
662 + var certs []*x509.Certificate
663
664 customTransport := http.DefaultTransport.(*http.Transport).Clone()
665 if hcp.TLS != nil {
@@ -597,11 +687,30 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
687 if err != nil {
688 return err
689 }
600 - if !caPool.AppendCertsFromPEM(pembs) {
601 - return fmt.Errorf("failed to add certs from URL: %s", uri)
690 + if res.StatusCode < 200 || res.StatusCode >= 300 {
691 + return fmt.Errorf("HTTP %d fetching CA certificate bundle from %s", res.StatusCode, uri)
692 + }
693 + // Parse PEM to extract certificates
694 + pemData := pembs
695 + for len(pemData) > 0 {
696 + var block *pem.Block
697 + block, pemData = pem.Decode(pemData)
698 + if block == nil {
699 + break
700 + }
701 + if block.Type != "CERTIFICATE" {
702 + continue
703 + }
704 + cert, err := x509.ParseCertificate(block.Bytes)
705 + if err != nil {
706 + return fmt.Errorf("parsing certificate from URL %s: %v", uri, err)
707 + }
708 + caPool.AddCert(cert)
709 + certs = append(certs, cert)
710 }
711 }
712 hcp.pool = caPool
713 + hcp.certs = certs
714 return nil
715 }
716
@@ -665,6 +774,179 @@ func (hcp HTTPCertPool) CertPool() *x509.CertPool {
774 return hcp.pool
775 }
776
777 +// Certificates implements CertificateProvider.
778 +func (hcp HTTPCertPool) Certificates() []*x509.Certificate {
779 + return hcp.certs
780 +}
781 +
782 +// SystemCAPool obtains the trusted root certificates from the system's
783 +// certificate pool using x509.SystemCertPool()
784 +type SystemCAPool struct {
785 + pool *x509.CertPool
786 +}
787 +
788 +// CaddyModule implements caddy.Module.
789 +func (SystemCAPool) CaddyModule() caddy.ModuleInfo {
790 + return caddy.ModuleInfo{
791 + ID: "tls.ca_pool.source.system",
792 + New: func() caddy.Module {
793 + return new(SystemCAPool)
794 + },
795 + }
796 +}
797 +
798 +// Provision implements caddy.Provisioner.
799 +func (scp *SystemCAPool) Provision(ctx caddy.Context) error {
800 + pool, err := x509.SystemCertPool()
801 + if err != nil {
802 + return fmt.Errorf("failed to load system cert pool: %v", err)
803 + }
804 + scp.pool = pool
805 + return nil
806 +}
807 +
808 +func (scp *SystemCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
809 + d.Next() // consume module name
810 + if d.CountRemainingArgs() > 0 {
811 + return d.ArgErr()
812 + }
813 + if d.NextBlock(0) {
814 + return d.Err("system trust pool does not support any configuration")
815 + }
816 + return nil
817 +}
818 +
819 +// CertPool implements CA.
820 +func (scp SystemCAPool) CertPool() *x509.CertPool {
821 + return scp.pool
822 +}
823 +
824 +// The `combined` pool type merges multiple pools. The `sources` pools must implement the
825 +// `CertificateProvider` interface, which allows them to export their certificate set.
826 +//
827 +// Note: SystemCAPool does not implement CertificateProvider because
828 +// x509.SystemCertPool() doesn't expose its certificates, so it cannot
829 +// be used as a source in CombinedCAPool.
830 +type CombinedCAPool struct {
831 + // The CA pool sources to combine. Each source is a CA pool provider module.
832 + SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
833 +
834 + sources []CA
835 + pool *x509.CertPool
836 + certs []*x509.Certificate
837 +}
838 +
839 +// CaddyModule implements caddy.Module.
840 +func (CombinedCAPool) CaddyModule() caddy.ModuleInfo {
841 + return caddy.ModuleInfo{
842 + ID: "tls.ca_pool.source.combined",
843 + New: func() caddy.Module {
844 + return new(CombinedCAPool)
845 + },
846 + }
847 +}
848 +
849 +// Provision implements caddy.Provisioner.
850 +func (ccp *CombinedCAPool) Provision(ctx caddy.Context) error {
851 + if len(ccp.SourcesRaw) == 0 {
852 + return fmt.Errorf("no sources specified for combined CA pool")
853 + }
854 +
855 + // Load all source modules
856 + sources, err := ctx.LoadModule(ccp, "SourcesRaw")
857 + if err != nil {
858 + return fmt.Errorf("loading CA pool sources: %v", err)
859 + }
860 +
861 + caPool := x509.NewCertPool()
862 + var allCerts []*x509.Certificate
863 +
864 + for _, src := range sources.([]any) {
865 + ca, ok := src.(CA)
866 + if !ok {
867 + return fmt.Errorf("source module is not a CA pool provider")
868 + }
869 + ccp.sources = append(ccp.sources, ca)
870 +
871 + certProvider, ok := ca.(CertificateProvider)
872 + if !ok {
873 + return fmt.Errorf("source %T does not implement CertificateProvider (required for combining)", ca)
874 + }
875 +
876 + certs := certProvider.Certificates()
877 + if certs == nil {
878 + return fmt.Errorf("source %T returned nil certificates", ca)
879 + }
880 + for _, cert := range certs {
881 + if cert == nil {
882 + return fmt.Errorf("source %T returned a nil certificate", ca)
883 + }
884 + caPool.AddCert(cert)
885 + allCerts = append(allCerts, cert)
886 + }
887 + }
888 +
889 + ccp.pool = caPool
890 + ccp.certs = allCerts
891 +
892 + return nil
893 +}
894 +
895 +// Syntax:
896 +//
897 +// trust_pool combined {
898 +// source <module_name> {
899 +// <module_config>
900 +// }
901 +// }
902 +//
903 +// The 'source' directive can be specified multiple times. Sources that
904 +// don't implement CertificateProvider (like 'system') cannot be combined.
905 +func (ccp *CombinedCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
906 + d.Next() // consume module name
907 + if d.CountRemainingArgs() > 0 {
908 + return d.ArgErr()
909 + }
910 +
911 + for nesting := d.Nesting(); d.NextBlock(nesting); {
912 + switch d.Val() {
913 + case "source":
914 + if !d.NextArg() {
915 + return d.ArgErr()
916 + }
917 + modStem := d.Val()
918 + modID := "tls.ca_pool.source." + modStem
919 + unm, err := caddyfile.UnmarshalModule(d, modID)
920 + if err != nil {
921 + return err
922 + }
923 + ca, ok := unm.(CA)
924 + if !ok {
925 + return d.Errf("module %s is not a CA pool provider", modID)
926 + }
927 + ccp.SourcesRaw = append(ccp.SourcesRaw, caddyconfig.JSONModuleObject(ca, "provider", modStem, nil))
928 + default:
929 + return d.Errf("unrecognized directive: %s", d.Val())
930 + }
931 + }
932 +
933 + if len(ccp.SourcesRaw) == 0 {
934 + return d.Err("no sources specified")
935 + }
936 +
937 + return nil
938 +}
939 +
940 +// CertPool implements CA.
941 +func (ccp CombinedCAPool) CertPool() *x509.CertPool {
942 + return ccp.pool
943 +}
944 +
945 +// Certificates implements CertificateProvider.
946 +func (ccp CombinedCAPool) Certificates() []*x509.Certificate {
947 + return ccp.certs
948 +}
949 +
950 var (
951 _ caddy.Module = (*InlineCAPool)(nil)
952 _ caddy.Provisioner = (*InlineCAPool)(nil)
@@ -696,4 +978,14 @@ var (
978 _ caddy.Validator = (*HTTPCertPool)(nil)
979 _ CA = (*HTTPCertPool)(nil)
980 _ caddyfile.Unmarshaler = (*HTTPCertPool)(nil)
981 +
982 + _ caddy.Module = (*SystemCAPool)(nil)
983 + _ caddy.Provisioner = (*SystemCAPool)(nil)
984 + _ CA = (*SystemCAPool)(nil)
985 + _ caddyfile.Unmarshaler = (*SystemCAPool)(nil)
986 +
987 + _ caddy.Module = (*CombinedCAPool)(nil)
988 + _ caddy.Provisioner = (*CombinedCAPool)(nil)
989 + _ CA = (*CombinedCAPool)(nil)
990 + _ caddyfile.Unmarshaler = (*CombinedCAPool)(nil)
991 )
modules/caddytls/capools_test.go
+217
@@ -1,6 +1,7 @@
1 package caddytls
2
3 import (
4 + "context"
5 "encoding/json"
6 "fmt"
7 "reflect"
@@ -776,3 +777,219 @@ func TestHTTPCertPoolUnmarshalCaddyfile(t *testing.T) {
777 })
778 }
779 }
780 +
781 +func TestSystemCAPoolUnmarshalCaddyfile(t *testing.T) {
782 + type args struct {
783 + d *caddyfile.Dispenser
784 + }
785 + tests := []struct {
786 + name string
787 + args args
788 + wantErr bool
789 + }{
790 + {
791 + name: "basic system pool configuration",
792 + args: args{
793 + d: caddyfile.NewTestDispenser(`system`),
794 + },
795 + wantErr: false,
796 + },
797 + {
798 + name: "system pool with arguments produces error",
799 + args: args{
800 + d: caddyfile.NewTestDispenser(`system foo`),
801 + },
802 + wantErr: true,
803 + },
804 + {
805 + name: "system pool with block produces error",
806 + args: args{
807 + d: caddyfile.NewTestDispenser(`system {
808 + foo bar
809 + }`),
810 + },
811 + wantErr: true,
812 + },
813 + }
814 + for _, tt := range tests {
815 + t.Run(tt.name, func(t *testing.T) {
816 + scp := &SystemCAPool{}
817 + if err := scp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr {
818 + t.Errorf("SystemCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
819 + }
820 + })
821 + }
822 +}
823 +
824 +func TestCombinedCAPoolUnmarshalCaddyfile(t *testing.T) {
825 + type args struct {
826 + d *caddyfile.Dispenser
827 + }
828 + tests := []struct {
829 + name string
830 + args args
831 + wantErr bool
832 + }{
833 + {
834 + name: "empty block produces error",
835 + args: args{
836 + d: caddyfile.NewTestDispenser(`combined {
837 + }`),
838 + },
839 + wantErr: true,
840 + },
841 + {
842 + name: "arguments on same line as module name produces error",
843 + args: args{
844 + d: caddyfile.NewTestDispenser(`combined foo`),
845 + },
846 + wantErr: true,
847 + },
848 + {
849 + name: "single source - system",
850 + args: args{
851 + d: caddyfile.NewTestDispenser(`combined {
852 + source system
853 + }`),
854 + },
855 + wantErr: false,
856 + },
857 + {
858 + name: "single source - inline with config",
859 + args: args{
860 + d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined {
861 + source inline {
862 + trust_der %s
863 + }
864 + }`, test_der_1)),
865 + },
866 + wantErr: false,
867 + },
868 + {
869 + name: "multiple sources produces error due to limitation",
870 + args: args{
871 + d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined {
872 + source system
873 + source inline {
874 + trust_der %s
875 + }
876 + }`, test_der_1)),
877 + },
878 + wantErr: false, // UnmarshalCaddyfile succeeds, but Provision will fail
879 + },
880 + {
881 + name: "source without module name produces error",
882 + args: args{
883 + d: caddyfile.NewTestDispenser(`combined {
884 + source
885 + }`),
886 + },
887 + wantErr: true,
888 + },
889 + {
890 + name: "invalid directive produces error",
891 + args: args{
892 + d: caddyfile.NewTestDispenser(`combined {
893 + invalid_directive foo
894 + }`),
895 + },
896 + wantErr: true,
897 + },
898 + }
899 + for _, tt := range tests {
900 + t.Run(tt.name, func(t *testing.T) {
901 + ccp := &CombinedCAPool{}
902 + if err := ccp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr {
903 + t.Errorf("CombinedCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
904 + }
905 + if !tt.wantErr && len(ccp.SourcesRaw) == 0 {
906 + t.Errorf("CombinedCAPool.UnmarshalCaddyfile() produced no sources")
907 + }
908 + })
909 + }
910 +}
911 +
912 +func TestSystemCAPoolProvision(t *testing.T) {
913 + scp := &SystemCAPool{}
914 + ctx := caddy.Context{Context: context.Background()}
915 +
916 + err := scp.Provision(ctx)
917 + if err != nil {
918 + t.Errorf("SystemCAPool.Provision() error = %v", err)
919 + }
920 +
921 + if scp.pool == nil {
922 + t.Error("SystemCAPool.Provision() did not create a cert pool")
923 + }
924 +
925 + pool := scp.CertPool()
926 + if pool == nil {
927 + t.Error("SystemCAPool.CertPool() returned nil")
928 + }
929 +}
930 +
931 +func TestCombinedCAPoolProvisionWithSystemFails(t *testing.T) {
932 + // Test that combining system pool fails during Provision
933 + // because SystemCAPool doesn't implement CertificateProvider
934 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
935 + defer cancel()
936 +
937 + // Create a combined pool with system source
938 + ccp := &CombinedCAPool{
939 + SourcesRaw: []json.RawMessage{
940 + json.RawMessage(`{"provider":"system"}`),
941 + },
942 + }
943 +
944 + err := ccp.Provision(ctx)
945 + if err == nil {
946 + t.Error("CombinedCAPool.Provision() with system source should fail, but succeeded")
947 + }
948 +
949 + // Verify error message mentions CertificateProvider
950 + if err != nil && !contains(err.Error(), "CertificateProvider") {
951 + t.Errorf("Expected error to mention CertificateProvider, got: %v", err)
952 + }
953 +}
954 +
955 +func TestCombinedCAPoolProvisionWithInlineSucceeds(t *testing.T) {
956 + // Test that combining inline pools works
957 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
958 + defer cancel()
959 +
960 + // Create a combined pool with inline source
961 + ccp := &CombinedCAPool{
962 + SourcesRaw: []json.RawMessage{
963 + json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)),
964 + },
965 + }
966 +
967 + err := ccp.Provision(ctx)
968 + if err != nil {
969 + t.Errorf("CombinedCAPool.Provision() with inline source failed: %v", err)
970 + }
971 +
972 + if ccp.pool == nil {
973 + t.Error("CombinedCAPool.Provision() did not create a cert pool")
974 + }
975 +
976 + pool := ccp.CertPool()
977 + if pool == nil {
978 + t.Error("CombinedCAPool.CertPool() returned nil")
979 + }
980 +}
981 +
982 +// Helper function for string contains check
983 +func contains(s, substr string) bool {
984 + return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
985 + (len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
986 +}
987 +
988 +func findSubstring(s, substr string) bool {
989 + for i := 0; i <= len(s)-len(substr); i++ {
990 + if s[i:i+len(substr)] == substr {
991 + return true
992 + }
993 + }
994 + return false
995 +}