mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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>
This commit is contained in:
parent
0c4444e13d
commit
4d2b03d2fc
1 changed files with 230 additions and 0 deletions
230
src/crypto/tls/bettertls_test.go
Normal file
230
src/crypto/tls/bettertls_test.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This test uses Netflix's BetterTLS test suite to test the crypto/x509
|
||||
// path building and name constraint validation.
|
||||
//
|
||||
// The test data in JSON form is around 31MB, so we fetch the BetterTLS
|
||||
// go module and use it to generate the JSON data on-the-fly in a tmp dir.
|
||||
//
|
||||
// For more information, see:
|
||||
// https://github.com/netflix/bettertls
|
||||
// https://netflixtechblog.com/bettertls-c9915cd255c0
|
||||
|
||||
package tls_test
|
||||
|
||||
import (
|
||||
"crypto/internal/cryptotest"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBetterTLS runs the "pathbuilding" and "nameconstraints" suites of
|
||||
// BetterTLS.
|
||||
//
|
||||
// The test cases in the pathbuilding suite are designed to test edge-cases
|
||||
// for path building and validation. In particular, the ["chain of pain"][0]
|
||||
// scenario where a validator treats path building as an operation with
|
||||
// a single possible outcome, instead of many.
|
||||
//
|
||||
// The test cases in the nameconstraints suite are designed to test edge-cases
|
||||
// for name constraint parsing and validation.
|
||||
//
|
||||
// [0]: https://medium.com/@sleevi_/path-building-vs-path-verifying-the-chain-of-pain-9fbab861d7d6
|
||||
func TestBetterTLS(t *testing.T) {
|
||||
testenv.SkipIfShortAndSlow(t)
|
||||
|
||||
data, roots := testData(t)
|
||||
|
||||
for _, suite := range []string{"pathbuilding", "nameconstraints"} {
|
||||
t.Run(suite, func(t *testing.T) {
|
||||
runTestSuite(t, suite, &data, roots)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runTestSuite(t *testing.T, suiteName string, data *betterTLS, roots *x509.CertPool) {
|
||||
suite, exists := data.Suites[suiteName]
|
||||
if !exists {
|
||||
t.Fatalf("missing %s suite", suiteName)
|
||||
}
|
||||
|
||||
t.Logf(
|
||||
"running %s test suite with %d test cases",
|
||||
suiteName, len(suite.TestCases))
|
||||
|
||||
for _, tc := range suite.TestCases {
|
||||
t.Logf("testing %s test case %d", suiteName, tc.ID)
|
||||
|
||||
certsDER, err := tc.Certs()
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"failed to decode certificates for test case %d: %v",
|
||||
tc.ID, err)
|
||||
}
|
||||
|
||||
if len(certsDER) == 0 {
|
||||
t.Fatalf("test case %d has no certificates", tc.ID)
|
||||
}
|
||||
|
||||
eeCert, err := x509.ParseCertificate(certsDER[0])
|
||||
if err != nil {
|
||||
// Several constraint test cases contain invalid end-entity
|
||||
// certificate extensions that we reject ahead of verification
|
||||
// time. We consider this a pass and skip further processing.
|
||||
//
|
||||
// For example, a SAN with a uniformResourceIdentifier general name
|
||||
// containing the value `"http://foo.bar, DNS:test.localhost"`, or
|
||||
// an iPAddress general name of the wrong length.
|
||||
if suiteName == "nameconstraints" && tc.Expected == expectedReject {
|
||||
t.Logf(
|
||||
"skipping expected reject test case %d "+
|
||||
"- end entity certificate parse error: %v",
|
||||
tc.ID, err)
|
||||
continue
|
||||
}
|
||||
t.Fatalf(
|
||||
"failed to parse end entity certificate for test case %d: %v",
|
||||
tc.ID, err)
|
||||
}
|
||||
|
||||
intermediates := x509.NewCertPool()
|
||||
for i, certDER := range certsDER[1:] {
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"failed to parse intermediate certificate %d for test case %d: %v",
|
||||
i+1, tc.ID, err)
|
||||
}
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
_, err = eeCert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
DNSName: tc.Hostname,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
})
|
||||
|
||||
switch tc.Expected {
|
||||
case expectedAccept:
|
||||
if err != nil {
|
||||
t.Errorf(
|
||||
"test case %d failed: expected success, got error: %v",
|
||||
tc.ID, err)
|
||||
}
|
||||
case expectedReject:
|
||||
if err == nil {
|
||||
t.Errorf(
|
||||
"test case %d failed: expected failure, but verification succeeded",
|
||||
tc.ID)
|
||||
}
|
||||
default:
|
||||
t.Fatalf(
|
||||
"test case %d failed: unknown expected result: %s",
|
||||
tc.ID, tc.Expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testData(t *testing.T) (betterTLS, *x509.CertPool) {
|
||||
const (
|
||||
bettertlsModule = "github.com/Netflix/bettertls"
|
||||
bettertlsVersion = "v0.0.0-20250909192348-e1e99e353074"
|
||||
)
|
||||
|
||||
bettertlsDir := cryptotest.FetchModule(t, bettertlsModule, bettertlsVersion)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
testsJSONPath := filepath.Join(tempDir, "tests.json")
|
||||
|
||||
cmd := testenv.Command(t, testenv.GoToolPath(t),
|
||||
"run", "./test-suites/cmd/bettertls",
|
||||
"export-tests",
|
||||
"--out", testsJSONPath)
|
||||
cmd.Dir = bettertlsDir
|
||||
|
||||
t.Log("running bettertls export-tests command")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"failed to run bettertls export-tests: %v\nOutput: %s",
|
||||
err, output)
|
||||
}
|
||||
|
||||
jsonData, err := os.ReadFile(testsJSONPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read exported tests.json: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("successfully loaded tests.json at %s", testsJSONPath)
|
||||
|
||||
var data betterTLS
|
||||
if err := json.Unmarshal(jsonData, &data); err != nil {
|
||||
t.Fatalf("failed to unmarshal JSON data: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("testing betterTLS revision: %s", data.Revision)
|
||||
t.Logf("number of test suites: %d", len(data.Suites))
|
||||
|
||||
rootDER, err := data.RootCert()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode trust root: %v", err)
|
||||
}
|
||||
|
||||
rootCert, err := x509.ParseCertificate(rootDER)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse trust root certificate: %v", err)
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(rootCert)
|
||||
|
||||
return data, roots
|
||||
}
|
||||
|
||||
type betterTLS struct {
|
||||
Revision string `json:"betterTlsRevision"`
|
||||
Root string `json:"trustRoot"`
|
||||
Suites map[string]betterTLSSuite `json:"suites"`
|
||||
}
|
||||
|
||||
func (b *betterTLS) RootCert() ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(b.Root)
|
||||
}
|
||||
|
||||
type betterTLSSuite struct {
|
||||
TestCases []betterTLSTest `json:"testCases"`
|
||||
}
|
||||
|
||||
type betterTLSTest struct {
|
||||
ID uint32 `json:"id"`
|
||||
Certificates []string `json:"certificates"`
|
||||
Hostname string `json:"hostname"`
|
||||
Expected expectedResult `json:"expected"`
|
||||
}
|
||||
|
||||
func (test *betterTLSTest) Certs() ([][]byte, error) {
|
||||
certs := make([][]byte, len(test.Certificates))
|
||||
for i, cert := range test.Certificates {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs[i] = decoded
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
type expectedResult string
|
||||
|
||||
const (
|
||||
expectedAccept expectedResult = "ACCEPT"
|
||||
expectedReject expectedResult = "REJECT"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue