Skip to content

Commit 87dfebe

Browse files
authored
Loosen certificate checks (#1413)
The checks we had were a bit too constrained, and don't follow the golang standard library code. So, replace our replacement functions with the originals. This level of checking (e.g. mistyping a path) is not really necessary when people are using our (e.g. cert-manager) manifests. Signed-off-by: Todd Short <[email protected]>
1 parent 5f8f202 commit 87dfebe

File tree

9 files changed

+5
-330
lines changed

9 files changed

+5
-330
lines changed

internal/httputil/certutil.go

Lines changed: 3 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package httputil
22

33
import (
4-
"bytes"
54
"crypto/x509"
6-
"encoding/base64"
7-
"encoding/pem"
85
"fmt"
96
"os"
107
"path/filepath"
@@ -13,192 +10,6 @@ import (
1310
"github.com/go-logr/logr"
1411
)
1512

16-
var pemStart = []byte("\n-----BEGIN ")
17-
var pemEnd = []byte("\n-----END ")
18-
var pemEndOfLine = []byte("-----")
19-
var colon = []byte(":")
20-
21-
// getLine results the first \r\n or \n delineated line from the given byte
22-
// array. The line does not include trailing whitespace or the trailing new
23-
// line bytes. The remainder of the byte array (also not including the new line
24-
// bytes) is also returned and this will always be smaller than the original
25-
// argument.
26-
func getLine(data []byte) ([]byte, []byte) {
27-
i := bytes.IndexByte(data, '\n')
28-
var j int
29-
if i < 0 {
30-
i = len(data)
31-
j = i
32-
} else {
33-
j = i + 1
34-
if i > 0 && data[i-1] == '\r' {
35-
i--
36-
}
37-
}
38-
return bytes.TrimRight(data[0:i], " \t"), data[j:]
39-
}
40-
41-
// removeSpacesAndTabs returns a copy of its input with all spaces and tabs
42-
// removed, if there were any. Otherwise, the input is returned unchanged.
43-
//
44-
// The base64 decoder already skips newline characters, so we don't need to
45-
// filter them out here.
46-
func removeSpacesAndTabs(data []byte) []byte {
47-
if !bytes.ContainsAny(data, " \t") {
48-
// Fast path; most base64 data within PEM contains newlines, but
49-
// no spaces nor tabs. Skip the extra alloc and work.
50-
return data
51-
}
52-
result := make([]byte, len(data))
53-
n := 0
54-
55-
for _, b := range data {
56-
if b == ' ' || b == '\t' {
57-
continue
58-
}
59-
result[n] = b
60-
n++
61-
}
62-
63-
return result[0:n]
64-
}
65-
66-
// This version of pem.Decode() is a bit less flexible, it will not skip over bad PEM
67-
// It is basically the guts of pem.Decode() inside the outer for loop, with error
68-
// returns rather than continues
69-
func pemDecode(data []byte) (*pem.Block, []byte) {
70-
// pemStart begins with a newline. However, at the very beginning of
71-
// the byte array, we'll accept the start string without it.
72-
rest := data
73-
if bytes.HasPrefix(rest, pemStart[1:]) {
74-
rest = rest[len(pemStart)-1:]
75-
} else if _, after, ok := bytes.Cut(rest, pemStart); ok {
76-
rest = after
77-
} else {
78-
return nil, data
79-
}
80-
81-
var typeLine []byte
82-
typeLine, rest = getLine(rest)
83-
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
84-
return nil, data
85-
}
86-
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
87-
88-
p := &pem.Block{
89-
Headers: make(map[string]string),
90-
Type: string(typeLine),
91-
}
92-
93-
for {
94-
// This loop terminates because getLine's second result is
95-
// always smaller than its argument.
96-
if len(rest) == 0 {
97-
return nil, data
98-
}
99-
line, next := getLine(rest)
100-
101-
key, val, ok := bytes.Cut(line, colon)
102-
if !ok {
103-
break
104-
}
105-
106-
key = bytes.TrimSpace(key)
107-
val = bytes.TrimSpace(val)
108-
p.Headers[string(key)] = string(val)
109-
rest = next
110-
}
111-
112-
var endIndex, endTrailerIndex int
113-
114-
// If there were no headers, the END line might occur
115-
// immediately, without a leading newline.
116-
if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) {
117-
endIndex = 0
118-
endTrailerIndex = len(pemEnd) - 1
119-
} else {
120-
endIndex = bytes.Index(rest, pemEnd)
121-
endTrailerIndex = endIndex + len(pemEnd)
122-
}
123-
124-
if endIndex < 0 {
125-
return nil, data
126-
}
127-
128-
// After the "-----" of the ending line, there should be the same type
129-
// and then a final five dashes.
130-
endTrailer := rest[endTrailerIndex:]
131-
endTrailerLen := len(typeLine) + len(pemEndOfLine)
132-
if len(endTrailer) < endTrailerLen {
133-
return nil, data
134-
}
135-
136-
restOfEndLine := endTrailer[endTrailerLen:]
137-
endTrailer = endTrailer[:endTrailerLen]
138-
if !bytes.HasPrefix(endTrailer, typeLine) ||
139-
!bytes.HasSuffix(endTrailer, pemEndOfLine) {
140-
return nil, data
141-
}
142-
143-
// The line must end with only whitespace.
144-
if s, _ := getLine(restOfEndLine); len(s) != 0 {
145-
return nil, data
146-
}
147-
148-
base64Data := removeSpacesAndTabs(rest[:endIndex])
149-
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
150-
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
151-
if err != nil {
152-
return nil, data
153-
}
154-
p.Bytes = p.Bytes[:n]
155-
156-
// the -1 is because we might have only matched pemEnd without the
157-
// leading newline if the PEM block was empty.
158-
_, rest = getLine(rest[endIndex+len(pemEnd)-1:])
159-
return p, rest
160-
}
161-
162-
// This version of (*x509.CertPool).AppendCertsFromPEM() will error out if parsing fails
163-
func appendCertsFromPEM(s *x509.CertPool, pemCerts []byte, firstExpiration *time.Time) error {
164-
n := 1
165-
for len(pemCerts) > 0 {
166-
var block *pem.Block
167-
block, pemCerts = pemDecode(pemCerts)
168-
if block == nil {
169-
return fmt.Errorf("unable to PEM decode cert %d", n)
170-
}
171-
// ignore non-certificates (e.g. keys)
172-
if block.Type != "CERTIFICATE" {
173-
continue
174-
}
175-
if len(block.Headers) != 0 {
176-
// This is a cert, but we're ignoring it, so bump the counter
177-
n++
178-
continue
179-
}
180-
181-
cert, err := x509.ParseCertificate(block.Bytes)
182-
if err != nil {
183-
return fmt.Errorf("unable to parse cert %d: %w", n, err)
184-
}
185-
if firstExpiration.IsZero() || firstExpiration.After(cert.NotAfter) {
186-
*firstExpiration = cert.NotAfter
187-
}
188-
now := time.Now()
189-
if now.Before(cert.NotBefore) {
190-
return fmt.Errorf("not yet valid cert %d: %q", n, cert.NotBefore.Format(time.RFC3339))
191-
} else if now.After(cert.NotAfter) {
192-
return fmt.Errorf("expired cert %d: %q", n, cert.NotAfter.Format(time.RFC3339))
193-
}
194-
// no return values - panics or always succeeds
195-
s.AddCert(cert)
196-
n++
197-
}
198-
199-
return nil
200-
}
201-
20213
func NewCertPool(caDir string, log logr.Logger) (*x509.CertPool, error) {
20314
caCertPool, err := x509.SystemCertPool()
20415
if err != nil {
@@ -231,11 +42,10 @@ func NewCertPool(caDir string, log logr.Logger) (*x509.CertPool, error) {
23142
if err != nil {
23243
return nil, fmt.Errorf("error reading cert file %q: %w", file, err)
23344
}
234-
err = appendCertsFromPEM(caCertPool, data, &firstExpiration)
235-
if err != nil {
236-
return nil, fmt.Errorf("error adding cert file %q: %w", file, err)
45+
// The return indicates if any certs were added
46+
if caCertPool.AppendCertsFromPEM(data) {
47+
count++
23748
}
238-
count++
23949
}
24050

24151
// Found no certs!

internal/httputil/certutil_test.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,15 @@ import (
1111
)
1212

1313
// The "good" test consists of 3 Amazon Root CAs, along with a "PRIVATE KEY" in one of the files
14-
// The "bad" test consists of 2 Amazon Root CAs, the second of which is garbage, and the test fails
15-
// The "ugly" test consists of a single file:
16-
// - Amazon_Root_CA_1
17-
// - garbage PEM
18-
// - Amazon_Root_CA_3
19-
// The error is _not_ detected because the golang standard library PEM decoder skips right over the garbage
20-
// This demonstrates the danger of putting multiple certificates into a single file
14+
// The "empty" test includes a single file with no PEM contents
2115
func TestNewCertPool(t *testing.T) {
2216
caDirs := []struct {
2317
dir string
2418
msg string
2519
}{
2620
{"../../testdata/certs/", `no certificates found in "../../testdata/certs/"`},
2721
{"../../testdata/certs/good", ""},
28-
{"../../testdata/certs/bad", `error adding cert file "../../testdata/certs/bad/Amazon_Root_CA_2.pem": unable to PEM decode cert 1`},
29-
{"../../testdata/certs/ugly", `error adding cert file "../../testdata/certs/ugly/Amazon_Root_CA.pem": unable to PEM decode cert 2`},
30-
{"../../testdata/certs/ugly2", `error adding cert file "../../testdata/certs/ugly2/Amazon_Root_CA_1.pem": unable to PEM decode cert 1`},
31-
{"../../testdata/certs/ugly3", `error adding cert file "../../testdata/certs/ugly3/not_a_cert.pem": unable to PEM decode cert 1`},
32-
{"../../testdata/certs/empty", `error adding cert file "../../testdata/certs/empty/empty.pem": unable to parse cert 1: x509: malformed certificate`},
33-
{"../../testdata/certs/expired", `error adding cert file "../../testdata/certs/expired/expired.pem": expired cert 1: "2024-01-02T15:00:00Z"`},
22+
{"../../testdata/certs/empty", `no certificates found in "../../testdata/certs/empty"`},
3423
}
3524

3625
log, _ := logr.FromContext(context.Background())

testdata/certs/bad/Amazon_Root_CA_1.pem

Lines changed: 0 additions & 20 deletions
This file was deleted.

testdata/certs/bad/Amazon_Root_CA_2.pem

Lines changed: 0 additions & 3 deletions
This file was deleted.

testdata/certs/bad/Amazon_Root_CA_3.pem

Lines changed: 0 additions & 12 deletions
This file was deleted.

testdata/certs/expired/expired.pem

Lines changed: 0 additions & 31 deletions
This file was deleted.

testdata/certs/ugly/Amazon_Root_CA.pem

Lines changed: 0 additions & 37 deletions
This file was deleted.

testdata/certs/ugly2/Amazon_Root_CA_1.pem

Lines changed: 0 additions & 20 deletions
This file was deleted.

testdata/certs/ugly3/not_a_cert.pem

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)