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  	t.Log("running bettertls export-tests command")
   152  	if out, err := cmd.CombinedOutput(); err != nil {
   153  		t.Fatalf("failed to run bettertls export-tests: %v\n%s", err, out)
   154  	}
   155  
   156  	jsonData, err := os.ReadFile(testsJSONPath)
   157  	if err != nil {
   158  		t.Fatalf("failed to read exported tests.json: %v", err)
   159  	}
   160  
   161  	t.Logf("successfully loaded tests.json at %s", testsJSONPath)
   162  
   163  	var data betterTLS
   164  	if err := json.Unmarshal(jsonData, &data); err != nil {
   165  		t.Fatalf("failed to unmarshal JSON data: %v", err)
   166  	}
   167  
   168  	t.Logf("testing betterTLS revision: %s", data.Revision)
   169  	t.Logf("number of test suites: %d", len(data.Suites))
   170  
   171  	rootDER, err := data.RootCert()
   172  	if err != nil {
   173  		t.Fatalf("failed to decode trust root: %v", err)
   174  	}
   175  
   176  	rootCert, err := ParseCertificate(rootDER)
   177  	if err != nil {
   178  		t.Fatalf("failed to parse trust root certificate: %v", err)
   179  	}
   180  
   181  	roots := NewCertPool()
   182  	roots.AddCert(rootCert)
   183  
   184  	return data, roots
   185  }
   186  
   187  type betterTLS struct {
   188  	Revision string                    `json:"betterTlsRevision"`
   189  	Root     string                    `json:"trustRoot"`
   190  	Suites   map[string]betterTLSSuite `json:"suites"`
   191  }
   192  
   193  func (b *betterTLS) RootCert() ([]byte, error) {
   194  	return base64.StdEncoding.DecodeString(b.Root)
   195  }
   196  
   197  type betterTLSSuite struct {
   198  	TestCases []betterTLSTest `json:"testCases"`
   199  }
   200  
   201  type betterTLSTest struct {
   202  	ID           uint32         `json:"id"`
   203  	Certificates []string       `json:"certificates"`
   204  	Hostname     string         `json:"hostname"`
   205  	Expected     expectedResult `json:"expected"`
   206  }
   207  
   208  func (test *betterTLSTest) Certs() ([][]byte, error) {
   209  	certs := make([][]byte, len(test.Certificates))
   210  	for i, cert := range test.Certificates {
   211  		decoded, err := base64.StdEncoding.DecodeString(cert)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		certs[i] = decoded
   216  	}
   217  	return certs, nil
   218  }
   219  
   220  type expectedResult string
   221  
   222  const (
   223  	expectedAccept expectedResult = "ACCEPT"
   224  	expectedReject expectedResult = "REJECT"
   225  )
   226  

View as plain text