Skip to content

Commit 4d2b03d

Browse files
cpugopherbot
authored andcommitted
crypto/tls: add BetterTLS test coverage
This commit adds test coverage of path building and name constraint verification using the suite of test data provided by Netflix's BetterTLS project. Since the uncompressed raw JSON test data exported by BetterTLS for external test integrations is ~31MB we use a similar approach to the BoGo and ACVP test integrations and fetch the BetterTLS Go module, and run its export tool on-the-fly to generate the test data in a tempdir. As expected, all tests pass currently and this coverage is mainly helpful in catching regressions, especially with tricky/cursed name constraints. Change-Id: I23d7c24232e314aece86bcbfd133b7f02c9e71b5 Reviewed-on: https://go-review.googlesource.com/c/go/+/717420 TryBot-Bypass: Daniel McCarney <daniel@binaryparadox.net> Reviewed-by: Roland Shoemaker <roland@golang.org> Auto-Submit: Daniel McCarney <daniel@binaryparadox.net> Reviewed-by: Michael Pratt <mpratt@google.com>
1 parent 0c4444e commit 4d2b03d

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

src/crypto/tls/bettertls_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This test uses Netflix's BetterTLS test suite to test the crypto/x509
6+
// path building and name constraint validation.
7+
//
8+
// The test data in JSON form is around 31MB, so we fetch the BetterTLS
9+
// go module and use it to generate the JSON data on-the-fly in a tmp dir.
10+
//
11+
// For more information, see:
12+
// https://github.com/netflix/bettertls
13+
// https://netflixtechblog.com/bettertls-c9915cd255c0
14+
15+
package tls_test
16+
17+
import (
18+
"crypto/internal/cryptotest"
19+
"crypto/x509"
20+
"encoding/base64"
21+
"encoding/json"
22+
"internal/testenv"
23+
"os"
24+
"path/filepath"
25+
"testing"
26+
)
27+
28+
// TestBetterTLS runs the "pathbuilding" and "nameconstraints" suites of
29+
// BetterTLS.
30+
//
31+
// The test cases in the pathbuilding suite are designed to test edge-cases
32+
// for path building and validation. In particular, the ["chain of pain"][0]
33+
// scenario where a validator treats path building as an operation with
34+
// a single possible outcome, instead of many.
35+
//
36+
// The test cases in the nameconstraints suite are designed to test edge-cases
37+
// for name constraint parsing and validation.
38+
//
39+
// [0]: https://medium.com/@sleevi_/path-building-vs-path-verifying-the-chain-of-pain-9fbab861d7d6
40+
func TestBetterTLS(t *testing.T) {
41+
testenv.SkipIfShortAndSlow(t)
42+
43+
data, roots := testData(t)
44+
45+
for _, suite := range []string{"pathbuilding", "nameconstraints"} {
46+
t.Run(suite, func(t *testing.T) {
47+
runTestSuite(t, suite, &data, roots)
48+
})
49+
}
50+
}
51+
52+
func runTestSuite(t *testing.T, suiteName string, data *betterTLS, roots *x509.CertPool) {
53+
suite, exists := data.Suites[suiteName]
54+
if !exists {
55+
t.Fatalf("missing %s suite", suiteName)
56+
}
57+
58+
t.Logf(
59+
"running %s test suite with %d test cases",
60+
suiteName, len(suite.TestCases))
61+
62+
for _, tc := range suite.TestCases {
63+
t.Logf("testing %s test case %d", suiteName, tc.ID)
64+
65+
certsDER, err := tc.Certs()
66+
if err != nil {
67+
t.Fatalf(
68+
"failed to decode certificates for test case %d: %v",
69+
tc.ID, err)
70+
}
71+
72+
if len(certsDER) == 0 {
73+
t.Fatalf("test case %d has no certificates", tc.ID)
74+
}
75+
76+
eeCert, err := x509.ParseCertificate(certsDER[0])
77+
if err != nil {
78+
// Several constraint test cases contain invalid end-entity
79+
// certificate extensions that we reject ahead of verification
80+
// time. We consider this a pass and skip further processing.
81+
//
82+
// For example, a SAN with a uniformResourceIdentifier general name
83+
// containing the value `"http://foo.bar, DNS:test.localhost"`, or
84+
// an iPAddress general name of the wrong length.
85+
if suiteName == "nameconstraints" && tc.Expected == expectedReject {
86+
t.Logf(
87+
"skipping expected reject test case %d "+
88+
"- end entity certificate parse error: %v",
89+
tc.ID, err)
90+
continue
91+
}
92+
t.Fatalf(
93+
"failed to parse end entity certificate for test case %d: %v",
94+
tc.ID, err)
95+
}
96+
97+
intermediates := x509.NewCertPool()
98+
for i, certDER := range certsDER[1:] {
99+
cert, err := x509.ParseCertificate(certDER)
100+
if err != nil {
101+
t.Fatalf(
102+
"failed to parse intermediate certificate %d for test case %d: %v",
103+
i+1, tc.ID, err)
104+
}
105+
intermediates.AddCert(cert)
106+
}
107+
108+
_, err = eeCert.Verify(x509.VerifyOptions{
109+
Roots: roots,
110+
Intermediates: intermediates,
111+
DNSName: tc.Hostname,
112+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
113+
})
114+
115+
switch tc.Expected {
116+
case expectedAccept:
117+
if err != nil {
118+
t.Errorf(
119+
"test case %d failed: expected success, got error: %v",
120+
tc.ID, err)
121+
}
122+
case expectedReject:
123+
if err == nil {
124+
t.Errorf(
125+
"test case %d failed: expected failure, but verification succeeded",
126+
tc.ID)
127+
}
128+
default:
129+
t.Fatalf(
130+
"test case %d failed: unknown expected result: %s",
131+
tc.ID, tc.Expected)
132+
}
133+
}
134+
}
135+
136+
func testData(t *testing.T) (betterTLS, *x509.CertPool) {
137+
const (
138+
bettertlsModule = "github.com/Netflix/bettertls"
139+
bettertlsVersion = "v0.0.0-20250909192348-e1e99e353074"
140+
)
141+
142+
bettertlsDir := cryptotest.FetchModule(t, bettertlsModule, bettertlsVersion)
143+
144+
tempDir := t.TempDir()
145+
testsJSONPath := filepath.Join(tempDir, "tests.json")
146+
147+
cmd := testenv.Command(t, testenv.GoToolPath(t),
148+
"run", "./test-suites/cmd/bettertls",
149+
"export-tests",
150+
"--out", testsJSONPath)
151+
cmd.Dir = bettertlsDir
152+
153+
t.Log("running bettertls export-tests command")
154+
output, err := cmd.CombinedOutput()
155+
if err != nil {
156+
t.Fatalf(
157+
"failed to run bettertls export-tests: %v\nOutput: %s",
158+
err, output)
159+
}
160+
161+
jsonData, err := os.ReadFile(testsJSONPath)
162+
if err != nil {
163+
t.Fatalf("failed to read exported tests.json: %v", err)
164+
}
165+
166+
t.Logf("successfully loaded tests.json at %s", testsJSONPath)
167+
168+
var data betterTLS
169+
if err := json.Unmarshal(jsonData, &data); err != nil {
170+
t.Fatalf("failed to unmarshal JSON data: %v", err)
171+
}
172+
173+
t.Logf("testing betterTLS revision: %s", data.Revision)
174+
t.Logf("number of test suites: %d", len(data.Suites))
175+
176+
rootDER, err := data.RootCert()
177+
if err != nil {
178+
t.Fatalf("failed to decode trust root: %v", err)
179+
}
180+
181+
rootCert, err := x509.ParseCertificate(rootDER)
182+
if err != nil {
183+
t.Fatalf("failed to parse trust root certificate: %v", err)
184+
}
185+
186+
roots := x509.NewCertPool()
187+
roots.AddCert(rootCert)
188+
189+
return data, roots
190+
}
191+
192+
type betterTLS struct {
193+
Revision string `json:"betterTlsRevision"`
194+
Root string `json:"trustRoot"`
195+
Suites map[string]betterTLSSuite `json:"suites"`
196+
}
197+
198+
func (b *betterTLS) RootCert() ([]byte, error) {
199+
return base64.StdEncoding.DecodeString(b.Root)
200+
}
201+
202+
type betterTLSSuite struct {
203+
TestCases []betterTLSTest `json:"testCases"`
204+
}
205+
206+
type betterTLSTest struct {
207+
ID uint32 `json:"id"`
208+
Certificates []string `json:"certificates"`
209+
Hostname string `json:"hostname"`
210+
Expected expectedResult `json:"expected"`
211+
}
212+
213+
func (test *betterTLSTest) Certs() ([][]byte, error) {
214+
certs := make([][]byte, len(test.Certificates))
215+
for i, cert := range test.Certificates {
216+
decoded, err := base64.StdEncoding.DecodeString(cert)
217+
if err != nil {
218+
return nil, err
219+
}
220+
certs[i] = decoded
221+
}
222+
return certs, nil
223+
}
224+
225+
type expectedResult string
226+
227+
const (
228+
expectedAccept expectedResult = "ACCEPT"
229+
expectedReject expectedResult = "REJECT"
230+
)

0 commit comments

Comments
 (0)