@marcel91200 / caddy Mirror · read-only / commits / 7ebe72bb
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, 18:30 UTC

caddypki: Add support for multiple intermediates in signing chain (#7057)

* caddypki: Add support for multiple intermediates in signing chain * Move intermediate lifetime configuration check In #7272 a check was changed to ensure that generated intermediate certificates would always use a lifetime that falls within the lifetime of the root. However, when a root and intermediate(s) are supplied, the configuration value was being used instead of the actual lifetimes of the certificates. The check was moved to only be performed when an intermediate is generated; not when loaded from disk. * Add tests for `pemDecodeCertificateChain` and `pemDecodeCertificate` * Use `crypto.Signer` instead of `any` in appropriate places * Use latest Smallstep packages --------- Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Herman Slatman committed Dec 3, 2025 at 19:30 UTC 7ebe72bbfe342688a325325ea79e46970d017eb7
9 files changed +700 -48
go.mod
+1 -1
@@ -35,6 +35,7 @@ require (
35 go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
36 go.opentelemetry.io/otel v1.38.0
37 go.opentelemetry.io/otel/sdk v1.38.0
38 + go.step.sm/crypto v0.74.0
39 go.uber.org/automaxprocs v1.6.0
40 go.uber.org/zap v1.27.1
41 go.uber.org/zap/exp v0.3.0
@@ -166,7 +167,6 @@ require (
167 go.opentelemetry.io/otel/metric v1.38.0 // indirect
168 go.opentelemetry.io/otel/trace v1.38.0
169 go.opentelemetry.io/proto/otlp v1.7.1 // indirect
169 - go.step.sm/crypto v0.74.0
170 go.uber.org/multierr v1.11.0 // indirect
171 golang.org/x/mod v0.29.0 // indirect
172 golang.org/x/sys v0.38.0
modules/caddypki/adminapi.go
+9 -4
@@ -222,11 +222,16 @@ func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
222 if err != nil {
223 return root, inter, err
224 }
225 - inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
226 - if err != nil {
227 - return root, inter, err
225 +
226 + for _, interCert := range ca.IntermediateCertificateChain() {
227 + pemBytes, err := pemEncodeCert(interCert.Raw)
228 + if err != nil {
229 + return nil, nil, err
230 + }
231 + inter = append(inter, pemBytes...)
232 }
229 - return root, inter, err
233 +
234 + return
235 }
236
237 // caInfo is the response structure for the CA info API endpoint.
modules/caddypki/ca.go
+43 -30
@@ -75,10 +75,11 @@ type CA struct {
75 // and module provisioning.
76 ID string `json:"-"`
77
78 - storage certmagic.Storage
79 - root, inter *x509.Certificate
80 - interKey any // TODO: should we just store these as crypto.Signer?
81 - mu *sync.RWMutex
78 + storage certmagic.Storage
79 + root *x509.Certificate
80 + interChain []*x509.Certificate
81 + interKey crypto.Signer
82 + mu *sync.RWMutex
83
84 rootCertPath string // mainly used for logging purposes if trusting
85 log *zap.Logger
@@ -127,14 +128,16 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
128 }
129
130 // load the certs and key that will be used for signing
130 - var rootCert, interCert *x509.Certificate
131 + var rootCert *x509.Certificate
132 + var rootCertChain, interCertChain []*x509.Certificate
133 var rootKey, interKey crypto.Signer
134 var err error
135 if ca.Root != nil {
136 if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
137 ca.rootCertPath = ca.Root.Certificate
138 }
137 - rootCert, rootKey, err = ca.Root.Load()
139 + rootCertChain, rootKey, err = ca.Root.Load()
140 + rootCert = rootCertChain[0]
141 } else {
142 ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
143 rootCert, rootKey, err = ca.loadOrGenRoot()
@@ -142,21 +145,23 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
145 if err != nil {
146 return err
147 }
145 - actualRootLifetime := time.Until(rootCert.NotAfter)
146 - if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime {
147 - return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime)
148 - }
148 +
149 if ca.Intermediate != nil {
150 - interCert, interKey, err = ca.Intermediate.Load()
150 + interCertChain, interKey, err = ca.Intermediate.Load()
151 } else {
152 - interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
152 + actualRootLifetime := time.Until(rootCert.NotAfter)
153 + if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime {
154 + return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime)
155 + }
156 +
157 + interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
158 }
159 if err != nil {
160 return err
161 }
162
163 ca.mu.Lock()
159 - ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey
164 + ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey
165 ca.mu.Unlock()
166
167 return nil
@@ -172,21 +177,21 @@ func (ca CA) RootCertificate() *x509.Certificate {
177 // RootKey returns the CA's root private key. Since the root key is
178 // not cached in memory long-term, it needs to be loaded from storage,
179 // which could yield an error.
175 -func (ca CA) RootKey() (any, error) {
180 +func (ca CA) RootKey() (crypto.Signer, error) {
181 _, rootKey, err := ca.loadOrGenRoot()
182 return rootKey, err
183 }
184
180 -// IntermediateCertificate returns the CA's intermediate
181 -// certificate (public key).
182 -func (ca CA) IntermediateCertificate() *x509.Certificate {
185 +// IntermediateCertificateChain returns the CA's intermediate
186 +// certificate chain.
187 +func (ca CA) IntermediateCertificateChain() []*x509.Certificate {
188 ca.mu.RLock()
189 defer ca.mu.RUnlock()
185 - return ca.inter
190 + return ca.interChain
191 }
192
193 // IntermediateKey returns the CA's intermediate private key.
189 -func (ca CA) IntermediateKey() any {
194 +func (ca CA) IntermediateKey() crypto.Signer {
195 ca.mu.RLock()
196 defer ca.mu.RUnlock()
197 return ca.interKey
@@ -207,26 +212,27 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit
212 // cert/key directly, since it's unlikely to expire
213 // while Caddy is running (long lifetime)
214 var issuerCert *x509.Certificate
210 - var issuerKey any
215 + var issuerKey crypto.Signer
216 issuerCert = rootCert
217 var err error
218 issuerKey, err = ca.RootKey()
219 if err != nil {
220 return nil, fmt.Errorf("loading signing key: %v", err)
221 }
217 - signerOption = authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer))
222 + signerOption = authority.WithX509Signer(issuerCert, issuerKey)
223 } else {
224 // if we're signing with intermediate, we need to make
225 // sure it's always fresh, because the intermediate may
226 // renew while Caddy is running (medium lifetime)
227 signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) {
223 - issuerCert := ca.IntermediateCertificate()
224 - issuerKey := ca.IntermediateKey().(crypto.Signer)
228 + issuerChain := ca.IntermediateCertificateChain()
229 + issuerCert := issuerChain[0]
230 + issuerKey := ca.IntermediateKey()
231 ca.log.Debug("using intermediate signer",
232 zap.String("serial", issuerCert.SerialNumber.String()),
233 zap.String("not_before", issuerCert.NotBefore.String()),
234 zap.String("not_after", issuerCert.NotAfter.String()))
229 - return []*x509.Certificate{issuerCert}, issuerKey, nil
235 + return issuerChain, issuerKey, nil
236 })
237 }
238
@@ -252,7 +258,11 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit
258
259 func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) {
260 if ca.Root != nil {
255 - return ca.Root.Load()
261 + rootChain, rootSigner, err := ca.Root.Load()
262 + if err != nil {
263 + return nil, nil, err
264 + }
265 + return rootChain[0], rootSigner, nil
266 }
267 rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert())
268 if err != nil {
@@ -268,7 +278,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer,
278 }
279
280 if rootCert == nil {
271 - rootCert, err = pemDecodeSingleCert(rootCertPEM)
281 + rootCert, err = pemDecodeCertificate(rootCertPEM)
282 if err != nil {
283 return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err)
284 }
@@ -314,7 +324,8 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err e
324 return rootCert, rootKey, nil
325 }
326
317 -func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
327 +func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) {
328 + var interCert *x509.Certificate
329 interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
330 if err != nil {
331 if !errors.Is(err, fs.ErrNotExist) {
@@ -326,10 +337,12 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
337 if err != nil {
338 return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
339 }
340 +
341 + interCertChain = append(interCertChain, interCert)
342 }
343
331 - if interCert == nil {
332 - interCert, err = pemDecodeSingleCert(interCertPEM)
344 + if len(interCertChain) == 0 {
345 + interCertChain, err = pemDecodeCertificateChain(interCertPEM)
346 if err != nil {
347 return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
348 }
@@ -346,7 +359,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
359 }
360 }
361
349 - return interCert, interKey, nil
362 + return interCertChain, interKey, nil
363 }
364
365 func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
modules/caddypki/crypto.go
+60 -5
@@ -17,15 +17,20 @@ package caddypki
17 import (
18 "bytes"
19 "crypto"
20 + "crypto/ecdsa"
21 + "crypto/ed25519"
22 + "crypto/rsa"
23 "crypto/x509"
24 "encoding/pem"
25 + "errors"
26 "fmt"
27 "os"
28
29 "github.com/caddyserver/certmagic"
30 + "go.step.sm/crypto/pemutil"
31 )
32
28 -func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
33 +func pemDecodeCertificate(pemDER []byte) (*x509.Certificate, error) {
34 pemBlock, remaining := pem.Decode(pemDER)
35 if pemBlock == nil {
36 return nil, fmt.Errorf("no PEM block found")
@@ -39,6 +44,15 @@ func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
44 return x509.ParseCertificate(pemBlock.Bytes)
45 }
46
47 +func pemDecodeCertificateChain(pemDER []byte) ([]*x509.Certificate, error) {
48 + chain, err := pemutil.ParseCertificateBundle(pemDER)
49 + if err != nil {
50 + return nil, fmt.Errorf("failed parsing certificate chain: %w", err)
51 + }
52 +
53 + return chain, nil
54 +}
55 +
56 func pemEncodeCert(der []byte) ([]byte, error) {
57 return pemEncode("CERTIFICATE", der)
58 }
@@ -70,15 +84,18 @@ type KeyPair struct {
84 Format string `json:"format,omitempty"`
85 }
86
73 -// Load loads the certificate and key.
74 -func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
87 +// Load loads the certificate chain and (optional) private key from
88 +// the corresponding files, using the configured format. If a
89 +// private key is read, it will be verified to belong to the first
90 +// certificate in the chain.
91 +func (kp KeyPair) Load() ([]*x509.Certificate, crypto.Signer, error) {
92 switch kp.Format {
93 case "", "pem_file":
94 certData, err := os.ReadFile(kp.Certificate)
95 if err != nil {
96 return nil, nil, err
97 }
81 - cert, err := pemDecodeSingleCert(certData)
98 + chain, err := pemDecodeCertificateChain(certData)
99 if err != nil {
100 return nil, nil, err
101 }
@@ -93,11 +110,49 @@ func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
110 if err != nil {
111 return nil, nil, err
112 }
113 + if err := verifyKeysMatch(chain[0], key); err != nil {
114 + return nil, nil, err
115 + }
116 }
117
98 - return cert, key, nil
118 + return chain, key, nil
119
120 default:
121 return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format)
122 }
123 }
124 +
125 +// verifyKeysMatch verifies that the public key in the [x509.Certificate] matches
126 +// the public key of the [crypto.Signer].
127 +func verifyKeysMatch(crt *x509.Certificate, signer crypto.Signer) error {
128 + switch pub := crt.PublicKey.(type) {
129 + case *rsa.PublicKey:
130 + pk, ok := signer.Public().(*rsa.PublicKey)
131 + if !ok {
132 + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
133 + }
134 + if !pub.Equal(pk) {
135 + return errors.New("private key does not match issuer public key")
136 + }
137 + case *ecdsa.PublicKey:
138 + pk, ok := signer.Public().(*ecdsa.PublicKey)
139 + if !ok {
140 + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
141 + }
142 + if !pub.Equal(pk) {
143 + return errors.New("private key does not match issuer public key")
144 + }
145 + case ed25519.PublicKey:
146 + pk, ok := signer.Public().(ed25519.PublicKey)
147 + if !ok {
148 + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
149 + }
150 + if !pub.Equal(pk) {
151 + return errors.New("private key does not match issuer public key")
152 + }
153 + default:
154 + return fmt.Errorf("unsupported key type: %T", pub)
155 + }
156 +
157 + return nil
158 +}
modules/caddypki/crypto_test.go new
+314
@@ -0,0 +1,314 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddypki
16 +
17 +import (
18 + "crypto/rand"
19 + "crypto/x509"
20 + "crypto/x509/pkix"
21 + "encoding/pem"
22 + "os"
23 + "path/filepath"
24 + "testing"
25 + "time"
26 +
27 + "go.step.sm/crypto/keyutil"
28 + "go.step.sm/crypto/pemutil"
29 +)
30 +
31 +func TestKeyPair_Load(t *testing.T) {
32 + rootSigner, err := keyutil.GenerateDefaultSigner()
33 + if err != nil {
34 + t.Fatalf("Failed creating signer: %v", err)
35 + }
36 +
37 + tmpl := &x509.Certificate{
38 + Subject: pkix.Name{CommonName: "test-root"},
39 + IsCA: true,
40 + MaxPathLen: 3,
41 + }
42 + rootBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootSigner.Public(), rootSigner)
43 + if err != nil {
44 + t.Fatalf("Creating root certificate failed: %v", err)
45 + }
46 +
47 + root, err := x509.ParseCertificate(rootBytes)
48 + if err != nil {
49 + t.Fatalf("Parsing root certificate failed: %v", err)
50 + }
51 +
52 + intermediateSigner, err := keyutil.GenerateDefaultSigner()
53 + if err != nil {
54 + t.Fatalf("Creating intermedaite signer failed: %v", err)
55 + }
56 +
57 + intermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
58 + Subject: pkix.Name{CommonName: "test-first-intermediate"},
59 + IsCA: true,
60 + MaxPathLen: 2,
61 + NotAfter: time.Now().Add(time.Hour),
62 + }, root, intermediateSigner.Public(), rootSigner)
63 + if err != nil {
64 + t.Fatalf("Creating intermediate certificate failed: %v", err)
65 + }
66 +
67 + intermediate, err := x509.ParseCertificate(intermediateBytes)
68 + if err != nil {
69 + t.Fatalf("Parsing intermediate certificate failed: %v", err)
70 + }
71 +
72 + var chainContents []byte
73 + chain := []*x509.Certificate{intermediate, root}
74 + for _, cert := range chain {
75 + b, err := pemutil.Serialize(cert)
76 + if err != nil {
77 + t.Fatalf("Failed serializing intermediate certificate: %v", err)
78 + }
79 + chainContents = append(chainContents, pem.EncodeToMemory(b)...)
80 + }
81 +
82 + dir := t.TempDir()
83 + rootCertFile := filepath.Join(dir, "root.pem")
84 + if _, err = pemutil.Serialize(root, pemutil.WithFilename(rootCertFile)); err != nil {
85 + t.Fatalf("Failed serializing root certificate: %v", err)
86 + }
87 + rootKeyFile := filepath.Join(dir, "root.key")
88 + if _, err = pemutil.Serialize(rootSigner, pemutil.WithFilename(rootKeyFile)); err != nil {
89 + t.Fatalf("Failed serializing root key: %v", err)
90 + }
91 + intermediateCertFile := filepath.Join(dir, "intermediate.pem")
92 + if _, err = pemutil.Serialize(intermediate, pemutil.WithFilename(intermediateCertFile)); err != nil {
93 + t.Fatalf("Failed serializing intermediate certificate: %v", err)
94 + }
95 + intermediateKeyFile := filepath.Join(dir, "intermediate.key")
96 + if _, err = pemutil.Serialize(intermediateSigner, pemutil.WithFilename(intermediateKeyFile)); err != nil {
97 + t.Fatalf("Failed serializing intermediate key: %v", err)
98 + }
99 + chainFile := filepath.Join(dir, "chain.pem")
100 + if err := os.WriteFile(chainFile, chainContents, 0644); err != nil {
101 + t.Fatalf("Failed writing intermediate chain: %v", err)
102 + }
103 +
104 + t.Run("ok/single-certificate-without-signer", func(t *testing.T) {
105 + kp := KeyPair{
106 + Certificate: rootCertFile,
107 + }
108 + chain, signer, err := kp.Load()
109 + if err != nil {
110 + t.Fatalf("Failed loading KeyPair: %v", err)
111 + }
112 + if len(chain) != 1 {
113 + t.Errorf("Expected 1 certificate in chain; got %d", len(chain))
114 + }
115 + if signer != nil {
116 + t.Error("Expected no signer to be returned")
117 + }
118 + })
119 +
120 + t.Run("ok/single-certificate-with-signer", func(t *testing.T) {
121 + kp := KeyPair{
122 + Certificate: rootCertFile,
123 + PrivateKey: rootKeyFile,
124 + }
125 + chain, signer, err := kp.Load()
126 + if err != nil {
127 + t.Fatalf("Failed loading KeyPair: %v", err)
128 + }
129 + if len(chain) != 1 {
130 + t.Errorf("Expected 1 certificate in chain; got %d", len(chain))
131 + }
132 + if signer == nil {
133 + t.Error("Expected signer to be returned")
134 + }
135 + })
136 +
137 + t.Run("ok/multiple-certificates-with-signer", func(t *testing.T) {
138 + kp := KeyPair{
139 + Certificate: chainFile,
140 + PrivateKey: intermediateKeyFile,
141 + }
142 + chain, signer, err := kp.Load()
143 + if err != nil {
144 + t.Fatalf("Failed loading KeyPair: %v", err)
145 + }
146 + if len(chain) != 2 {
147 + t.Errorf("Expected 2 certificates in chain; got %d", len(chain))
148 + }
149 + if signer == nil {
150 + t.Error("Expected signer to be returned")
151 + }
152 + })
153 +
154 + t.Run("fail/non-matching-public-key", func(t *testing.T) {
155 + kp := KeyPair{
156 + Certificate: intermediateCertFile,
157 + PrivateKey: rootKeyFile,
158 + }
159 + chain, signer, err := kp.Load()
160 + if err == nil {
161 + t.Error("Expected loading KeyPair to return an error")
162 + }
163 + if chain != nil {
164 + t.Error("Expected no chain to be returned")
165 + }
166 + if signer != nil {
167 + t.Error("Expected no signer to be returned")
168 + }
169 + })
170 +}
171 +
172 +func Test_pemDecodeCertificate(t *testing.T) {
173 + signer, err := keyutil.GenerateDefaultSigner()
174 + if err != nil {
175 + t.Fatalf("Failed creating signer: %v", err)
176 + }
177 +
178 + tmpl := &x509.Certificate{
179 + Subject: pkix.Name{CommonName: "test-cert"},
180 + IsCA: true,
181 + MaxPathLen: 3,
182 + }
183 + derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)
184 + if err != nil {
185 + t.Fatalf("Creating root certificate failed: %v", err)
186 + }
187 + cert, err := x509.ParseCertificate(derBytes)
188 + if err != nil {
189 + t.Fatalf("Parsing root certificate failed: %v", err)
190 + }
191 +
192 + pemBlock, err := pemutil.Serialize(cert)
193 + if err != nil {
194 + t.Fatalf("Failed serializing certificate: %v", err)
195 + }
196 + pemData := pem.EncodeToMemory(pemBlock)
197 +
198 + t.Run("ok", func(t *testing.T) {
199 + cert, err := pemDecodeCertificate(pemData)
200 + if err != nil {
201 + t.Fatalf("Failed decoding PEM data: %v", err)
202 + }
203 + if cert == nil {
204 + t.Errorf("Expected a certificate in PEM data")
205 + }
206 + })
207 +
208 + t.Run("fail/no-pem-data", func(t *testing.T) {
209 + cert, err := pemDecodeCertificate(nil)
210 + if err == nil {
211 + t.Fatalf("Expected pemDecodeCertificate to return an error")
212 + }
213 + if cert != nil {
214 + t.Errorf("Expected pemDecodeCertificate to return nil")
215 + }
216 + })
217 +
218 + t.Run("fail/multiple", func(t *testing.T) {
219 + multiplePEMData := append(pemData, pemData...)
220 + cert, err := pemDecodeCertificate(multiplePEMData)
221 + if err == nil {
222 + t.Fatalf("Expected pemDecodeCertificate to return an error")
223 + }
224 + if cert != nil {
225 + t.Errorf("Expected pemDecodeCertificate to return nil")
226 + }
227 + })
228 +
229 + t.Run("fail/no-pem-certificate", func(t *testing.T) {
230 + pkData := pem.EncodeToMemory(&pem.Block{
231 + Type: "PRIVATE KEY",
232 + Bytes: []byte("some-bogus-private-key"),
233 + })
234 + cert, err := pemDecodeCertificate(pkData)
235 + if err == nil {
236 + t.Fatalf("Expected pemDecodeCertificate to return an error")
237 + }
238 + if cert != nil {
239 + t.Errorf("Expected pemDecodeCertificate to return nil")
240 + }
241 + })
242 +}
243 +
244 +func Test_pemDecodeCertificateChain(t *testing.T) {
245 + signer, err := keyutil.GenerateDefaultSigner()
246 + if err != nil {
247 + t.Fatalf("Failed creating signer: %v", err)
248 + }
249 +
250 + tmpl := &x509.Certificate{
251 + Subject: pkix.Name{CommonName: "test-cert"},
252 + IsCA: true,
253 + MaxPathLen: 3,
254 + }
255 + derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)
256 + if err != nil {
257 + t.Fatalf("Creating root certificate failed: %v", err)
258 + }
259 + cert, err := x509.ParseCertificate(derBytes)
260 + if err != nil {
261 + t.Fatalf("Parsing root certificate failed: %v", err)
262 + }
263 +
264 + pemBlock, err := pemutil.Serialize(cert)
265 + if err != nil {
266 + t.Fatalf("Failed serializing certificate: %v", err)
267 + }
268 + pemData := pem.EncodeToMemory(pemBlock)
269 +
270 + t.Run("ok/single", func(t *testing.T) {
271 + certs, err := pemDecodeCertificateChain(pemData)
272 + if err != nil {
273 + t.Fatalf("Failed decoding PEM data: %v", err)
274 + }
275 + if len(certs) != 1 {
276 + t.Errorf("Expected 1 certificate in PEM data; got %d", len(certs))
277 + }
278 + })
279 +
280 + t.Run("ok/multiple", func(t *testing.T) {
281 + multiplePEMData := append(pemData, pemData...)
282 + certs, err := pemDecodeCertificateChain(multiplePEMData)
283 + if err != nil {
284 + t.Fatalf("Failed decoding PEM data: %v", err)
285 + }
286 + if len(certs) != 2 {
287 + t.Errorf("Expected 2 certificates in PEM data; got %d", len(certs))
288 + }
289 + })
290 +
291 + t.Run("fail/no-pem-certificate", func(t *testing.T) {
292 + pkData := pem.EncodeToMemory(&pem.Block{
293 + Type: "PRIVATE KEY",
294 + Bytes: []byte("some-bogus-private-key"),
295 + })
296 + certs, err := pemDecodeCertificateChain(pkData)
297 + if err == nil {
298 + t.Fatalf("Expected pemDecodeCertificateChain to return an error")
299 + }
300 + if len(certs) != 0 {
301 + t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs))
302 + }
303 + })
304 +
305 + t.Run("fail/no-der-certificate", func(t *testing.T) {
306 + certs, err := pemDecodeCertificateChain([]byte("invalid-der-data"))
307 + if err == nil {
308 + t.Fatalf("Expected pemDecodeCertificateChain to return an error")
309 + }
310 + if len(certs) != 0 {
311 + t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs))
312 + }
313 + })
314 +}
modules/caddypki/maintain.go
+5 -5
@@ -66,16 +66,16 @@ func (p *PKI) renewCertsForCA(ca *CA) error {
66 if needsRenewal(ca.root) {
67 // TODO: implement root renewal (use same key)
68 log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)",
69 - zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
69 + zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
70 )
71 }
72 }
73
74 // only maintain the intermediate if it's not manually provided in the config
75 if ca.Intermediate == nil {
76 - if needsRenewal(ca.inter) {
76 + if needsRenewal(ca.interChain[0]) {
77 log.Info("intermediate expires soon; renewing",
78 - zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
78 + zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
79 )
80
81 rootCert, rootKey, err := ca.loadOrGenRoot()
@@ -86,10 +86,10 @@ func (p *PKI) renewCertsForCA(ca *CA) error {
86 if err != nil {
87 return fmt.Errorf("generating new certificate: %v", err)
88 }
89 - ca.inter, ca.interKey = interCert, interKey
89 + ca.interChain, ca.interKey = []*x509.Certificate{interCert}, interKey
90
91 log.Info("renewed intermediate",
92 - zap.Time("new_expiration", ca.inter.NotAfter),
92 + zap.Time("new_expiration", ca.interChain[0].NotAfter),
93 )
94 }
95 }
modules/caddytls/capools.go
+4 -2
@@ -257,7 +257,7 @@ func (PKIIntermediateCAPool) CaddyModule() caddy.ModuleInfo {
257 }
258 }
259
260 -// Loads the PKI app and load the intermediate certificates into the certificate pool
260 +// Loads the PKI app and loads the intermediate certificates into the certificate pool
261 func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
262 pkiApp, err := ctx.AppIfConfigured("pki")
263 if err != nil {
@@ -274,7 +274,9 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
274
275 caPool := x509.NewCertPool()
276 for _, ca := range p.ca {
277 - caPool.AddCert(ca.IntermediateCertificate())
277 + for _, c := range ca.IntermediateCertificateChain() {
278 + caPool.AddCert(c)
279 + }
280 }
281 p.pool = caPool
282 return nil
modules/caddytls/internalissuer.go
+2 -1
@@ -115,7 +115,8 @@ func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateReques
115 if iss.SignWithRoot {
116 issuerCert = iss.ca.RootCertificate()
117 } else {
118 - issuerCert = iss.ca.IntermediateCertificate()
118 + chain := iss.ca.IntermediateCertificateChain()
119 + issuerCert = chain[0]
120 }
121
122 // ensure issued certificate does not expire later than its issuer
modules/caddytls/internalissuer_test.go new
+262
@@ -0,0 +1,262 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddytls
16 +
17 +import (
18 + "crypto/rand"
19 + "crypto/x509"
20 + "crypto/x509/pkix"
21 + "encoding/pem"
22 + "fmt"
23 + "os"
24 + "path/filepath"
25 + "testing"
26 + "time"
27 +
28 + "github.com/caddyserver/caddy/v2"
29 + "github.com/caddyserver/caddy/v2/modules/caddypki"
30 + "go.uber.org/zap"
31 +
32 + "go.step.sm/crypto/keyutil"
33 + "go.step.sm/crypto/pemutil"
34 +)
35 +
36 +func TestInternalIssuer_Issue(t *testing.T) {
37 + rootSigner, err := keyutil.GenerateDefaultSigner()
38 + if err != nil {
39 + t.Fatalf("Creating root signer failed: %v", err)
40 + }
41 +
42 + tmpl := &x509.Certificate{
43 + Subject: pkix.Name{CommonName: "test-root"},
44 + IsCA: true,
45 + MaxPathLen: 3,
46 + NotAfter: time.Now().Add(7 * 24 * time.Hour),
47 + NotBefore: time.Now().Add(-7 * 24 * time.Hour),
48 + }
49 + rootBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootSigner.Public(), rootSigner)
50 + if err != nil {
51 + t.Fatalf("Creating root certificate failed: %v", err)
52 + }
53 +
54 + root, err := x509.ParseCertificate(rootBytes)
55 + if err != nil {
56 + t.Fatalf("Parsing root certificate failed: %v", err)
57 + }
58 +
59 + firstIntermediateSigner, err := keyutil.GenerateDefaultSigner()
60 + if err != nil {
61 + t.Fatalf("Creating intermedaite signer failed: %v", err)
62 + }
63 +
64 + firstIntermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
65 + Subject: pkix.Name{CommonName: "test-first-intermediate"},
66 + IsCA: true,
67 + MaxPathLen: 2,
68 + NotAfter: time.Now().Add(24 * time.Hour),
69 + NotBefore: time.Now().Add(-24 * time.Hour),
70 + }, root, firstIntermediateSigner.Public(), rootSigner)
71 + if err != nil {
72 + t.Fatalf("Creating intermediate certificate failed: %v", err)
73 + }
74 +
75 + firstIntermediate, err := x509.ParseCertificate(firstIntermediateBytes)
76 + if err != nil {
77 + t.Fatalf("Parsing intermediate certificate failed: %v", err)
78 + }
79 +
80 + secondIntermediateSigner, err := keyutil.GenerateDefaultSigner()
81 + if err != nil {
82 + t.Fatalf("Creating second intermedaite signer failed: %v", err)
83 + }
84 +
85 + secondIntermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
86 + Subject: pkix.Name{CommonName: "test-second-intermediate"},
87 + IsCA: true,
88 + MaxPathLen: 2,
89 + NotAfter: time.Now().Add(24 * time.Hour),
90 + NotBefore: time.Now().Add(-24 * time.Hour),
91 + }, firstIntermediate, secondIntermediateSigner.Public(), firstIntermediateSigner)
92 + if err != nil {
93 + t.Fatalf("Creating second intermediate certificate failed: %v", err)
94 + }
95 +
96 + secondIntermediate, err := x509.ParseCertificate(secondIntermediateBytes)
97 + if err != nil {
98 + t.Fatalf("Parsing second intermediate certificate failed: %v", err)
99 + }
100 +
101 + dir := t.TempDir()
102 + storageDir := filepath.Join(dir, "certmagic")
103 + rootCertFile := filepath.Join(dir, "root.pem")
104 + if _, err = pemutil.Serialize(root, pemutil.WithFilename(rootCertFile)); err != nil {
105 + t.Fatalf("Failed serializing root certificate: %v", err)
106 + }
107 + intermediateCertFile := filepath.Join(dir, "intermediate.pem")
108 + if _, err = pemutil.Serialize(firstIntermediate, pemutil.WithFilename(intermediateCertFile)); err != nil {
109 + t.Fatalf("Failed serializing intermediate certificate: %v", err)
110 + }
111 + intermediateKeyFile := filepath.Join(dir, "intermediate.key")
112 + if _, err = pemutil.Serialize(firstIntermediateSigner, pemutil.WithFilename(intermediateKeyFile)); err != nil {
113 + t.Fatalf("Failed serializing intermediate key: %v", err)
114 + }
115 +
116 + var intermediateChainContents []byte
117 + intermediateChain := []*x509.Certificate{secondIntermediate, firstIntermediate}
118 + for _, cert := range intermediateChain {
119 + b, err := pemutil.Serialize(cert)
120 + if err != nil {
121 + t.Fatalf("Failed serializing intermediate certificate: %v", err)
122 + }
123 + intermediateChainContents = append(intermediateChainContents, pem.EncodeToMemory(b)...)
124 + }
125 + intermediateChainFile := filepath.Join(dir, "intermediates.pem")
126 + if err := os.WriteFile(intermediateChainFile, intermediateChainContents, 0644); err != nil {
127 + t.Fatalf("Failed writing intermediate chain: %v", err)
128 + }
129 + intermediateChainKeyFile := filepath.Join(dir, "intermediates.key")
130 + if _, err = pemutil.Serialize(secondIntermediateSigner, pemutil.WithFilename(intermediateChainKeyFile)); err != nil {
131 + t.Fatalf("Failed serializing intermediate key: %v", err)
132 + }
133 +
134 + signer, err := keyutil.GenerateDefaultSigner()
135 + if err != nil {
136 + t.Fatalf("Failed creating signer: %v", err)
137 + }
138 +
139 + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
140 + Subject: pkix.Name{CommonName: "test"},
141 + }, signer)
142 + if err != nil {
143 + t.Fatalf("Failed creating CSR: %v", err)
144 + }
145 +
146 + csr, err := x509.ParseCertificateRequest(csrBytes)
147 + if err != nil {
148 + t.Fatalf("Failed parsing CSR: %v", err)
149 + }
150 +
151 + t.Run("generated-with-defaults", func(t *testing.T) {
152 + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
153 + t.Cleanup(cancel)
154 + logger := zap.NewNop()
155 +
156 + ca := &caddypki.CA{
157 + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)),
158 + }
159 + if err := ca.Provision(caddyCtx, "local-test-generated", logger); err != nil {
160 + t.Fatalf("Failed provisioning CA: %v", err)
161 + }
162 +
163 + iss := InternalIssuer{
164 + SignWithRoot: false,
165 + ca: ca,
166 + logger: logger,
167 + }
168 +
169 + c, err := iss.Issue(t.Context(), csr)
170 + if err != nil {
171 + t.Fatalf("Failed issuing certificate: %v", err)
172 + }
173 +
174 + chain, err := pemutil.ParseCertificateBundle(c.Certificate)
175 + if err != nil {
176 + t.Errorf("Failed issuing certificate: %v", err)
177 + }
178 + if len(chain) != 2 {
179 + t.Errorf("Expected 2 certificates in chain; got %d", len(chain))
180 + }
181 + })
182 +
183 + t.Run("single-intermediate-from-disk", func(t *testing.T) {
184 + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
185 + t.Cleanup(cancel)
186 + logger := zap.NewNop()
187 +
188 + ca := &caddypki.CA{
189 + Root: &caddypki.KeyPair{
190 + Certificate: rootCertFile,
191 + },
192 + Intermediate: &caddypki.KeyPair{
193 + Certificate: intermediateCertFile,
194 + PrivateKey: intermediateKeyFile,
195 + },
196 + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)),
197 + }
198 +
199 + if err := ca.Provision(caddyCtx, "local-test-single-intermediate", logger); err != nil {
200 + t.Fatalf("Failed provisioning CA: %v", err)
201 + }
202 +
203 + iss := InternalIssuer{
204 + ca: ca,
205 + SignWithRoot: false,
206 + logger: logger,
207 + }
208 +
209 + c, err := iss.Issue(t.Context(), csr)
210 + if err != nil {
211 + t.Fatalf("Failed issuing certificate: %v", err)
212 + }
213 +
214 + chain, err := pemutil.ParseCertificateBundle(c.Certificate)
215 + if err != nil {
216 + t.Errorf("Failed issuing certificate: %v", err)
217 + }
218 + if len(chain) != 2 {
219 + t.Errorf("Expected 2 certificates in chain; got %d", len(chain))
220 + }
221 + })
222 +
223 + t.Run("multiple-intermediates-from-disk", func(t *testing.T) {
224 + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
225 + t.Cleanup(cancel)
226 + logger := zap.NewNop()
227 +
228 + ca := &caddypki.CA{
229 + Root: &caddypki.KeyPair{
230 + Certificate: rootCertFile,
231 + },
232 + Intermediate: &caddypki.KeyPair{
233 + Certificate: intermediateChainFile,
234 + PrivateKey: intermediateChainKeyFile,
235 + },
236 + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)),
237 + }
238 +
239 + if err := ca.Provision(caddyCtx, "local-test", zap.NewNop()); err != nil {
240 + t.Fatalf("Failed provisioning CA: %v", err)
241 + }
242 +
243 + iss := InternalIssuer{
244 + ca: ca,
245 + SignWithRoot: false,
246 + logger: logger,
247 + }
248 +
249 + c, err := iss.Issue(t.Context(), csr)
250 + if err != nil {
251 + t.Fatalf("Failed issuing certificate: %v", err)
252 + }
253 +
254 + chain, err := pemutil.ParseCertificateBundle(c.Certificate)
255 + if err != nil {
256 + t.Errorf("Failed issuing certificate: %v", err)
257 + }
258 + if len(chain) != 3 {
259 + t.Errorf("Expected 3 certificates in chain; got %d", len(chain))
260 + }
261 + })
262 +}