Source file src/crypto/x509/bettertls_test.go

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

View as plain text