Skip to content

Commit a5870fc

Browse files
authored
Merge pull request #5 from quix-labs/dev
Automatically fetch missing certificates to complete the certificate chain.
2 parents da03cf0 + 54ad3ba commit a5870fc

File tree

4 files changed

+224
-19
lines changed

4 files changed

+224
-19
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ https://your-domain {
7070
get_certificate pfx {
7171
path test.pfx
7272
password password
73+
74+
# If set to false, only the certificates from the .pfx file will be sent.
75+
# If set to true (default), all the intermediate certificates will be downloaded, including those up to the root CA.
76+
fetch_full_chain true
7377
}
7478
7579
# Or shortcut -> get_certificate pfx test.pfx password

certificates.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package CADDY_PFX_CERTIFICATES
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
)
11+
12+
func getCertificateChain(initialCerts []*x509.Certificate) ([]*x509.Certificate, error) {
13+
certsState := &certState{}
14+
var fullChain []*x509.Certificate
15+
16+
// Add initial certificates to the state
17+
for _, cert := range initialCerts {
18+
addOrUpdateState(certsState, cert)
19+
fullChain = append(fullChain, cert)
20+
}
21+
22+
// Resolve the full chain, including downloading missing certificates
23+
resolvedCerts, err := getUnresolvedCertificates(certsState)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
// Append resolved certificates to the PEM data
29+
for _, cert := range resolvedCerts {
30+
fullChain = append(fullChain, cert)
31+
}
32+
33+
return fullChain, nil
34+
}
35+
36+
type CertificateState struct {
37+
SubjectKeyId string
38+
AuthorityKeyId string
39+
Resolved bool
40+
IssuingCertificateURLs []string
41+
}
42+
43+
type certState []CertificateState
44+
45+
func addOrUpdateState(certsState *certState, cert *x509.Certificate) {
46+
for i := range *certsState {
47+
if (*certsState)[i].SubjectKeyId == string(cert.SubjectKeyId) {
48+
return
49+
}
50+
}
51+
52+
*certsState = append(*certsState, CertificateState{
53+
SubjectKeyId: string(cert.SubjectKeyId),
54+
AuthorityKeyId: string(cert.AuthorityKeyId),
55+
IssuingCertificateURLs: cert.IssuingCertificateURL,
56+
})
57+
58+
var unresolvedCerts []CertificateState
59+
for _, state := range *certsState {
60+
if state.AuthorityKeyId != string(cert.SubjectKeyId) {
61+
unresolvedCerts = append(unresolvedCerts, state)
62+
}
63+
}
64+
*certsState = unresolvedCerts
65+
}
66+
67+
func getUnresolvedCertificates(certsState *certState) ([]*x509.Certificate, error) {
68+
var resolvedCerts []*x509.Certificate
69+
if len(*certsState) == 0 {
70+
return resolvedCerts, nil
71+
}
72+
73+
state := (*certsState)[0]
74+
*certsState = (*certsState)[1:] // Shift
75+
76+
for _, url := range state.IssuingCertificateURLs {
77+
cert, err := fetchCertificateFromURL(url)
78+
if err != nil {
79+
continue // Skip on error but continue processing
80+
}
81+
resolvedCerts = append(resolvedCerts, cert)
82+
addOrUpdateState(certsState, cert) // Recursively append remaining certificates
83+
}
84+
85+
// Recur for the remaining unresolved certificates
86+
moreResolved, err := getUnresolvedCertificates(certsState)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
// Combine resolved certificates
92+
resolvedCerts = append(resolvedCerts, moreResolved...)
93+
return resolvedCerts, nil
94+
}
95+
96+
func fetchCertificateFromURL(url string) (cert *x509.Certificate, err error) {
97+
resp, err := http.Get(url)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to fetch certificate from URL %s: %v", url, err)
100+
}
101+
defer func() {
102+
if closeErr := resp.Body.Close(); closeErr != nil {
103+
err = errors.Join(err, closeErr)
104+
}
105+
}()
106+
107+
if resp.StatusCode != http.StatusOK {
108+
return nil, fmt.Errorf("received non-OK HTTP status: %s", resp.Status)
109+
}
110+
111+
certData, err := io.ReadAll(resp.Body)
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to read certificate data: %v", err)
114+
}
115+
116+
// Try to decode in PEM
117+
block, _ := pem.Decode(certData)
118+
if block != nil && block.Type == "CERTIFICATE" {
119+
cert, err := x509.ParseCertificate(block.Bytes)
120+
if err != nil {
121+
return nil, fmt.Errorf("failed to parse PEM certificate: %v", err)
122+
}
123+
return cert, nil
124+
}
125+
126+
// Try to decode in DER
127+
cert, err = x509.ParseCertificate(certData)
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to parse DER certificate: %v", err)
130+
}
131+
132+
return cert, nil
133+
}

