blob: 5dc3b7c13ea79123982313dcd2039f126a445ef7 [file] [log] [blame]
package tls
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"internal/byteorder"
"internal/testenv"
"io"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
var (
port = flag.String("port", "", "")
server = flag.Bool("server", false, "")
isHandshakerSupported = flag.Bool("is-handshaker-supported", false, "")
keyfile = flag.String("key-file", "", "")
certfile = flag.String("cert-file", "", "")
trustCert = flag.String("trust-cert", "", "")
minVersion = flag.Int("min-version", VersionSSL30, "")
maxVersion = flag.Int("max-version", VersionTLS13, "")
noTLS13 = flag.Bool("no-tls13", false, "")
requireAnyClientCertificate = flag.Bool("require-any-client-certificate", false, "")
shimWritesFirst = flag.Bool("shim-writes-first", false, "")
resumeCount = flag.Int("resume-count", 0, "")
curves = flagStringSlice("curves", "")
expectedCurve = flag.String("expect-curve-id", "", "")
shimID = flag.Uint64("shim-id", 0, "")
_ = flag.Bool("ipv6", false, "")
echConfigListB64 = flag.String("ech-config-list", "", "")
expectECHAccepted = flag.Bool("expect-ech-accept", false, "")
expectHRR = flag.Bool("expect-hrr", false, "")
expectedECHRetryConfigs = flag.String("expect-ech-retry-configs", "", "")
expectNoECHRetryConfigs = flag.Bool("expect-no-ech-retry-configs", false, "")
onInitialExpectECHAccepted = flag.Bool("on-initial-expect-ech-accept", false, "")
_ = flag.Bool("expect-no-ech-name-override", false, "")
_ = flag.String("expect-ech-name-override", "", "")
_ = flag.Bool("reverify-on-resume", false, "")
onResumeECHConfigListB64 = flag.String("on-resume-ech-config-list", "", "")
_ = flag.Bool("on-resume-expect-reject-early-data", false, "")
onResumeExpectECHAccepted = flag.Bool("on-resume-expect-ech-accept", false, "")
_ = flag.Bool("on-resume-expect-no-ech-name-override", false, "")
expectedServerName = flag.String("expect-server-name", "", "")
expectSessionMiss = flag.Bool("expect-session-miss", false, "")
_ = flag.Bool("enable-early-data", false, "")
_ = flag.Bool("on-resume-expect-accept-early-data", false, "")
_ = flag.Bool("expect-ticket-supports-early-data", false, "")
onResumeShimWritesFirst = flag.Bool("on-resume-shim-writes-first", false, "")
advertiseALPN = flag.String("advertise-alpn", "", "")
expectALPN = flag.String("expect-alpn", "", "")
hostName = flag.String("host-name", "", "")
verifyPeer = flag.Bool("verify-peer", false, "")
_ = flag.Bool("use-custom-verify-callback", false, "")
)
type stringSlice []string
func flagStringSlice(name, usage string) *stringSlice {
f := &stringSlice{}
flag.Var(f, name, usage)
return f
}
func (saf stringSlice) String() string {
return strings.Join(saf, ",")
}
func (saf stringSlice) Set(s string) error {
saf = append(saf, s)
return nil
}
func bogoShim() {
if *isHandshakerSupported {
fmt.Println("No")
return
}
cfg := &Config{
ServerName: "test",
MinVersion: uint16(*minVersion),
MaxVersion: uint16(*maxVersion),
ClientSessionCache: NewLRUClientSessionCache(0),
}
if *noTLS13 && cfg.MaxVersion == VersionTLS13 {
cfg.MaxVersion = VersionTLS12
}
if *advertiseALPN != "" {
alpns := *advertiseALPN
for len(alpns) > 0 {
alpnLen := int(alpns[0])
cfg.NextProtos = append(cfg.NextProtos, alpns[1:1+alpnLen])
alpns = alpns[alpnLen+1:]
}
}
if *hostName != "" {
cfg.ServerName = *hostName
}
if *keyfile != "" || *certfile != "" {
pair, err := LoadX509KeyPair(*certfile, *keyfile)
if err != nil {
log.Fatalf("load key-file err: %s", err)
}
cfg.Certificates = []Certificate{pair}
}
if *trustCert != "" {
pool := x509.NewCertPool()
certFile, err := os.ReadFile(*trustCert)
if err != nil {
log.Fatalf("load trust-cert err: %s", err)
}
block, _ := pem.Decode(certFile)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Fatalf("parse trust-cert err: %s", err)
}
pool.AddCert(cert)
cfg.RootCAs = pool
}
if *requireAnyClientCertificate {
cfg.ClientAuth = RequireAnyClientCert
}
if *verifyPeer {
cfg.ClientAuth = VerifyClientCertIfGiven
}
if *echConfigListB64 != "" {
echConfigList, err := base64.StdEncoding.DecodeString(*echConfigListB64)
if err != nil {
log.Fatalf("parse ech-config-list err: %s", err)
}
cfg.EncryptedClientHelloConfigList = echConfigList
cfg.MinVersion = VersionTLS13
}
if len(*curves) != 0 {
for _, curveStr := range *curves {
id, err := strconv.Atoi(curveStr)
if err != nil {
log.Fatalf("failed to parse curve id %q: %s", curveStr, err)
}
cfg.CurvePreferences = append(cfg.CurvePreferences, CurveID(id))
}
}
for i := 0; i < *resumeCount+1; i++ {
if i > 0 && (*onResumeECHConfigListB64 != "") {
echConfigList, err := base64.StdEncoding.DecodeString(*onResumeECHConfigListB64)
if err != nil {
log.Fatalf("parse ech-config-list err: %s", err)
}
cfg.EncryptedClientHelloConfigList = echConfigList
}
conn, err := net.Dial("tcp", net.JoinHostPort("localhost", *port))
if err != nil {
log.Fatalf("dial err: %s", err)
}
defer conn.Close()
// Write the shim ID we were passed as a little endian uint64
shimIDBytes := make([]byte, 8)
byteorder.LePutUint64(shimIDBytes, *shimID)
if _, err := conn.Write(shimIDBytes); err != nil {
log.Fatalf("failed to write shim id: %s", err)
}
var tlsConn *Conn
if *server {
tlsConn = Server(conn, cfg)
} else {
tlsConn = Client(conn, cfg)
}
if i == 0 && *shimWritesFirst {
if _, err := tlsConn.Write([]byte("hello")); err != nil {
log.Fatalf("write err: %s", err)
}
}
for {
buf := make([]byte, 500)
var n int
n, err = tlsConn.Read(buf)
if err != nil {
break
}
buf = buf[:n]
for i := range buf {
buf[i] ^= 0xff
}
if _, err = tlsConn.Write(buf); err != nil {
break
}
}
if err != nil && err != io.EOF {
retryErr, ok := err.(*ECHRejectionError)
if !ok {
log.Fatalf("unexpected error type returned: %v", err)
}
if *expectNoECHRetryConfigs && len(retryErr.RetryConfigList) > 0 {
log.Fatalf("expected no ECH retry configs, got some")
}
if *expectedECHRetryConfigs != "" {
expectedRetryConfigs, err := base64.StdEncoding.DecodeString(*expectedECHRetryConfigs)
if err != nil {
log.Fatalf("failed to decode expected retry configs: %s", err)
}
if !bytes.Equal(retryErr.RetryConfigList, expectedRetryConfigs) {
log.Fatalf("unexpected retry list returned: got %x, want %x", retryErr.RetryConfigList, expectedRetryConfigs)
}
}
log.Fatalf("conn error: %s", err)
}
cs := tlsConn.ConnectionState()
if cs.HandshakeComplete {
if *expectALPN != "" && cs.NegotiatedProtocol != *expectALPN {
log.Fatalf("unexpected protocol negotiated: want %q, got %q", *expectALPN, cs.NegotiatedProtocol)
}
if *expectECHAccepted && !cs.ECHAccepted {
log.Fatal("expected ECH to be accepted, but connection state shows it was not")
} else if i == 0 && *onInitialExpectECHAccepted && !cs.ECHAccepted {
log.Fatal("expected ECH to be accepted, but connection state shows it was not")
} else if i > 0 && *onResumeExpectECHAccepted && !cs.ECHAccepted {
log.Fatal("expected ECH to be accepted on resumption, but connection state shows it was not")
} else if i == 0 && !*expectECHAccepted && cs.ECHAccepted {
log.Fatal("did not expect ECH, but it was accepted")
}
if *expectHRR && !cs.testingOnlyDidHRR {
log.Fatal("expected HRR but did not do it")
}
if *expectSessionMiss && cs.DidResume {
log.Fatal("unexpected session resumption")
}
if *expectedServerName != "" && cs.ServerName != *expectedServerName {
log.Fatalf("unexpected server name: got %q, want %q", cs.ServerName, *expectedServerName)
}
}
if *expectedCurve != "" {
expectedCurveID, err := strconv.Atoi(*expectedCurve)
if err != nil {
log.Fatalf("failed to parse -expect-curve-id: %s", err)
}
if tlsConn.curveID != CurveID(expectedCurveID) {
log.Fatalf("unexpected curve id: want %d, got %d", expectedCurveID, tlsConn.curveID)
}
}
}
}
func TestBogoSuite(t *testing.T) {
testenv.SkipIfShortAndSlow(t)
testenv.MustHaveExternalNetwork(t)
testenv.MustHaveGoRun(t)
testenv.MustHaveExec(t)
if testing.Short() {
t.Skip("skipping in short mode")
}
if testenv.Builder() != "" && runtime.GOOS == "windows" {
t.Skip("#66913: windows network connections are flakey on builders")
}
// In order to make Go test caching work as expected, we stat the
// bogo_config.json file, so that the Go testing hooks know that it is
// important for this test and will invalidate a cached test result if the
// file changes.
if _, err := os.Stat("bogo_config.json"); err != nil {
t.Fatal(err)
}
var bogoDir string
if *bogoLocalDir != "" {
bogoDir = *bogoLocalDir
} else {
const boringsslModVer = "v0.0.0-20240523173554-273a920f84e8"
output, err := exec.Command("go", "mod", "download", "-json", "boringssl.googlesource.com/boringssl.git@"+boringsslModVer).CombinedOutput()
if err != nil {
t.Fatalf("failed to download boringssl: %s", err)
}
var j struct {
Dir string
}
if err := json.Unmarshal(output, &j); err != nil {
t.Fatalf("failed to parse 'go mod download' output: %s", err)
}
bogoDir = j.Dir
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
resultsFile := filepath.Join(t.TempDir(), "results.json")
args := []string{
"test",
".",
fmt.Sprintf("-shim-config=%s", filepath.Join(cwd, "bogo_config.json")),
fmt.Sprintf("-shim-path=%s", os.Args[0]),
"-shim-extra-flags=-bogo-mode",
"-allow-unimplemented",
"-loose-errors", // TODO(roland): this should be removed eventually
fmt.Sprintf("-json-output=%s", resultsFile),
}
if *bogoFilter != "" {
args = append(args, fmt.Sprintf("-test=%s", *bogoFilter))
}
goCmd, err := testenv.GoTool()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(goCmd, args...)
out := &strings.Builder{}
cmd.Stderr = out
cmd.Dir = filepath.Join(bogoDir, "ssl/test/runner")
err = cmd.Run()
// NOTE: we don't immediately check the error, because the failure could be either because
// the runner failed for some unexpected reason, or because a test case failed, and we
// cannot easily differentiate these cases. We check if the JSON results file was written,
// which should only happen if the failure was because of a test failure, and use that
// to determine the failure mode.
resultsJSON, jsonErr := os.ReadFile(resultsFile)
if jsonErr != nil {
if err != nil {
t.Fatalf("bogo failed: %s\n%s", err, out)
}
t.Fatalf("failed to read results JSON file: %s", jsonErr)
}
var results bogoResults
if err := json.Unmarshal(resultsJSON, &results); err != nil {
t.Fatalf("failed to parse results JSON: %s", err)
}
// assertResults contains test results we want to make sure
// are present in the output. They are only checked if -bogo-filter
// was not passed.
assertResults := map[string]string{
"CurveTest-Client-Kyber-TLS13": "PASS",
"CurveTest-Server-Kyber-TLS13": "PASS",
}
for name, result := range results.Tests {
// This is not really the intended way to do this... but... it works?
t.Run(name, func(t *testing.T) {
if result.Actual == "FAIL" && result.IsUnexpected {
t.Fatal(result.Error)
}
if expectedResult, ok := assertResults[name]; ok && expectedResult != result.Actual {
t.Fatalf("unexpected result: got %s, want %s", result.Actual, assertResults[name])
}
delete(assertResults, name)
if result.Actual == "SKIP" {
t.Skip()
}
})
}
if *bogoFilter == "" {
// Anything still in assertResults did not show up in the results, so we should fail
for name, expectedResult := range assertResults {
t.Run(name, func(t *testing.T) {
t.Fatalf("expected test to run with result %s, but it was not present in the test results", expectedResult)
})
}
}
}
// bogoResults is a copy of boringssl.googlesource.com/boringssl/testresults.Results
type bogoResults struct {
Version int `json:"version"`
Interrupted bool `json:"interrupted"`
PathDelimiter string `json:"path_delimiter"`
SecondsSinceEpoch float64 `json:"seconds_since_epoch"`
NumFailuresByType map[string]int `json:"num_failures_by_type"`
Tests map[string]struct {
Actual string `json:"actual"`
Expected string `json:"expected"`
IsUnexpected bool `json:"is_unexpected"`
Error string `json:"error,omitempty"`
} `json:"tests"`
}