Help us shape the future of Go! Take the 2026 Go Developer Survey

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

View as plain text