module.go

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ func init() {
2323

2424
// PfxCertGetter allow user to set path to .pfx file to load TLS certificate
2525
type PfxCertGetter struct {
26-
// The path to file with domain-certificate dictionary. Required.
26+
// Path to your .pfx file.
2727
Path string `json:"path,omitempty"`
28-
// The password used to decode pfx file. Required.
28+
// Password used to decode pfx file. Required.
2929
Password string `json:"password,omitempty"`
30+
// FetchFullChain allows Caddy server to automatically download the certificate chain.
31+
FetchFullChain *bool `json:"fetch_full_chain,omitempty"`
3032

3133
CacheCertName string
3234

@@ -49,14 +51,23 @@ func (getter *PfxCertGetter) Provision(ctx caddy.Context) error {
4951
return fmt.Errorf("path is required")
5052
}
5153

54+
if getter.FetchFullChain == nil {
55+
tmp := true
56+
getter.FetchFullChain = &tmp
57+
}
58+
5259
// Get the modification time of the file
5360
fileInfo, err := os.Stat(getter.Path)
5461
if err != nil {
5562
return err
5663
}
5764
modTime := fileInfo.ModTime()
5865

59-
getter.CacheCertName = getter.Path + "." + modTime.Format(time.RFC3339) + "-chain+pkey.pem"
66+
if *getter.FetchFullChain {
67+
getter.CacheCertName = getter.Path + "." + modTime.Format(time.RFC3339) + "-fullchain+pkey.pem"
68+
} else {
69+
getter.CacheCertName = getter.Path + "." + modTime.Format(time.RFC3339) + "-chain+pkey.pem"
70+
}
6071

6172
return nil
6273
}
@@ -86,23 +97,14 @@ func (getter *PfxCertGetter) GetCertificate(ctx context.Context, hello *tls.Clie
8697
}
8798

8899
switch block.Type {
89-
case "CERTIFICATE":
90-
cert.Certificate = append(cert.Certificate, block.Bytes)
91-
92-
// If leaf already defined, skip
93-
if cert.Leaf != nil {
94-
break
95-
}
96-
97-
// Mark first certificate as leaf
98-
if cert.Leaf, err = x509.ParseCertificate(block.Bytes); err != nil {
99-
return nil, err
100-
}
101-
102100
case "RSA PRIVATE KEY":
103101
if cert.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
104102
return nil, err
105103
}
104+
break
105+
case "CERTIFICATE":
106+
cert.Certificate = append(cert.Certificate, block.Bytes)
107+
break
106108
}
107109

108110
pemData = rest
@@ -126,11 +128,35 @@ func (getter *PfxCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
126128
return d.ArgErr()
127129
}
128130
getter.Path = d.Val()
129-
} else if key == "password" {
131+
continue
132+
}
133+
if key == "password" {
130134
if !d.NextArg() {
131135
return d.ArgErr()
132136
}
133137
getter.Password = d.Val()
138+
continue
139+
}
140+
if key == "fetch_full_chain" {
141+
if !d.NextArg() {
142+
return d.ArgErr()
143+
}
144+
145+
if d.Val() == "true" {
146+
tmp := true
147+
getter.FetchFullChain = &tmp
148+
} else if d.Val() == "false" {
149+
tmp := false
150+
getter.FetchFullChain = &tmp
151+
} else {
152+
return d.Err(d.Val() + " is not a valid value for fetch_full_chain")
153+
}
154+
155+
// Ensure no more arguments
156+
if d.NextArg() {
157+
return d.ArgErr()
158+
}
159+
continue
134160
} else {
135161
return d.Err(key + " not allowed here")
136162
}
@@ -178,8 +204,14 @@ func (getter *PfxCertGetter) GenerateParsedKeys(ctx context.Context) error {
178204
Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)),
179205
})...)
180206

181-
// Append leaf + intermediates certificate
182-
for _, caCert := range append([]*x509.Certificate{certificate}, caCerts...) {
207+
// Combine leaf and intermediates from PFX and fetch the full chain automatically
208+
chain, err := getCertificateChain(append([]*x509.Certificate{certificate}, caCerts...))
209+
if err != nil {
210+
return err
211+
}
212+
213+
// Append all certificates
214+
for _, caCert := range chain {
183215
pemData = append(pemData, pem.EncodeToMemory(&pem.Block{
184216
Type: "CERTIFICATE",
185217
Bytes: caCert.Raw,

scripts/transform_certs.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
3+
IN=./path/to/your.pfx
4+
PASS=pfx_password
5+
OUT_PATH=./your_output
6+
7+
mkdir -p $OUT_PATH
8+
9+
function openssl-pass {
10+
openssl pkcs12 -in "$IN" -password "pass:$PASS" "$@"
11+
}
12+
13+
# Extract the RSA Key from the PFX file:
14+
openssl-pass -nocerts -nodes -out "$OUT_PATH/rsa-key-pfx.pem"
15+
16+
# Extract the Public Certificate from the PFX file:
17+
openssl-pass -clcerts -nokeys -out "$OUT_PATH/public-cert-pfx.pem"
18+
19+
# Extract the CA Chain from the PFX file:
20+
openssl-pass -cacerts -nokeys -chain -out "$OUT_PATH/ca-pfx.pem"
21+
22+
# Convert the RSA Key from PFX format to PEM:
23+
openssl rsa -in $OUT_PATH/rsa-key-pfx.pem -out "$OUT_PATH/rsa-key.pem"
24+
25+
# Convert the x509 Public Certificate and CA Chain from PFX to PEM format:
26+
openssl x509 -in "$OUT_PATH/public-cert-pfx.pem" -out "$OUT_PATH/cert.pem"
27+
openssl x509 -in "$OUT_PATH/ca-pfx.pem" -out "$OUT_PATH/ca.pem"
28+
29+
# Combine certs to generate chain
30+
cat "$OUT_PATH/cert.pem" "$OUT_PATH/ca.pem" > "$OUT_PATH/chain.pem"
31+
32+
# Remove unused/unnecessary files
33+
rm "$OUT_PATH/ca-pfx.pem" "$OUT_PATH/public-cert-pfx.pem" "$OUT_PATH/rsa-key-pfx.pem"
34+
35+
# Run openssl to verify certificate
36+
openssl verify -CAfile "$OUT_PATH/chain.pem" "$OUT_PATH/ca.pem"

0 commit comments

Comments
 (0)