Skip to content

Commit a239db2

Browse files
Lappihuanhdurand0710
authored andcommitted
BUG/MINOR: normalize certificates to fix renewal with multiple newlines
1 parent 5e9e36f commit a239db2

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

pkg/haproxy/certs/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,33 @@ func certContent(key, crt []byte) []byte {
418418
buff = append(buff, byte('\n'))
419419
}
420420
buff = append(buff, crt...)
421+
buff = normalizePEM(buff)
421422
return buff
422423
}
423424

425+
func normalizePEM(data []byte) []byte {
426+
if len(data) == 0 {
427+
return data
428+
}
429+
430+
result := make([]byte, 0, len(data))
431+
lastWasNewline := false
432+
433+
for _, b := range data {
434+
if b == '\n' {
435+
if !lastWasNewline {
436+
result = append(result, b)
437+
lastWasNewline = true
438+
}
439+
} else {
440+
result = append(result, b)
441+
lastWasNewline = false
442+
}
443+
}
444+
445+
return result
446+
}
447+
424448
func (c *certs) SetAPI(api api.HAProxyClient) {
425449
c.client = api
426450
}

pkg/haproxy/certs/main_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package certs
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func TestNormalizePEM(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
input []byte
12+
expected []byte
13+
}{
14+
{
15+
name: "single newline between certs",
16+
input: []byte("cert1\n-----BEGIN CERTIFICATE-----\ncert2"),
17+
expected: []byte("cert1\n-----BEGIN CERTIFICATE-----\ncert2"),
18+
},
19+
{
20+
name: "multiple newlines between certs",
21+
input: []byte("cert1\n\n\n-----BEGIN CERTIFICATE-----\ncert2"),
22+
expected: []byte("cert1\n-----BEGIN CERTIFICATE-----\ncert2"),
23+
},
24+
{
25+
name: "two newlines between certs",
26+
input: []byte("-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----\nintermediate"),
27+
expected: []byte("-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nintermediate"),
28+
},
29+
{
30+
name: "empty input",
31+
input: []byte(""),
32+
expected: []byte(""),
33+
},
34+
{
35+
name: "no newlines",
36+
input: []byte("single line"),
37+
expected: []byte("single line"),
38+
},
39+
{
40+
name: "trailing newlines",
41+
input: []byte("content\n\n\n"),
42+
expected: []byte("content\n"),
43+
},
44+
{
45+
name: "leading newlines",
46+
input: []byte("\n\n\ncontent"),
47+
expected: []byte("\ncontent"),
48+
},
49+
{
50+
name: "multiple newlines throughout",
51+
input: []byte("line1\n\nline2\n\n\nline3\n\n"),
52+
expected: []byte("line1\nline2\nline3\n"),
53+
},
54+
{
55+
name: "realistic certificate chain with multiple newlines",
56+
input: []byte(`-----END CERTIFICATE-----
57+
58+
-----BEGIN CERTIFICATE-----
59+
MIIBoDCCAUWgAwIBAgIUIzgFRNsANKPpAq4aaEv4xggFvsQwCgYIKoZIzj0EAwIw
60+
-----END CERTIFICATE-----`),
61+
expected: []byte(`-----END CERTIFICATE-----
62+
-----BEGIN CERTIFICATE-----
63+
MIIBoDCCAUWgAwIBAgIUIzgFRNsANKPpAq4aaEv4xggFvsQwCgYIKoZIzj0EAwIw
64+
-----END CERTIFICATE-----`),
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
result := normalizePEM(tt.input)
71+
if !bytes.Equal(result, tt.expected) {
72+
t.Errorf("normalizePEM() = %q, want %q", result, tt.expected)
73+
}
74+
})
75+
}
76+
}
77+
78+
func TestCertContent(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
key []byte
82+
cert []byte
83+
expectedSubstring string
84+
}{
85+
{
86+
name: "key and cert without trailing newline",
87+
key: []byte("-----BEGIN PRIVATE KEY-----\nkey_content"),
88+
cert: []byte("-----BEGIN CERTIFICATE-----\ncert_content"),
89+
expectedSubstring: "-----BEGIN PRIVATE KEY-----\nkey_content\n-----BEGIN CERTIFICATE-----\ncert_content",
90+
},
91+
{
92+
name: "key with trailing newline",
93+
key: []byte("-----BEGIN PRIVATE KEY-----\nkey_content\n"),
94+
cert: []byte("-----BEGIN CERTIFICATE-----\ncert_content"),
95+
expectedSubstring: "-----BEGIN PRIVATE KEY-----\nkey_content\n-----BEGIN CERTIFICATE-----\ncert_content",
96+
},
97+
{
98+
name: "certificate chain with multiple newlines",
99+
key: []byte("key_data\n"),
100+
cert: []byte("-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----\nintermediate"),
101+
expectedSubstring: "key_data\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nintermediate",
102+
},
103+
{
104+
name: "empty key",
105+
key: []byte(""),
106+
cert: []byte("cert_data"),
107+
expectedSubstring: "cert_data",
108+
},
109+
{
110+
name: "cert with multiple newlines between leaf and intermediate",
111+
key: []byte("PRIVATE_KEY_CONTENT\n"),
112+
cert: []byte("-----BEGIN CERTIFICATE-----\nMIIChzCCAi2gAwIBAgIUXIC63krrMatmZZVL8+sy1cedepYwCgYIKoZIzj0EAwIw\n" +
113+
"-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----\n" +
114+
"MIIBoDCCAUWgAwIBAgIUIzgFRNsANKPpAq4aaEv4xggFvsQwCgYIKoZIzj0EAwIw\n" +
115+
"-----END CERTIFICATE-----"),
116+
expectedSubstring: "PRIVATE_KEY_CONTENT\n-----BEGIN CERTIFICATE-----\nMIIChzCCAi2gAwIBAgIUXIC63krrMatmZZVL8+sy1cedepYwCgYIKoZIzj0EAwIw\n" +
117+
"-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n" +
118+
"MIIBoDCCAUWgAwIBAgIUIzgFRNsANKPpAq4aaEv4xggFvsQwCgYIKoZIzj0EAwIw\n" +
119+
"-----END CERTIFICATE-----",
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
result := certContent(tt.key, tt.cert)
126+
resultStr := string(result)
127+
if !bytes.Contains(result, []byte(tt.expectedSubstring)) {
128+
t.Errorf("certContent() result does not contain expected substring.\nGot: %q\nExpected to contain: %q", resultStr, tt.expectedSubstring)
129+
}
130+
131+
if bytes.Contains(result, []byte("\n\n")) {
132+
t.Errorf("certContent() result contains multiple consecutive newlines: %q", resultStr)
133+
}
134+
})
135+
}
136+
}

0 commit comments

Comments
 (0)