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
+}