Skip to content

Commit 924645f

Browse files
authored
Skip initial key in PEM bundle (joe-elliott#227)
1 parent 013313f commit 924645f

File tree

2 files changed

+196
-1
lines changed

2 files changed

+196
-1
lines changed

src/exporters/certHelpers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ func parseAsPEM(certBytes []byte) (bool, []certMetric, error) {
8888
}
8989
// Remove trailing whitespaces to prevent possible error in loop
9090
rest = []byte(strings.TrimRightFunc(string(rest), unicode.IsSpace))
91-
blocks = append(blocks, block)
91+
if block.Type == "CERTIFICATE" {
92+
blocks = append(blocks, block)
93+
}
9294
// Export the remaining certificates in the certificate chain
9395
for len(rest) != 0 {
9496
block, rest = pem.Decode(rest)

src/exporters/certHelpers_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package exporters
2+
3+
import (
4+
"encoding/pem"
5+
"testing"
6+
7+
"github.com/joe-elliott/cert-exporter/internal/testutil"
8+
)
9+
10+
func TestParseAsPEM(t *testing.T) {
11+
const certCN = "cert"
12+
cert := testutil.GenerateCertificate(t, testutil.CertConfig{
13+
CommonName: certCN,
14+
Days: 90,
15+
IsCA: false,
16+
})
17+
const rootCN = "root-cert"
18+
root := testutil.GenerateCertificate(t, testutil.CertConfig{
19+
CommonName: rootCN,
20+
Days: 365,
21+
IsCA: true,
22+
})
23+
const intermediateCN = "intermediate-cert"
24+
intermediate := testutil.GenerateSignedCertificate(t, testutil.CertConfig{
25+
CommonName: intermediateCN,
26+
Days: 180,
27+
IsCA: false,
28+
}, root)
29+
30+
tests := []struct {
31+
name string
32+
setupFunc func(t *testing.T) []byte
33+
wantParsed bool
34+
wantMetrics int
35+
wantErr bool
36+
validateFunc func(t *testing.T, metrics []certMetric)
37+
}{
38+
{
39+
name: "valid single certificate",
40+
setupFunc: func(t *testing.T) []byte {
41+
return cert.CertPEM
42+
},
43+
wantParsed: true,
44+
wantMetrics: 1,
45+
wantErr: false,
46+
validateFunc: func(t *testing.T, metrics []certMetric) {
47+
if metrics[0].cn != certCN {
48+
t.Errorf("Expected CN '%s', got '%s'", certCN, metrics[0].cn)
49+
}
50+
if metrics[0].durationUntilExpiry <= 0 {
51+
t.Errorf("Expected positive duration until expiry, got %f", metrics[0].durationUntilExpiry)
52+
}
53+
},
54+
},
55+
{
56+
name: "valid certificate chain",
57+
setupFunc: func(t *testing.T) []byte {
58+
return testutil.CreateCertBundle(intermediate, root)
59+
},
60+
wantParsed: true,
61+
wantMetrics: 2,
62+
wantErr: false,
63+
validateFunc: func(t *testing.T, metrics []certMetric) {
64+
if metrics[0].cn != intermediateCN {
65+
t.Errorf("Expected first cert CN '%s', got '%s'", intermediateCN, metrics[0].cn)
66+
}
67+
if metrics[1].cn != rootCN {
68+
t.Errorf("Expected second cert CN '%s', got '%s'", rootCN, metrics[1].cn)
69+
}
70+
},
71+
},
72+
{
73+
name: "certificate and chain with whitespaces",
74+
setupFunc: func(t *testing.T) []byte {
75+
// Combine and add whitespaces.
76+
combined := append(cert.CertPEM, []byte(" \n\t \n")...)
77+
combined = append(combined, intermediate.CertPEM...)
78+
combined = append(combined, []byte("\n\n")...)
79+
combined = append(combined, root.CertPEM...)
80+
combined = append(combined, []byte("\n\t\t\n")...)
81+
return combined
82+
},
83+
wantParsed: true,
84+
wantMetrics: 3,
85+
wantErr: false,
86+
},
87+
{
88+
name: "certificate with private key and chain (should only parse certificates)",
89+
setupFunc: func(t *testing.T) []byte {
90+
// Combine cert and key.
91+
combined := append(cert.CertPEM, cert.PrivateKeyPEM...)
92+
// And the chain.
93+
combined = append(combined, intermediate.CertPEM...)
94+
combined = append(combined, root.CertPEM...)
95+
return combined
96+
},
97+
wantParsed: true,
98+
wantMetrics: 3, // Should only parse the certificate, not the key.
99+
wantErr: false,
100+
},
101+
{
102+
name: "certificate with private key (on first place) and chain (should only parse certificates)",
103+
setupFunc: func(t *testing.T) []byte {
104+
// Combine cert and key.
105+
combined := append(cert.PrivateKeyPEM, cert.CertPEM...)
106+
// And the chain.
107+
combined = append(combined, intermediate.CertPEM...)
108+
combined = append(combined, root.CertPEM...)
109+
return combined
110+
},
111+
wantParsed: true,
112+
wantMetrics: 3, // Should only parse the certificate, not the key.
113+
wantErr: false,
114+
},
115+
{
116+
name: "invalid PEM data",
117+
setupFunc: func(t *testing.T) []byte {
118+
return []byte("this is not a valid PEM certificate")
119+
},
120+
wantParsed: false,
121+
wantMetrics: 0,
122+
wantErr: true,
123+
},
124+
{
125+
name: "empty input",
126+
setupFunc: func(t *testing.T) []byte {
127+
return []byte("")
128+
},
129+
wantParsed: false,
130+
wantMetrics: 0,
131+
wantErr: true,
132+
},
133+
{
134+
name: "only private key (no certificate)",
135+
setupFunc: func(t *testing.T) []byte {
136+
return cert.PrivateKeyPEM
137+
},
138+
wantParsed: true,
139+
wantMetrics: 0, // No certificates, only key.
140+
wantErr: false,
141+
},
142+
{
143+
name: "corrupted certificate data",
144+
setupFunc: func(t *testing.T) []byte {
145+
// Create a PEM block with invalid certificate data.
146+
block := &pem.Block{
147+
Type: "CERTIFICATE",
148+
Bytes: []byte("invalid certificate data"),
149+
}
150+
return pem.EncodeToMemory(block)
151+
},
152+
wantParsed: true,
153+
wantMetrics: 0,
154+
wantErr: true,
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
certBytes := tt.setupFunc(t)
161+
parsed, metrics, err := parseAsPEM(certBytes)
162+
163+
if parsed != tt.wantParsed {
164+
t.Errorf("parseAsPEM() parsed = %v, want %v", parsed, tt.wantParsed)
165+
}
166+
167+
if (err != nil) != tt.wantErr {
168+
t.Errorf("parseAsPEM() error = %v, wantErr %v", err, tt.wantErr)
169+
}
170+
171+
if len(metrics) != tt.wantMetrics {
172+
t.Errorf("parseAsPEM() returned %d metrics, want %d", len(metrics), tt.wantMetrics)
173+
}
174+
175+
// Validate metrics if validation function is provided.
176+
if tt.validateFunc != nil && len(metrics) > 0 {
177+
tt.validateFunc(t, metrics)
178+
}
179+
180+
// Validate common metric properties if we have metrics.
181+
if len(metrics) > 0 && !tt.wantErr {
182+
for i, metric := range metrics {
183+
if metric.notBefore == 0 {
184+
t.Errorf("metric[%d] notBefore should not be zero", i)
185+
}
186+
if metric.notAfter == 0 {
187+
t.Errorf("metric[%d] notAfter should not be zero", i)
188+
}
189+
}
190+
}
191+
})
192+
}
193+
}

0 commit comments

Comments
 (0)