Source file src/crypto/tls/bogo_shim_test.go

     1  // Copyright 2024 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  package tls
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/internal/cryptotest"
    10  	"crypto/x509"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"encoding/pem"
    14  	"errors"
    15  	"flag"
    16  	"fmt"
    17  	"html/template"
    18  	"internal/byteorder"
    19  	"internal/testenv"
    20  	"io"
    21  	"log"
    22  	"net"
    23  	"os"
    24  	"path/filepath"
    25  	"runtime"
    26  	"slices"
    27  	"strconv"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"golang.org/x/crypto/cryptobyte"
    33  )
    34  
    35  // boringsslModVer is the version of BoringSSL that we test against.
    36  // The pseudo-version can be found by executing:
    37  //
    38  //	go mod download -json boringssl.googlesource.com/boringssl.git@latest
    39  const boringsslModVer = "v0.0.0-20260209204302-2a7ca5404e13"
    40  
    41  var (
    42  	port   = flag.String("port", "", "")
    43  	server = flag.Bool("server", false, "")
    44  
    45  	isHandshakerSupported = flag.Bool("is-handshaker-supported", false, "")
    46  
    47  	keyfile      = flag.String("key-file", "", "")
    48  	certfile     = flag.String("cert-file", "", "")
    49  	ocspResponse = flagBase64("ocsp-response", "")
    50  	signingPrefs = flagIntSlice("signing-prefs", "")
    51  
    52  	trustCert = flag.String("trust-cert", "", "")
    53  
    54  	minVersion    = flag.Int("min-version", VersionSSL30, "")
    55  	maxVersion    = flag.Int("max-version", VersionTLS13, "")
    56  	expectVersion = flag.Int("expect-version", 0, "")
    57  
    58  	noTLS1  = flag.Bool("no-tls1", false, "")
    59  	noTLS11 = flag.Bool("no-tls11", false, "")
    60  	noTLS12 = flag.Bool("no-tls12", false, "")
    61  	noTLS13 = flag.Bool("no-tls13", false, "")
    62  
    63  	requireAnyClientCertificate = flag.Bool("require-any-client-certificate", false, "")
    64  
    65  	shimWritesFirst = flag.Bool("shim-writes-first", false, "")
    66  
    67  	resumeCount = flag.Int("resume-count", 0, "")
    68  
    69  	curves        = flagIntSlice("curves", "")
    70  	expectedCurve = flag.String("expect-curve-id", "", "")
    71  
    72  	verifyPrefs        = flagIntSlice("verify-prefs", "")
    73  	expectedSigAlg     = flag.String("expect-peer-signature-algorithm", "", "")
    74  	expectedPeerSigAlg = flagIntSlice("expect-peer-verify-pref", "")
    75  
    76  	shimID = flag.Uint64("shim-id", 0, "")
    77  	_      = flag.Bool("ipv6", false, "")
    78  
    79  	echConfigList              = flagBase64("ech-config-list", "")
    80  	expectECHAccepted          = flag.Bool("expect-ech-accept", false, "")
    81  	expectHRR                  = flag.Bool("expect-hrr", false, "")
    82  	expectNoHRR                = flag.Bool("expect-no-hrr", false, "")
    83  	expectedECHRetryConfigs    = flag.String("expect-ech-retry-configs", "", "")
    84  	expectNoECHRetryConfigs    = flag.Bool("expect-no-ech-retry-configs", false, "")
    85  	onInitialExpectECHAccepted = flag.Bool("on-initial-expect-ech-accept", false, "")
    86  	_                          = flag.Bool("expect-no-ech-name-override", false, "")
    87  	_                          = flag.String("expect-ech-name-override", "", "")
    88  	_                          = flag.Bool("reverify-on-resume", false, "")
    89  	onResumeECHConfigList      = flagBase64("on-resume-ech-config-list", "")
    90  	_                          = flag.Bool("on-resume-expect-reject-early-data", false, "")
    91  	onResumeExpectECHAccepted  = flag.Bool("on-resume-expect-ech-accept", false, "")
    92  	_                          = flag.Bool("on-resume-expect-no-ech-name-override", false, "")
    93  	expectedServerName         = flag.String("expect-server-name", "", "")
    94  	echServerConfig            = flagStringSlice("ech-server-config", "")
    95  	echServerKey               = flagStringSlice("ech-server-key", "")
    96  	echServerRetryConfig       = flagStringSlice("ech-is-retry-config", "")
    97  
    98  	expectSessionMiss = flag.Bool("expect-session-miss", false, "")
    99  
   100  	_ = flag.Bool("enable-early-data", false, "")
   101  	_ = flag.Bool("on-resume-expect-accept-early-data", false, "")
   102  	_ = flag.Bool("expect-ticket-supports-early-data", false, "")
   103  	_ = flag.Bool("on-resume-shim-writes-first", false, "")
   104  
   105  	advertiseALPN        = flag.String("advertise-alpn", "", "")
   106  	expectALPN           = flag.String("expect-alpn", "", "")
   107  	rejectALPN           = flag.Bool("reject-alpn", false, "")
   108  	declineALPN          = flag.Bool("decline-alpn", false, "")
   109  	expectAdvertisedALPN = flag.String("expect-advertised-alpn", "", "")
   110  	selectALPN           = flag.String("select-alpn", "", "")
   111  
   112  	hostName = flag.String("host-name", "", "")
   113  
   114  	verifyPeer = flag.Bool("verify-peer", false, "")
   115  	_          = flag.Bool("use-custom-verify-callback", false, "")
   116  
   117  	waitForDebugger = flag.Bool("wait-for-debugger", false, "")
   118  )
   119  
   120  type stringSlice []string
   121  
   122  func flagStringSlice(name, usage string) *stringSlice {
   123  	f := new(stringSlice)
   124  	flag.Var(f, name, usage)
   125  	return f
   126  }
   127  
   128  func (saf *stringSlice) String() string {
   129  	return strings.Join(*saf, ",")
   130  }
   131  
   132  func (saf *stringSlice) Set(s string) error {
   133  	*saf = append(*saf, s)
   134  	return nil
   135  }
   136  
   137  type intSlice []int64
   138  
   139  func flagIntSlice(name, usage string) *intSlice {
   140  	f := new(intSlice)
   141  	flag.Var(f, name, usage)
   142  	return f
   143  }
   144  
   145  func (sf *intSlice) String() string {
   146  	return strings.Join(strings.Split(fmt.Sprint(*sf), " "), ",")
   147  }
   148  
   149  func (sf *intSlice) Set(s string) error {
   150  	i, err := strconv.ParseInt(s, 10, 64)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	*sf = append(*sf, i)
   155  	return nil
   156  }
   157  
   158  type base64Flag []byte
   159  
   160  func flagBase64(name, usage string) *base64Flag {
   161  	f := new(base64Flag)
   162  	flag.Var(f, name, usage)
   163  	return f
   164  }
   165  
   166  func (f *base64Flag) String() string {
   167  	return base64.StdEncoding.EncodeToString(*f)
   168  }
   169  
   170  func (f *base64Flag) Set(s string) error {
   171  	if *f != nil {
   172  		return fmt.Errorf("multiple base64 values not supported")
   173  	}
   174  	b, err := base64.StdEncoding.DecodeString(s)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	*f = b
   179  	return nil
   180  }
   181  
   182  func bogoShim() {
   183  	if *isHandshakerSupported {
   184  		fmt.Println("No")
   185  		return
   186  	}
   187  
   188  	fmt.Printf("BoGo shim flags: %q", os.Args[1:])
   189  
   190  	// Test with both the default and insecure cipher suites.
   191  	var ciphersuites []uint16
   192  	for _, s := range append(CipherSuites(), InsecureCipherSuites()...) {
   193  		ciphersuites = append(ciphersuites, s.ID)
   194  	}
   195  
   196  	cfg := &Config{
   197  		ServerName: "test",
   198  
   199  		MinVersion: uint16(*minVersion),
   200  		MaxVersion: uint16(*maxVersion),
   201  
   202  		ClientSessionCache: NewLRUClientSessionCache(0),
   203  
   204  		CipherSuites: ciphersuites,
   205  
   206  		GetConfigForClient: func(chi *ClientHelloInfo) (*Config, error) {
   207  
   208  			if *expectAdvertisedALPN != "" {
   209  
   210  				s := cryptobyte.String(*expectAdvertisedALPN)
   211  
   212  				var expectedALPNs []string
   213  
   214  				for !s.Empty() {
   215  					var alpn cryptobyte.String
   216  					if !s.ReadUint8LengthPrefixed(&alpn) {
   217  						return nil, fmt.Errorf("unexpected error while parsing arguments for -expect-advertised-alpn")
   218  					}
   219  					expectedALPNs = append(expectedALPNs, string(alpn))
   220  				}
   221  
   222  				if !slices.Equal(chi.SupportedProtos, expectedALPNs) {
   223  					return nil, fmt.Errorf("unexpected ALPN: got %q, want %q", chi.SupportedProtos, expectedALPNs)
   224  				}
   225  			}
   226  			return nil, nil
   227  		},
   228  	}
   229  
   230  	if *noTLS1 {
   231  		cfg.MinVersion = VersionTLS11
   232  		if *noTLS11 {
   233  			cfg.MinVersion = VersionTLS12
   234  			if *noTLS12 {
   235  				cfg.MinVersion = VersionTLS13
   236  				if *noTLS13 {
   237  					log.Fatalf("no supported versions enabled")
   238  				}
   239  			}
   240  		}
   241  	} else if *noTLS13 {
   242  		cfg.MaxVersion = VersionTLS12
   243  		if *noTLS12 {
   244  			cfg.MaxVersion = VersionTLS11
   245  			if *noTLS11 {
   246  				cfg.MaxVersion = VersionTLS10
   247  				if *noTLS1 {
   248  					log.Fatalf("no supported versions enabled")
   249  				}
   250  			}
   251  		}
   252  	}
   253  
   254  	if *advertiseALPN != "" {
   255  		alpns := *advertiseALPN
   256  		for len(alpns) > 0 {
   257  			alpnLen := int(alpns[0])
   258  			cfg.NextProtos = append(cfg.NextProtos, alpns[1:1+alpnLen])
   259  			alpns = alpns[alpnLen+1:]
   260  		}
   261  	}
   262  
   263  	if *rejectALPN {
   264  		cfg.NextProtos = []string{"unnegotiableprotocol"}
   265  	}
   266  
   267  	if *declineALPN {
   268  		cfg.NextProtos = []string{}
   269  	}
   270  	if *selectALPN != "" {
   271  		cfg.NextProtos = []string{*selectALPN}
   272  	}
   273  
   274  	if *hostName != "" {
   275  		cfg.ServerName = *hostName
   276  	}
   277  
   278  	if *keyfile != "" || *certfile != "" {
   279  		pair, err := LoadX509KeyPair(*certfile, *keyfile)
   280  		if err != nil {
   281  			log.Fatalf("load key-file err: %s", err)
   282  		}
   283  		for _, id := range *signingPrefs {
   284  			pair.SupportedSignatureAlgorithms = append(pair.SupportedSignatureAlgorithms, SignatureScheme(id))
   285  		}
   286  		pair.OCSPStaple = *ocspResponse
   287  		// Use Get[Client]Certificate to force the use of the certificate, which
   288  		// more closely matches the BoGo expectations (e.g. handshake failure if
   289  		// no client certificates are compatible).
   290  		cfg.GetCertificate = func(chi *ClientHelloInfo) (*Certificate, error) {
   291  			if *expectedPeerSigAlg != nil {
   292  				if len(chi.SignatureSchemes) != len(*expectedPeerSigAlg) {
   293  					return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg)
   294  				}
   295  				for i := range *expectedPeerSigAlg {
   296  					if chi.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) {
   297  						return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg)
   298  					}
   299  				}
   300  			}
   301  			return &pair, nil
   302  		}
   303  		cfg.GetClientCertificate = func(cri *CertificateRequestInfo) (*Certificate, error) {
   304  			if *expectedPeerSigAlg != nil {
   305  				if len(cri.SignatureSchemes) != len(*expectedPeerSigAlg) {
   306  					return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg)
   307  				}
   308  				for i := range *expectedPeerSigAlg {
   309  					if cri.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) {
   310  						return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg)
   311  					}
   312  				}
   313  			}
   314  			return &pair, nil
   315  		}
   316  	}
   317  	if *trustCert != "" {
   318  		pool := x509.NewCertPool()
   319  		certFile, err := os.ReadFile(*trustCert)
   320  		if err != nil {
   321  			log.Fatalf("load trust-cert err: %s", err)
   322  		}
   323  		block, _ := pem.Decode(certFile)
   324  		cert, err := x509.ParseCertificate(block.Bytes)
   325  		if err != nil {
   326  			log.Fatalf("parse trust-cert err: %s", err)
   327  		}
   328  		pool.AddCert(cert)
   329  		cfg.RootCAs = pool
   330  	}
   331  
   332  	if *requireAnyClientCertificate {
   333  		cfg.ClientAuth = RequireAnyClientCert
   334  	}
   335  	if *verifyPeer {
   336  		cfg.ClientAuth = VerifyClientCertIfGiven
   337  	}
   338  
   339  	if *echConfigList != nil {
   340  		cfg.EncryptedClientHelloConfigList = *echConfigList
   341  		cfg.MinVersion = VersionTLS13
   342  	}
   343  
   344  	if *curves != nil {
   345  		for _, id := range *curves {
   346  			cfg.CurvePreferences = append(cfg.CurvePreferences, CurveID(id))
   347  		}
   348  	}
   349  
   350  	if *verifyPrefs != nil {
   351  		for _, id := range *verifyPrefs {
   352  			testingOnlySupportedSignatureAlgorithms = append(testingOnlySupportedSignatureAlgorithms, SignatureScheme(id))
   353  		}
   354  	}
   355  
   356  	if *echServerConfig != nil {
   357  		if len(*echServerConfig) != len(*echServerKey) || len(*echServerConfig) != len(*echServerRetryConfig) {
   358  			log.Fatal("-ech-server-config, -ech-server-key, and -ech-is-retry-config mismatch")
   359  		}
   360  
   361  		for i, c := range *echServerConfig {
   362  			configBytes, err := base64.StdEncoding.DecodeString(c)
   363  			if err != nil {
   364  				log.Fatalf("parse ech-server-config err: %s", err)
   365  			}
   366  			privBytes, err := base64.StdEncoding.DecodeString((*echServerKey)[i])
   367  			if err != nil {
   368  				log.Fatalf("parse ech-server-key err: %s", err)
   369  			}
   370  
   371  			cfg.EncryptedClientHelloKeys = append(cfg.EncryptedClientHelloKeys, EncryptedClientHelloKey{
   372  				Config:      configBytes,
   373  				PrivateKey:  privBytes,
   374  				SendAsRetry: (*echServerRetryConfig)[i] == "1",
   375  			})
   376  		}
   377  	}
   378  
   379  	for i := 0; i < *resumeCount+1; i++ {
   380  		if i > 0 && *onResumeECHConfigList != nil {
   381  			cfg.EncryptedClientHelloConfigList = *onResumeECHConfigList
   382  		}
   383  
   384  		conn, err := net.Dial("tcp", net.JoinHostPort("localhost", *port))
   385  		if err != nil {
   386  			log.Fatalf("dial err: %s", err)
   387  		}
   388  		defer conn.Close()
   389  
   390  		// Write the shim ID we were passed as a little endian uint64
   391  		shimIDBytes := make([]byte, 8)
   392  		byteorder.LEPutUint64(shimIDBytes, *shimID)
   393  		if _, err := conn.Write(shimIDBytes); err != nil {
   394  			log.Fatalf("failed to write shim id: %s", err)
   395  		}
   396  
   397  		var tlsConn *Conn
   398  		if *server {
   399  			tlsConn = Server(conn, cfg)
   400  		} else {
   401  			tlsConn = Client(conn, cfg)
   402  		}
   403  
   404  		if i == 0 && *shimWritesFirst {
   405  			if _, err := tlsConn.Write([]byte("hello")); err != nil {
   406  				log.Fatalf("write err: %s", err)
   407  			}
   408  		}
   409  
   410  		// If we were instructed to wait for a debugger, then send SIGSTOP to ourselves.
   411  		// When the debugger attaches it will continue the process.
   412  		if *waitForDebugger {
   413  			pauseProcess()
   414  		}
   415  
   416  		for {
   417  			buf := make([]byte, 500)
   418  			var n int
   419  			n, err = tlsConn.Read(buf)
   420  			if err != nil {
   421  				break
   422  			}
   423  			buf = buf[:n]
   424  			for i := range buf {
   425  				buf[i] ^= 0xff
   426  			}
   427  			if _, err = tlsConn.Write(buf); err != nil {
   428  				break
   429  			}
   430  		}
   431  		if err != io.EOF {
   432  			// Flush the TLS conn and then perform a graceful shutdown of the
   433  			// TCP connection to avoid the runner side hitting an unexpected
   434  			// write error before it has processed the alert we may have
   435  			// generated for the error condition.
   436  			orderlyShutdown(tlsConn)
   437  
   438  			retryErr, ok := err.(*ECHRejectionError)
   439  			if !ok {
   440  				log.Fatal(err)
   441  			}
   442  			if *expectNoECHRetryConfigs && len(retryErr.RetryConfigList) > 0 {
   443  				log.Fatalf("expected no ECH retry configs, got some")
   444  			}
   445  			if *expectedECHRetryConfigs != "" {
   446  				expectedRetryConfigs, err := base64.StdEncoding.DecodeString(*expectedECHRetryConfigs)
   447  				if err != nil {
   448  					log.Fatalf("failed to decode expected retry configs: %s", err)
   449  				}
   450  				if !bytes.Equal(retryErr.RetryConfigList, expectedRetryConfigs) {
   451  					log.Fatalf("unexpected retry list returned: got %x, want %x", retryErr.RetryConfigList, expectedRetryConfigs)
   452  				}
   453  			}
   454  			log.Fatalf("conn error: %s", err)
   455  		}
   456  
   457  		cs := tlsConn.ConnectionState()
   458  		if cs.HandshakeComplete {
   459  			if *expectALPN != "" && cs.NegotiatedProtocol != *expectALPN {
   460  				log.Fatalf("unexpected protocol negotiated: want %q, got %q", *expectALPN, cs.NegotiatedProtocol)
   461  			}
   462  
   463  			if *selectALPN != "" && cs.NegotiatedProtocol != *selectALPN {
   464  				log.Fatalf("unexpected protocol negotiated: want %q, got %q", *selectALPN, cs.NegotiatedProtocol)
   465  			}
   466  
   467  			if *expectVersion != 0 && cs.Version != uint16(*expectVersion) {
   468  				log.Fatalf("expected ssl version %d, got %d", *expectVersion, cs.Version)
   469  			}
   470  			if *declineALPN && cs.NegotiatedProtocol != "" {
   471  				log.Fatal("unexpected ALPN protocol")
   472  			}
   473  			if *expectECHAccepted && !cs.ECHAccepted {
   474  				log.Fatal("expected ECH to be accepted, but connection state shows it was not")
   475  			} else if i == 0 && *onInitialExpectECHAccepted && !cs.ECHAccepted {
   476  				log.Fatal("expected ECH to be accepted, but connection state shows it was not")
   477  			} else if i > 0 && *onResumeExpectECHAccepted && !cs.ECHAccepted {
   478  				log.Fatal("expected ECH to be accepted on resumption, but connection state shows it was not")
   479  			} else if i == 0 && !*expectECHAccepted && cs.ECHAccepted {
   480  				log.Fatal("did not expect ECH, but it was accepted")
   481  			}
   482  
   483  			if *expectHRR && !cs.HelloRetryRequest {
   484  				log.Fatal("expected HRR but did not do it")
   485  			}
   486  
   487  			if *expectNoHRR && cs.HelloRetryRequest {
   488  				log.Fatal("expected no HRR but did do it")
   489  			}
   490  
   491  			if *expectSessionMiss && cs.DidResume {
   492  				log.Fatal("unexpected session resumption")
   493  			}
   494  
   495  			if *expectedServerName != "" && cs.ServerName != *expectedServerName {
   496  				log.Fatalf("unexpected server name: got %q, want %q", cs.ServerName, *expectedServerName)
   497  			}
   498  		}
   499  
   500  		if *expectedCurve != "" {
   501  			expectedCurveID, err := strconv.Atoi(*expectedCurve)
   502  			if err != nil {
   503  				log.Fatalf("failed to parse -expect-curve-id: %s", err)
   504  			}
   505  			if cs.CurveID != CurveID(expectedCurveID) {
   506  				log.Fatalf("unexpected curve id: want %d, got %d", expectedCurveID, tlsConn.curveID)
   507  			}
   508  		}
   509  
   510  		// TODO: implement testingOnlyPeerSignatureAlgorithm on resumption.
   511  		if *expectedSigAlg != "" && !cs.DidResume {
   512  			expectedSigAlgID, err := strconv.Atoi(*expectedSigAlg)
   513  			if err != nil {
   514  				log.Fatalf("failed to parse -expect-peer-signature-algorithm: %s", err)
   515  			}
   516  			if cs.testingOnlyPeerSignatureAlgorithm != SignatureScheme(expectedSigAlgID) {
   517  				log.Fatalf("unexpected peer signature algorithm: want %s, got %s", SignatureScheme(expectedSigAlgID), cs.testingOnlyPeerSignatureAlgorithm)
   518  			}
   519  		}
   520  	}
   521  }
   522  
   523  // If the test case produces an error, we don't want to immediately close the
   524  // TCP connection after generating an alert. The runner side may try to write
   525  // additional data to the connection before it reads the alert. If the conn
   526  // has already been torn down, then these writes will produce an unexpected
   527  // broken pipe err and fail the test.
   528  func orderlyShutdown(tlsConn *Conn) {
   529  	// Flush any pending alert data
   530  	tlsConn.flush()
   531  
   532  	netConn := tlsConn.NetConn()
   533  	tcpConn := netConn.(*net.TCPConn)
   534  	tcpConn.CloseWrite()
   535  
   536  	// Read and discard any data that was sent by the peer.
   537  	buf := make([]byte, maxPlaintext)
   538  	for {
   539  		n, err := tcpConn.Read(buf)
   540  		if n == 0 || err != nil {
   541  			break
   542  		}
   543  	}
   544  
   545  	tcpConn.CloseRead()
   546  }
   547  
   548  func TestBogoSuite(t *testing.T) {
   549  	if testing.Short() {
   550  		t.Skip("skipping in short mode")
   551  	}
   552  	if testenv.Builder() != "" && runtime.GOOS == "windows" {
   553  		t.Skip("#66913: windows network connections are flakey on builders")
   554  	}
   555  	skipFIPS(t)
   556  
   557  	// In order to make Go test caching work as expected, we stat the
   558  	// bogo_config.json file, so that the Go testing hooks know that it is
   559  	// important for this test and will invalidate a cached test result if the
   560  	// file changes.
   561  	if _, err := os.Stat("bogo_config.json"); err != nil {
   562  		t.Fatal(err)
   563  	}
   564  
   565  	var bogoDir string
   566  	if *bogoLocalDir != "" {
   567  		ensureLocalBogo(t, *bogoLocalDir)
   568  		bogoDir = *bogoLocalDir
   569  	} else {
   570  		bogoDir = cryptotest.FetchModule(t, "boringssl.googlesource.com/boringssl.git", boringsslModVer)
   571  	}
   572  
   573  	cwd, err := os.Getwd()
   574  	if err != nil {
   575  		t.Fatal(err)
   576  	}
   577  
   578  	resultsFile := filepath.Join(t.TempDir(), "results.json")
   579  
   580  	args := []string{
   581  		"test",
   582  		".",
   583  		fmt.Sprintf("-shim-config=%s", filepath.Join(cwd, "bogo_config.json")),
   584  		fmt.Sprintf("-shim-path=%s", testenv.Executable(t)),
   585  		"-shim-extra-flags=-bogo-mode",
   586  		"-allow-unimplemented",
   587  		"-loose-errors", // TODO(roland): this should be removed eventually
   588  		fmt.Sprintf("-json-output=%s", resultsFile),
   589  	}
   590  	if *bogoFilter != "" {
   591  		args = append(args, fmt.Sprintf("-test=%s", *bogoFilter))
   592  	}
   593  
   594  	cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   595  	cmd.Dir = filepath.Join(bogoDir, "ssl/test/runner")
   596  	out, err := cmd.CombinedOutput()
   597  	// NOTE: we don't immediately check the error, because the failure could be either because
   598  	// the runner failed for some unexpected reason, or because a test case failed, and we
   599  	// cannot easily differentiate these cases. We check if the JSON results file was written,
   600  	// which should only happen if the failure was because of a test failure, and use that
   601  	// to determine the failure mode.
   602  
   603  	resultsJSON, jsonErr := os.ReadFile(resultsFile)
   604  	if jsonErr != nil {
   605  		if err != nil {
   606  			t.Fatalf("bogo failed: %s\n%s", err, out)
   607  		}
   608  		t.Fatalf("failed to read results JSON file: %s", jsonErr)
   609  	}
   610  
   611  	var results bogoResults
   612  	if err := json.Unmarshal(resultsJSON, &results); err != nil {
   613  		t.Fatalf("failed to parse results JSON: %s", err)
   614  	}
   615  
   616  	if *bogoReport != "" {
   617  		if err := generateReport(results, *bogoReport); err != nil {
   618  			t.Fatalf("failed to generate report: %v", err)
   619  		}
   620  	}
   621  
   622  	// assertResults contains test results we want to make sure
   623  	// are present in the output. They are only checked if -bogo-filter
   624  	// was not passed.
   625  	assertResults := map[string]string{
   626  		"CurveTest-Client-X25519MLKEM768-TLS13": "PASS",
   627  		"CurveTest-Server-X25519MLKEM768-TLS13": "PASS",
   628  
   629  		// Various signature algorithm tests checking that we enforce our
   630  		// preferences on the peer.
   631  		"ClientAuth-Enforced":                    "PASS",
   632  		"ServerAuth-Enforced":                    "PASS",
   633  		"ClientAuth-Enforced-TLS13":              "PASS",
   634  		"ServerAuth-Enforced-TLS13":              "PASS",
   635  		"VerifyPreferences-Advertised":           "PASS",
   636  		"VerifyPreferences-Enforced":             "PASS",
   637  		"Client-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
   638  		"Server-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
   639  		"Client-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
   640  		"Server-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
   641  	}
   642  
   643  	for name, result := range results.Tests {
   644  		// This is not really the intended way to do this... but... it works?
   645  		t.Run(name, func(t *testing.T) {
   646  			if result.Actual == "FAIL" && result.IsUnexpected {
   647  				t.Fail()
   648  			}
   649  			if result.Error != "" {
   650  				t.Log(result.Error)
   651  			}
   652  			if exp, ok := assertResults[name]; ok && exp != result.Actual {
   653  				t.Errorf("unexpected result: got %s, want %s", result.Actual, exp)
   654  			}
   655  			delete(assertResults, name)
   656  			if result.Actual == "SKIP" {
   657  				t.SkipNow()
   658  			}
   659  		})
   660  	}
   661  	if *bogoFilter == "" {
   662  		// Anything still in assertResults did not show up in the results, so we should fail
   663  		for name, expectedResult := range assertResults {
   664  			t.Run(name, func(t *testing.T) {
   665  				t.Fatalf("expected test to run with result %s, but it was not present in the test results", expectedResult)
   666  			})
   667  		}
   668  	}
   669  }
   670  
   671  // ensureLocalBogo fetches BoringSSL to localBogoDir at the correct revision
   672  // (from boringsslModVer) if localBogoDir doesn't already exist.
   673  //
   674  // If localBogoDir does exist, ensureLocalBogo fails the test if it isn't
   675  // a directory.
   676  func ensureLocalBogo(t *testing.T, localBogoDir string) {
   677  	t.Helper()
   678  
   679  	if stat, err := os.Stat(localBogoDir); err == nil {
   680  		if !stat.IsDir() {
   681  			t.Fatalf("local bogo dir (%q) exists but is not a directory", localBogoDir)
   682  		}
   683  
   684  		t.Logf("using local bogo checkout from %q", localBogoDir)
   685  		return
   686  	} else if !errors.Is(err, os.ErrNotExist) {
   687  		t.Fatalf("failed to stat local bogo dir (%q): %v", localBogoDir, err)
   688  	}
   689  
   690  	testenv.MustHaveExecPath(t, "git")
   691  
   692  	idx := strings.LastIndex(boringsslModVer, "-")
   693  	if idx == -1 || idx == len(boringsslModVer)-1 {
   694  		t.Fatalf("invalid boringsslModVer format: %q", boringsslModVer)
   695  	}
   696  	commitSHA := boringsslModVer[idx+1:]
   697  
   698  	t.Logf("cloning boringssl@%s to %q", commitSHA, localBogoDir)
   699  	cloneCmd := testenv.Command(t, "git", "clone", "--no-checkout", "https://boringssl.googlesource.com/boringssl", localBogoDir)
   700  	if err := cloneCmd.Run(); err != nil {
   701  		t.Fatalf("git clone failed: %v", err)
   702  	}
   703  
   704  	checkoutCmd := testenv.Command(t, "git", "checkout", commitSHA)
   705  	checkoutCmd.Dir = localBogoDir
   706  	if err := checkoutCmd.Run(); err != nil {
   707  		t.Fatalf("git checkout failed: %v", err)
   708  	}
   709  
   710  	t.Logf("using fresh local bogo checkout from %q", localBogoDir)
   711  }
   712  
   713  func generateReport(results bogoResults, outPath string) error {
   714  	data := reportData{
   715  		Results:   results,
   716  		Timestamp: time.Unix(int64(results.SecondsSinceEpoch), 0).Format("2006-01-02 15:04:05"),
   717  		Revision:  boringsslModVer,
   718  	}
   719  
   720  	tmpl := template.Must(template.New("report").Parse(reportTemplate))
   721  	file, err := os.Create(outPath)
   722  	if err != nil {
   723  		return err
   724  	}
   725  	defer file.Close()
   726  
   727  	return tmpl.Execute(file, data)
   728  }
   729  
   730  // bogoResults is a copy of boringssl.googlesource.com/boringssl/testresults.Results
   731  type bogoResults struct {
   732  	Version           int            `json:"version"`
   733  	Interrupted       bool           `json:"interrupted"`
   734  	PathDelimiter     string         `json:"path_delimiter"`
   735  	SecondsSinceEpoch float64        `json:"seconds_since_epoch"`
   736  	NumFailuresByType map[string]int `json:"num_failures_by_type"`
   737  	Tests             map[string]struct {
   738  		Actual       string `json:"actual"`
   739  		Expected     string `json:"expected"`
   740  		IsUnexpected bool   `json:"is_unexpected"`
   741  		Error        string `json:"error,omitempty"`
   742  	} `json:"tests"`
   743  }
   744  
   745  type reportData struct {
   746  	Results     bogoResults
   747  	SkipReasons map[string]string
   748  	Timestamp   string
   749  	Revision    string
   750  }
   751  
   752  const reportTemplate = `
   753  <!DOCTYPE html>
   754  <html>
   755  <head>
   756      <title>BoGo Results Report</title>
   757      <style>
   758          body { font-family: monospace; margin: 20px; }
   759          .summary { background: #f5f5f5; padding: 10px; margin-bottom: 20px; }
   760          .controls { margin-bottom: 10px; }
   761          .controls input, select { margin-right: 10px; }
   762          table { width: 100%; border-collapse: collapse; table-layout: fixed; }
   763          th, td { border: 1px solid #ddd; padding: 8px; text-align: left; vertical-align: top; }
   764          th { background-color: #f2f2f2; cursor: pointer; }
   765          .name-col { width: 30%; }
   766          .status-col { width: 8%; }
   767          .actual-col { width: 8%; }
   768          .expected-col { width: 8%; }
   769          .error-col { width: 26%; }
   770          .PASS { background-color: #d4edda; }
   771          .FAIL { background-color: #f8d7da; }
   772          .SKIP { background-color: #fff3cd; }
   773          .error {
   774              font-family: monospace;
   775              font-size: 0.9em;
   776              color: #721c24;
   777              white-space: pre-wrap;
   778              word-break: break-word;
   779          }
   780      </style>
   781  </head>
   782  <body>
   783  <h1>BoGo Results Report</h1>
   784  
   785  <div class="summary">
   786      <strong>Generated:</strong> {{.Timestamp}} | <strong>BoGo Revision:</strong> {{.Revision}}<br>
   787      {{range $status, $count := .Results.NumFailuresByType}}
   788      <strong>{{$status}}:</strong> {{$count}} |
   789      {{end}}
   790  </div>
   791  
   792  <div class="controls">
   793      <input type="text" id="search" placeholder="Search tests..." onkeyup="filterTests()">
   794      <select id="statusFilter" onchange="filterTests()">
   795          <option value="">All</option>
   796          <option value="FAIL">Failed</option>
   797          <option value="PASS">Passed</option>
   798          <option value="SKIP">Skipped</option>
   799      </select>
   800  </div>
   801  
   802  <table id="resultsTable">
   803      <thead>
   804      <tr>
   805          <th class="name-col" onclick="sortBy('name')">Test Name</th>
   806          <th class="status-col" onclick="sortBy('status')">Status</th>
   807          <th class="actual-col" onclick="sortBy('actual')">Actual</th>
   808          <th class="expected-col" onclick="sortBy('expected')">Expected</th>
   809          <th class="error-col">Error</th>
   810      </tr>
   811      </thead>
   812      <tbody>
   813      {{range $name, $test := .Results.Tests}}
   814      <tr class="{{$test.Actual}}" data-name="{{$name}}" data-status="{{$test.Actual}}">
   815          <td>{{$name}}</td>
   816          <td>{{$test.Actual}}</td>
   817          <td>{{$test.Actual}}</td>
   818          <td>{{$test.Expected}}</td>
   819          <td class="error">{{$test.Error}}</td>
   820      </tr>
   821      {{end}}
   822      </tbody>
   823  </table>
   824  
   825  <script>
   826      function filterTests() {
   827          const search = document.getElementById('search').value.toLowerCase();
   828          const status = document.getElementById('statusFilter').value;
   829          const rows = document.querySelectorAll('#resultsTable tbody tr');
   830  
   831          rows.forEach(row => {
   832              const name = row.dataset.name.toLowerCase();
   833              const rowStatus = row.dataset.status;
   834              const matchesSearch = name.includes(search);
   835              const matchesStatus = !status || rowStatus === status;
   836  
   837              row.style.display = matchesSearch && matchesStatus ? '' : 'none';
   838          });
   839      }
   840  
   841      function sortBy(column) {
   842          const tbody = document.querySelector('#resultsTable tbody');
   843          const rows = Array.from(tbody.querySelectorAll('tr'));
   844  
   845          rows.sort((a, b) => {
   846              if (column === 'status') {
   847                  const statusOrder = {'FAIL': 0, 'PASS': 1, 'SKIP': 2};
   848                  const aStatus = a.dataset.status;
   849                  const bStatus = b.dataset.status;
   850                  if (aStatus !== bStatus) {
   851                      return statusOrder[aStatus] - statusOrder[bStatus];
   852                  }
   853                  return a.dataset.name.localeCompare(b.dataset.name);
   854              } else {
   855                  return a.dataset.name.localeCompare(b.dataset.name);
   856              }
   857          });
   858  
   859          rows.forEach(row => tbody.appendChild(row));
   860          filterTests();
   861      }
   862  
   863      sortBy("status");
   864  </script>
   865  </body>
   866  </html>
   867  `
   868  

View as plain text