Based on Nekogram. Key additions: - Rebrand to FoxiGram (app name, APK name, applicationId com.foxigram.app) - Embedded Xray (VLESS+Reality) proxy client via JNI libxray.so - Bundled hidden one-tap proxies (LTE + WiFi), read-only in UI - Auto-restore proxy on restart, rebind to active network (LTE/WiFi) - Server credentials externalized to git-ignored XrayServers.java (+ template) - libxray Go source included; compiled .so, keystore, google-services.json ignored
2345 lines
70 KiB
Go
2345 lines
70 KiB
Go
// Copyright 2016 The BoringSSL Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package runner
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
_ "embed"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"boringssl.googlesource.com/boringssl.git/util/testresult"
|
|
"golang.org/x/crypto/cryptobyte"
|
|
)
|
|
|
|
var (
|
|
useValgrind = flag.Bool("valgrind", false, "If true, run code under valgrind")
|
|
useGDB = flag.Bool("gdb", false, "If true, run BoringSSL code under gdb")
|
|
useLLDB = flag.Bool("lldb", false, "If true, run BoringSSL code under lldb")
|
|
useRR = flag.Bool("rr-record", false, "If true, run BoringSSL code under `rr record`.")
|
|
waitForDebugger = flag.Bool("wait-for-debugger", false, "If true, jobs will run one at a time and pause for a debugger to attach")
|
|
flagDebug = flag.Bool("debug", false, "Hexdump the contents of the connection")
|
|
mallocTest = flag.Int64("malloc-test", -1, "If non-negative, run each test with each malloc in turn failing from the given number onwards.")
|
|
mallocTestDebug = flag.Bool("malloc-test-debug", false, "If true, ask bssl_shim to abort rather than fail a malloc. This can be used with a specific value for --malloc-test to identity the malloc failing that is causing problems.")
|
|
jsonOutput = flag.String("json-output", "", "The file to output JSON results to.")
|
|
pipe = flag.Bool("pipe", false, "If true, print status output suitable for piping into another program.")
|
|
testToRun = flag.String("test", "", "Semicolon-separated patterns of tests to run, or empty to run all tests")
|
|
skipTest = flag.String("skip", "", "Semicolon-separated patterns of tests to skip")
|
|
allowHintMismatch = flag.String("allow-hint-mismatch", "", "Semicolon-separated patterns of tests where hints may mismatch")
|
|
numWorkersFlag = flag.Int("num-workers", runtime.NumCPU(), "The number of workers to run in parallel.")
|
|
shimPath = flag.String("shim-path", "../../../build/ssl/test/bssl_shim", "The location of the shim binary.")
|
|
shimExtraFlags = flag.String("shim-extra-flags", "", "Semicolon-separated extra flags to pass to the shim binary on each invocation.")
|
|
handshakerPath = flag.String("handshaker-path", "../../../build/ssl/test/handshaker", "The location of the handshaker binary.")
|
|
fuzzer = flag.Bool("fuzzer", false, "If true, tests against a BoringSSL built in fuzzer mode.")
|
|
transcriptDir = flag.String("transcript-dir", "", "The directory in which to write transcripts.")
|
|
idleTimeout = flag.Duration("idle-timeout", 15*time.Second, "The number of seconds to wait for a read or write to bssl_shim.")
|
|
deterministic = flag.Bool("deterministic", false, "If true, uses a deterministic PRNG in the runner.")
|
|
allowUnimplemented = flag.Bool("allow-unimplemented", false, "If true, report pass even if some tests are unimplemented.")
|
|
looseErrors = flag.Bool("loose-errors", false, "If true, allow shims to report an untranslated error code.")
|
|
shimConfigFile = flag.String("shim-config", "", "A config file to use to configure the tests for this shim.")
|
|
includeDisabled = flag.Bool("include-disabled", false, "If true, also runs disabled tests.")
|
|
repeatUntilFailure = flag.Bool("repeat-until-failure", false, "If true, the first selected test will be run repeatedly until failure.")
|
|
)
|
|
|
|
// ShimConfigurations is used with the “json” package and represents a shim
|
|
// config file.
|
|
type ShimConfiguration struct {
|
|
// DisabledTests maps from a glob-based pattern to a freeform string.
|
|
// The glob pattern is used to exclude tests from being run and the
|
|
// freeform string is unparsed but expected to explain why the test is
|
|
// disabled.
|
|
DisabledTests map[string]string
|
|
|
|
// ErrorMap maps from expected error strings to the correct error
|
|
// string for the shim in question. For example, it might map
|
|
// “:NO_SHARED_CIPHER:” (a BoringSSL error string) to something
|
|
// like “SSL_ERROR_NO_CYPHER_OVERLAP”.
|
|
ErrorMap map[string][]string
|
|
|
|
// HalfRTTTickets is the number of half-RTT tickets the client should
|
|
// expect before half-RTT data when testing 0-RTT.
|
|
HalfRTTTickets int
|
|
|
|
// AllCurves is the list of all curve code points supported by the shim.
|
|
// This is currently used to control tests that enable all curves but may
|
|
// automatically disable tests in the future.
|
|
AllCurves []int
|
|
|
|
// MaxACKBuffer is the maximum number of received records the shim is
|
|
// expected to retain when ACKing.
|
|
MaxACKBuffer int
|
|
}
|
|
|
|
// Setup shimConfig defaults aligning with BoringSSL.
|
|
var shimConfig ShimConfiguration = ShimConfiguration{
|
|
HalfRTTTickets: 2,
|
|
MaxACKBuffer: 32,
|
|
}
|
|
|
|
//go:embed rsa_2048_key.pem
|
|
var rsa2048KeyPEM []byte
|
|
|
|
//go:embed rsa_1024_key.pem
|
|
var rsa1024KeyPEM []byte
|
|
|
|
//go:embed ecdsa_p224_key.pem
|
|
var ecdsaP224KeyPEM []byte
|
|
|
|
//go:embed ecdsa_p256_key.pem
|
|
var ecdsaP256KeyPEM []byte
|
|
|
|
//go:embed ecdsa_p384_key.pem
|
|
var ecdsaP384KeyPEM []byte
|
|
|
|
//go:embed ecdsa_p521_key.pem
|
|
var ecdsaP521KeyPEM []byte
|
|
|
|
//go:embed ed25519_key.pem
|
|
var ed25519KeyPEM []byte
|
|
|
|
//go:embed channel_id_key.pem
|
|
var channelIDKeyPEM []byte
|
|
|
|
var (
|
|
rsa1024Key rsa.PrivateKey
|
|
rsa2048Key rsa.PrivateKey
|
|
|
|
ecdsaP224Key ecdsa.PrivateKey
|
|
ecdsaP256Key ecdsa.PrivateKey
|
|
ecdsaP384Key ecdsa.PrivateKey
|
|
ecdsaP521Key ecdsa.PrivateKey
|
|
|
|
ed25519Key ed25519.PrivateKey
|
|
|
|
channelIDKey ecdsa.PrivateKey
|
|
)
|
|
|
|
var channelIDKeyPath string
|
|
|
|
func initKeys() {
|
|
// Since key generation is not particularly cheap (especially RSA), and the
|
|
// runner is intended to run on systems which may be resouece constrained,
|
|
// we load keys from disk instead of dynamically generating them. We treat
|
|
// key files the same as dynamically generated certificates, writing them
|
|
// out to temporary files before passing them to the shim.
|
|
|
|
for _, k := range []struct {
|
|
pemBytes []byte
|
|
key *rsa.PrivateKey
|
|
}{
|
|
{rsa1024KeyPEM, &rsa1024Key},
|
|
{rsa2048KeyPEM, &rsa2048Key},
|
|
} {
|
|
key, err := loadPEMKey(k.pemBytes)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to load RSA test key: %s", err))
|
|
}
|
|
*k.key = *(key.(*rsa.PrivateKey))
|
|
}
|
|
|
|
for _, k := range []struct {
|
|
pemBytes []byte
|
|
key *ecdsa.PrivateKey
|
|
}{
|
|
{ecdsaP224KeyPEM, &ecdsaP224Key},
|
|
{ecdsaP256KeyPEM, &ecdsaP256Key},
|
|
{ecdsaP384KeyPEM, &ecdsaP384Key},
|
|
{ecdsaP521KeyPEM, &ecdsaP521Key},
|
|
{channelIDKeyPEM, &channelIDKey},
|
|
} {
|
|
key, err := loadPEMKey(k.pemBytes)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to load ECDSA test key: %s", err))
|
|
}
|
|
*k.key = *(key.(*ecdsa.PrivateKey))
|
|
}
|
|
|
|
k, err := loadPEMKey(ed25519KeyPEM)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to load Ed25519 test key: %s", err))
|
|
}
|
|
ed25519Key = k.(ed25519.PrivateKey)
|
|
|
|
channelIDKeyPath = writeTempKeyFile(&channelIDKey)
|
|
}
|
|
|
|
var channelIDBytes []byte
|
|
|
|
var (
|
|
testOCSPResponse = []byte{1, 2, 3, 4}
|
|
testOCSPResponse2 = []byte{5, 6, 7, 8}
|
|
testSCTList = []byte{0, 6, 0, 4, 5, 6, 7, 8}
|
|
testSCTList2 = []byte{0, 6, 0, 4, 1, 2, 3, 4}
|
|
)
|
|
|
|
var (
|
|
testOCSPExtension = append([]byte{byte(extensionStatusRequest) >> 8, byte(extensionStatusRequest), 0, 8, statusTypeOCSP, 0, 0, 4}, testOCSPResponse...)
|
|
testSCTExtension = append([]byte{byte(extensionSignedCertificateTimestamp) >> 8, byte(extensionSignedCertificateTimestamp), 0, byte(len(testSCTList))}, testSCTList...)
|
|
)
|
|
|
|
var (
|
|
rsaCertificate Credential
|
|
rsaChainCertificate Credential
|
|
rsa1024Certificate Credential
|
|
ecdsaP224Certificate Credential
|
|
ecdsaP256Certificate Credential
|
|
ecdsaP384Certificate Credential
|
|
ecdsaP521Certificate Credential
|
|
ed25519Certificate Credential
|
|
garbageCertificate Credential
|
|
)
|
|
|
|
func initCertificates() {
|
|
for _, def := range []struct {
|
|
name string
|
|
key crypto.Signer
|
|
out *Credential
|
|
}{
|
|
{"Test RSA-1024 Cert", &rsa1024Key, &rsa1024Certificate},
|
|
{"Test RSA-2048 Cert", &rsa2048Key, &rsaCertificate},
|
|
{"Test ECDSA P-224 Cert", &ecdsaP224Key, &ecdsaP224Certificate},
|
|
{"Test ECDSA P-256 Cert", &ecdsaP256Key, &ecdsaP256Certificate},
|
|
{"Test ECDSA P-384 Cert", &ecdsaP384Key, &ecdsaP384Certificate},
|
|
{"Test ECDSA P-521 Cert", &ecdsaP521Key, &ecdsaP521Certificate},
|
|
{"Test Ed25519 Cert", ed25519Key, &ed25519Certificate},
|
|
} {
|
|
template := *baseCertTemplate
|
|
template.Subject.CommonName = def.name
|
|
*def.out = generateSingleCertChain(&template, def.key)
|
|
}
|
|
|
|
channelIDBytes = make([]byte, 64)
|
|
writeIntPadded(channelIDBytes[:32], channelIDKey.X)
|
|
writeIntPadded(channelIDBytes[32:], channelIDKey.Y)
|
|
|
|
garbageCertificate.Certificate = [][]byte{[]byte("GARBAGE")}
|
|
garbageCertificate.PrivateKey = rsaCertificate.PrivateKey
|
|
|
|
// Build a basic three cert chain for testing chain specific things.
|
|
rootTmpl := *baseCertTemplate
|
|
rootTmpl.Subject.CommonName = "test root"
|
|
rootCert := generateTestCert(&rootTmpl, nil, &rsa2048Key)
|
|
intermediateTmpl := *baseCertTemplate
|
|
intermediateTmpl.Subject.CommonName = "test inter"
|
|
intermediateCert := generateTestCert(&intermediateTmpl, rootCert, &rsa2048Key)
|
|
leafTmpl := *baseCertTemplate
|
|
leafTmpl.IsCA, leafTmpl.BasicConstraintsValid = false, false
|
|
leafCert := generateTestCert(nil, intermediateCert, &rsa2048Key)
|
|
|
|
keyPath := writeTempKeyFile(&rsa2048Key)
|
|
rootCertPath, chainPath := writeTempCertFile([]*x509.Certificate{rootCert}), writeTempCertFile([]*x509.Certificate{leafCert, intermediateCert})
|
|
|
|
rsaChainCertificate = Credential{
|
|
Certificate: [][]byte{leafCert.Raw, intermediateCert.Raw},
|
|
RootCertificate: rootCert.Raw,
|
|
PrivateKey: &rsa2048Key,
|
|
Leaf: leafCert,
|
|
ChainPath: chainPath,
|
|
KeyPath: keyPath,
|
|
RootPath: rootCertPath,
|
|
}
|
|
}
|
|
|
|
func flagInts(flagName string, vals []int) []string {
|
|
ret := make([]string, 0, 2*len(vals))
|
|
for _, val := range vals {
|
|
ret = append(ret, flagName, strconv.Itoa(val))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func base64FlagValue(in []byte) string {
|
|
return base64.StdEncoding.EncodeToString(in)
|
|
}
|
|
|
|
func useDebugger() bool {
|
|
return *useGDB || *useLLDB || *useRR || *waitForDebugger
|
|
}
|
|
|
|
func loadPEMKey(pemBytes []byte) (crypto.PrivateKey, error) {
|
|
block, _ := pem.Decode(pemBytes)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM block found")
|
|
}
|
|
|
|
if block.Type != "PRIVATE KEY" {
|
|
return nil, fmt.Errorf("unexpected PEM type (expected \"PRIVATE KEY\"): %s", block.Type)
|
|
}
|
|
|
|
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse PKCS#8 key: %s", err)
|
|
}
|
|
|
|
return k, nil
|
|
}
|
|
|
|
// recordVersionToWire maps a record-layer protocol version to its wire
|
|
// representation.
|
|
func recordVersionToWire(vers uint16, protocol protocol) uint16 {
|
|
if protocol == dtls {
|
|
switch vers {
|
|
case VersionTLS12:
|
|
return VersionDTLS12
|
|
case VersionTLS10:
|
|
return VersionDTLS10
|
|
}
|
|
} else {
|
|
switch vers {
|
|
case VersionSSL30, VersionTLS10, VersionTLS11, VersionTLS12:
|
|
return vers
|
|
}
|
|
}
|
|
|
|
panic("unknown version")
|
|
}
|
|
|
|
// encodeDERValues encodes a series of bytestrings in comma-separated-hex form.
|
|
func encodeDERValues(values [][]byte) string {
|
|
var ret string
|
|
for i, v := range values {
|
|
if i > 0 {
|
|
ret += ","
|
|
}
|
|
ret += hex.EncodeToString(v)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func decodeHexOrPanic(in string) []byte {
|
|
ret, err := hex.DecodeString(in)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type testType int
|
|
|
|
const (
|
|
clientTest testType = iota
|
|
serverTest
|
|
)
|
|
|
|
func (t testType) String() string {
|
|
switch t {
|
|
case clientTest:
|
|
return "Client"
|
|
case serverTest:
|
|
return "Server"
|
|
}
|
|
panic(fmt.Sprintf("bad test type %d", t))
|
|
}
|
|
|
|
type protocol int
|
|
|
|
const (
|
|
tls protocol = iota
|
|
dtls
|
|
quic
|
|
)
|
|
|
|
func (p protocol) String() string {
|
|
switch p {
|
|
case tls:
|
|
return "TLS"
|
|
case dtls:
|
|
return "DTLS"
|
|
case quic:
|
|
return "QUIC"
|
|
}
|
|
return "unknown protocol"
|
|
}
|
|
|
|
const (
|
|
alpn = 1
|
|
npn = 2
|
|
)
|
|
|
|
// connectionExpectations contains connection-level test expectations to check
|
|
// on the runner side.
|
|
type connectionExpectations struct {
|
|
// version, if non-zero, specifies the TLS version that must be negotiated.
|
|
version uint16
|
|
// cipher, if non-zero, specifies the TLS cipher suite that should be
|
|
// negotiated.
|
|
cipher uint16
|
|
// channelID controls whether the connection should have negotiated a
|
|
// Channel ID with channelIDKey.
|
|
channelID bool
|
|
// nextProto controls whether the connection should negotiate a next
|
|
// protocol via NPN or ALPN.
|
|
nextProto string
|
|
// noNextProto, if true, means that no next protocol should be negotiated.
|
|
noNextProto bool
|
|
// nextProtoType, if non-zero, is the next protocol negotiation mechanism.
|
|
nextProtoType int
|
|
// srtpProtectionProfile is the DTLS-SRTP profile that should be negotiated.
|
|
// If zero, none should be negotiated.
|
|
srtpProtectionProfile uint16
|
|
// peerSignatureAlgorithm, if not zero, is the signature algorithm that the
|
|
// peer should have used in the handshake.
|
|
peerSignatureAlgorithm signatureAlgorithm
|
|
// curveID, if not zero, is the curve that the handshake should have used.
|
|
curveID CurveID
|
|
// peerCertificate, if not nil, is the credential the peer is expected to
|
|
// send.
|
|
peerCertificate *Credential
|
|
// quicTransportParams contains the QUIC transport parameters that are to be
|
|
// sent by the peer using codepoint 57.
|
|
quicTransportParams []byte
|
|
// quicTransportParamsLegacy contains the QUIC transport parameters that are
|
|
// to be sent by the peer using legacy codepoint 0xffa5.
|
|
quicTransportParamsLegacy []byte
|
|
// peerApplicationSettings are the expected application settings for the
|
|
// connection. If nil, no application settings are expected.
|
|
peerApplicationSettings []byte
|
|
// peerApplicationSettingsOld are the expected application settings for
|
|
// the connection that are to be sent by the peer using old codepoint.
|
|
// If nil, no application settings are expected.
|
|
peerApplicationSettingsOld []byte
|
|
// echAccepted is whether ECH should have been accepted on this connection.
|
|
echAccepted bool
|
|
}
|
|
|
|
type testCase struct {
|
|
testType testType
|
|
protocol protocol
|
|
name string
|
|
config Config
|
|
shouldFail bool
|
|
expectedError string
|
|
// expectedLocalError, if not empty, contains a substring that must be
|
|
// found in the local error.
|
|
expectedLocalError string
|
|
// expectations contains test expectations for the initial
|
|
// connection.
|
|
expectations connectionExpectations
|
|
// resumeExpectations, if non-nil, contains test expectations for the
|
|
// resumption connection. If nil, |expectations| is used.
|
|
resumeExpectations *connectionExpectations
|
|
// messageLen is the length, in bytes, of the test message that will be
|
|
// sent.
|
|
messageLen int
|
|
// messageCount is the number of test messages that will be sent.
|
|
messageCount int
|
|
// resumeSession controls whether a second connection should be tested
|
|
// which attempts to resume the first session.
|
|
resumeSession bool
|
|
// resumeRenewedSession controls whether a third connection should be
|
|
// tested which attempts to resume the second connection's session.
|
|
resumeRenewedSession bool
|
|
// expectResumeRejected, if true, specifies that the attempted
|
|
// resumption must be rejected by the client. This is only valid for a
|
|
// serverTest.
|
|
expectResumeRejected bool
|
|
// resumeConfig, if not nil, points to a Config to be used on
|
|
// resumption. Unless newSessionsOnResume is set,
|
|
// SessionTicketKey, ServerSessionCache, and
|
|
// ClientSessionCache are copied from the initial connection's
|
|
// config. If nil, the initial connection's config is used.
|
|
resumeConfig *Config
|
|
// newSessionsOnResume, if true, will cause resumeConfig to
|
|
// use a different session resumption context.
|
|
newSessionsOnResume bool
|
|
// noSessionCache, if true, will cause the server to run without a
|
|
// session cache.
|
|
noSessionCache bool
|
|
// sendPrefix sends a prefix on the socket before actually performing a
|
|
// handshake.
|
|
sendPrefix string
|
|
// shimWritesFirst controls whether the shim sends an initial "hello"
|
|
// message before doing a roundtrip with the runner.
|
|
shimWritesFirst bool
|
|
// readWithUnfinishedWrite behaves like shimWritesFirst, but the shim
|
|
// does not complete the write until responding to the first runner
|
|
// message.
|
|
readWithUnfinishedWrite bool
|
|
// shimShutsDown, if true, runs a test where the shim shuts down the
|
|
// connection immediately after the handshake rather than echoing
|
|
// messages from the runner. The runner will default to not sending
|
|
// application data.
|
|
shimShutsDown bool
|
|
// renegotiate indicates the number of times the connection should be
|
|
// renegotiated during the exchange.
|
|
renegotiate int
|
|
// sendHalfHelloRequest, if true, causes the server to send half a
|
|
// HelloRequest when the handshake completes.
|
|
sendHalfHelloRequest bool
|
|
// renegotiateCiphers is a list of ciphersuite ids that will be
|
|
// switched in just before renegotiation.
|
|
renegotiateCiphers []uint16
|
|
// replayWrites, if true, configures the underlying transport
|
|
// to replay every write it makes in DTLS tests.
|
|
replayWrites bool
|
|
// damageFirstWrite, if true, configures the underlying transport to
|
|
// damage the final byte of the first application data write.
|
|
damageFirstWrite bool
|
|
// exportKeyingMaterial, if non-zero, configures the test to exchange
|
|
// keying material and verify they match.
|
|
exportKeyingMaterial int
|
|
exportLabel string
|
|
exportContext string
|
|
useExportContext bool
|
|
// flags, if not empty, contains a list of command-line flags that will
|
|
// be passed to the shim program.
|
|
flags []string
|
|
// testTLSUnique, if true, causes the shim to send the tls-unique value
|
|
// which will be compared against the expected value.
|
|
testTLSUnique bool
|
|
// sendEmptyRecords is the number of consecutive empty records to send
|
|
// before each test message.
|
|
sendEmptyRecords int
|
|
// sendWarningAlerts is the number of consecutive warning alerts to send
|
|
// before each test message.
|
|
sendWarningAlerts int
|
|
// sendUserCanceledAlerts is the number of consecutive user_canceled alerts to
|
|
// send before each test message.
|
|
sendUserCanceledAlerts int
|
|
// sendBogusAlertType, if true, causes a bogus alert of invalid type to
|
|
// be sent before each test message.
|
|
sendBogusAlertType bool
|
|
// sendKeyUpdates is the number of consecutive key updates to send
|
|
// before and after the test message.
|
|
sendKeyUpdates int
|
|
// keyUpdateRequest is the KeyUpdateRequest value to send in KeyUpdate messages.
|
|
keyUpdateRequest byte
|
|
// shimSendsKeyUpdateBeforeRead indicates the shim should send a KeyUpdate
|
|
// message before each read.
|
|
shimSendsKeyUpdateBeforeRead bool
|
|
// expectUnsolicitedKeyUpdate makes the test expect a one or more KeyUpdate
|
|
// messages while reading data from the shim. Don't use this in combination
|
|
// with any of the fields that send a KeyUpdate otherwise any received
|
|
// KeyUpdate might not be as unsolicited as expected.
|
|
expectUnsolicitedKeyUpdate bool
|
|
// expectMessageDropped, if true, means the test message is expected to
|
|
// be dropped by the client rather than echoed back.
|
|
expectMessageDropped bool
|
|
// shimPrefix is the prefix that the shim will send to the server.
|
|
shimPrefix string
|
|
// resumeShimPrefix is the prefix that the shim will send to the server on a
|
|
// resumption.
|
|
resumeShimPrefix string
|
|
// exportTrafficSecrets, if true, configures the test to export the TLS 1.3
|
|
// traffic secrets and confirms that they match.
|
|
exportTrafficSecrets bool
|
|
// skipTransportParamsConfig, if true, will skip automatic configuration of
|
|
// sending QUIC transport parameters when protocol == quic.
|
|
skipTransportParamsConfig bool
|
|
// skipQUICALPNConfig, if true, will skip automatic configuration of
|
|
// sending a fake ALPN when protocol == quic.
|
|
skipQUICALPNConfig bool
|
|
// earlyData, if true, configures default settings for an early data test.
|
|
// expectEarlyDataRejected controls whether the test is for early data
|
|
// accept or reject. In a client test, the shim will be configured to send
|
|
// an initial write in early data which, on accept, the runner will enforce.
|
|
// In a server test, the runner will send some default message in early
|
|
// data, which the shim is expected to echo in half-RTT.
|
|
earlyData bool
|
|
// expectEarlyDataRejected, if earlyData is true, is whether early data is
|
|
// expected to be rejected. In a client test, this controls whether the shim
|
|
// should retry for early rejection. In a server test, this is whether the
|
|
// test expects the shim to reject early data.
|
|
expectEarlyDataRejected bool
|
|
// skipSplitHandshake, if true, will skip the generation of a split
|
|
// handshake copy of the test.
|
|
skipSplitHandshake bool
|
|
// skipHints, if true, will skip the generation of a handshake hints copy of
|
|
// the test.
|
|
skipHints bool
|
|
// skipVersionNameCheck, if true, will skip the consistency check between
|
|
// test name and the versions.
|
|
skipVersionNameCheck bool
|
|
// shimCertificate, if not nil, is the default credential which should be
|
|
// configured at the shim. If set, it must be an X.509 credential.
|
|
shimCertificate *Credential
|
|
// handshakerCertificate, if not nil, overrides the default credential which
|
|
// on the handshaker.
|
|
handshakerCertificate *Credential
|
|
// shimCredentials is a list of credentials which should be configured at
|
|
// the shim. It differs from shimCertificate only in whether the old or
|
|
// new APIs are used.
|
|
shimCredentials []*Credential
|
|
resumeShimCredentials []*Credential
|
|
}
|
|
|
|
var testCases []testCase
|
|
|
|
func appendTranscript(path string, data []byte) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
|
|
settings, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
// If the shim aborted before writing a file, use a default
|
|
// settings block, so the transcript is still somewhat valid.
|
|
settings = []byte{0, 0} // kDataTag
|
|
}
|
|
|
|
settings = append(settings, data...)
|
|
return os.WriteFile(path, settings, 0644)
|
|
}
|
|
|
|
// A timeoutConn implements an idle timeout on each Read and Write operation.
|
|
type timeoutConn struct {
|
|
net.Conn
|
|
timeout time.Duration
|
|
}
|
|
|
|
func (t *timeoutConn) Read(b []byte) (int, error) {
|
|
if !*useGDB {
|
|
if err := t.SetReadDeadline(time.Now().Add(t.timeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return t.Conn.Read(b)
|
|
}
|
|
|
|
func (t *timeoutConn) Write(b []byte) (int, error) {
|
|
if !*useGDB {
|
|
if err := t.SetWriteDeadline(time.Now().Add(t.timeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return t.Conn.Write(b)
|
|
}
|
|
|
|
func makeTestMessage(msgIdx int, messageLen int) []byte {
|
|
testMessage := make([]byte, messageLen)
|
|
for i := range testMessage {
|
|
testMessage[i] = 0x42 ^ byte(msgIdx)
|
|
}
|
|
return testMessage
|
|
}
|
|
|
|
func expectedReply(b []byte) []byte {
|
|
ret := make([]byte, len(b))
|
|
for i, v := range b {
|
|
ret[i] = v ^ 0xff
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func doExchange(test *testCase, config *Config, conn net.Conn, isResume bool, transcripts *[][]byte, num int) error {
|
|
if !test.noSessionCache {
|
|
if config.ClientSessionCache == nil {
|
|
config.ClientSessionCache = NewLRUClientSessionCache(1)
|
|
}
|
|
if config.ServerSessionCache == nil {
|
|
config.ServerSessionCache = NewLRUServerSessionCache(1)
|
|
}
|
|
}
|
|
if test.testType != clientTest {
|
|
// Supply a ServerName to ensure a constant session cache key,
|
|
// rather than falling back to net.Conn.RemoteAddr.
|
|
if len(config.ServerName) == 0 {
|
|
config.ServerName = "test"
|
|
}
|
|
}
|
|
|
|
if *fuzzer {
|
|
config.Bugs.NullAllCiphers = true
|
|
}
|
|
if *deterministic {
|
|
config.Time = func() time.Time { return time.Unix(1234, 1234) }
|
|
}
|
|
|
|
if !useDebugger() {
|
|
conn = &timeoutConn{conn, *idleTimeout}
|
|
}
|
|
|
|
if test.protocol == dtls {
|
|
config.Bugs.PacketAdaptor = newPacketAdaptor(conn)
|
|
conn = config.Bugs.PacketAdaptor
|
|
}
|
|
|
|
if *flagDebug || len(*transcriptDir) != 0 {
|
|
local, peer := "client", "server"
|
|
if test.testType == clientTest {
|
|
local, peer = peer, local
|
|
}
|
|
connDebug := &recordingConn{
|
|
Conn: conn,
|
|
isDatagram: test.protocol == dtls,
|
|
local: local,
|
|
peer: peer,
|
|
}
|
|
conn = connDebug
|
|
if *flagDebug {
|
|
defer connDebug.WriteTo(os.Stdout)
|
|
}
|
|
if len(*transcriptDir) != 0 {
|
|
defer func() {
|
|
if num == len(*transcripts) {
|
|
*transcripts = append(*transcripts, connDebug.Transcript())
|
|
} else {
|
|
panic("transcripts are out of sync")
|
|
}
|
|
}()
|
|
|
|
// Record ClientHellos for the decode_client_hello_inner fuzzer.
|
|
var clientHelloCount int
|
|
config.Bugs.RecordClientHelloInner = func(encodedInner, outer []byte) error {
|
|
name := fmt.Sprintf("%s-%d-%d", test.name, num, clientHelloCount)
|
|
clientHelloCount++
|
|
dir := filepath.Join(*transcriptDir, "decode_client_hello_inner")
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
bb := cryptobyte.NewBuilder(nil)
|
|
addUint24LengthPrefixedBytes(bb, encodedInner)
|
|
bb.AddBytes(outer)
|
|
return os.WriteFile(filepath.Join(dir, name), bb.BytesOrPanic(), 0644)
|
|
}
|
|
}
|
|
|
|
if config.Bugs.PacketAdaptor != nil {
|
|
config.Bugs.PacketAdaptor.debug = connDebug
|
|
}
|
|
}
|
|
if test.protocol == quic {
|
|
config.Bugs.MockQUICTransport = newMockQUICTransport(conn)
|
|
// The MockQUICTransport will panic if Read or Write is
|
|
// called. When a MockQUICTransport is set, separate
|
|
// methods should be used to actually read and write
|
|
// records. By setting the conn to it here, it ensures
|
|
// Read or Write aren't accidentally used instead of the
|
|
// methods provided by MockQUICTransport.
|
|
conn = config.Bugs.MockQUICTransport
|
|
}
|
|
|
|
if test.replayWrites {
|
|
conn = newReplayAdaptor(conn)
|
|
}
|
|
|
|
var connDamage *damageAdaptor
|
|
if test.damageFirstWrite {
|
|
connDamage = newDamageAdaptor(conn)
|
|
conn = connDamage
|
|
}
|
|
|
|
if test.sendPrefix != "" {
|
|
if _, err := conn.Write([]byte(test.sendPrefix)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var tlsConn *Conn
|
|
if test.testType == clientTest {
|
|
if test.protocol == dtls {
|
|
tlsConn = DTLSServer(conn, config)
|
|
} else {
|
|
tlsConn = Server(conn, config)
|
|
}
|
|
} else {
|
|
config.InsecureSkipVerify = true
|
|
if test.protocol == dtls {
|
|
tlsConn = DTLSClient(conn, config)
|
|
} else {
|
|
tlsConn = Client(conn, config)
|
|
}
|
|
}
|
|
defer tlsConn.Close()
|
|
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
return err
|
|
}
|
|
|
|
expectations := &test.expectations
|
|
if isResume && test.resumeExpectations != nil {
|
|
expectations = test.resumeExpectations
|
|
}
|
|
connState := tlsConn.ConnectionState()
|
|
if vers := connState.Version; expectations.version != 0 && vers != expectations.version {
|
|
return fmt.Errorf("got version %x, expected %x", vers, expectations.version)
|
|
}
|
|
|
|
if cipher := connState.CipherSuite; expectations.cipher != 0 && cipher != expectations.cipher {
|
|
return fmt.Errorf("got cipher %x, expected %x", cipher, expectations.cipher)
|
|
}
|
|
if didResume := connState.DidResume; isResume && didResume == test.expectResumeRejected {
|
|
return fmt.Errorf("didResume is %t, but we expected the opposite", didResume)
|
|
}
|
|
|
|
if expectations.channelID {
|
|
channelID := connState.ChannelID
|
|
if channelID == nil {
|
|
return fmt.Errorf("no channel ID negotiated")
|
|
}
|
|
if channelID.Curve != channelIDKey.Curve ||
|
|
channelIDKey.X.Cmp(channelIDKey.X) != 0 ||
|
|
channelIDKey.Y.Cmp(channelIDKey.Y) != 0 {
|
|
return fmt.Errorf("incorrect channel ID")
|
|
}
|
|
} else if connState.ChannelID != nil {
|
|
return fmt.Errorf("channel ID unexpectedly negotiated")
|
|
}
|
|
|
|
if expected := expectations.nextProto; expected != "" {
|
|
if actual := connState.NegotiatedProtocol; actual != expected {
|
|
return fmt.Errorf("next proto mismatch: got %s, wanted %s", actual, expected)
|
|
}
|
|
}
|
|
|
|
if expectations.noNextProto {
|
|
if actual := connState.NegotiatedProtocol; actual != "" {
|
|
return fmt.Errorf("got unexpected next proto %s", actual)
|
|
}
|
|
}
|
|
|
|
if expectations.nextProtoType != 0 {
|
|
if (expectations.nextProtoType == alpn) != connState.NegotiatedProtocolFromALPN {
|
|
return fmt.Errorf("next proto type mismatch")
|
|
}
|
|
}
|
|
|
|
if expectations.peerApplicationSettings != nil {
|
|
if !connState.HasApplicationSettings {
|
|
return errors.New("application settings should have been negotiated")
|
|
}
|
|
if !bytes.Equal(connState.PeerApplicationSettings, expectations.peerApplicationSettings) {
|
|
return fmt.Errorf("peer application settings mismatch: got %q, wanted %q", connState.PeerApplicationSettings, expectations.peerApplicationSettings)
|
|
}
|
|
} else if connState.HasApplicationSettings {
|
|
return errors.New("application settings unexpectedly negotiated")
|
|
}
|
|
|
|
if expectations.peerApplicationSettingsOld != nil {
|
|
if !connState.HasApplicationSettingsOld {
|
|
return errors.New("old application settings should have been negotiated")
|
|
}
|
|
if !bytes.Equal(connState.PeerApplicationSettingsOld, expectations.peerApplicationSettingsOld) {
|
|
return fmt.Errorf("old peer application settings mismatch: got %q, wanted %q", connState.PeerApplicationSettingsOld, expectations.peerApplicationSettingsOld)
|
|
}
|
|
} else if connState.HasApplicationSettingsOld {
|
|
return errors.New("old application settings unexpectedly negotiated")
|
|
}
|
|
|
|
if p := connState.SRTPProtectionProfile; p != expectations.srtpProtectionProfile {
|
|
return fmt.Errorf("SRTP profile mismatch: got %d, wanted %d", p, expectations.srtpProtectionProfile)
|
|
}
|
|
|
|
if expected := expectations.peerSignatureAlgorithm; expected != 0 && expected != connState.PeerSignatureAlgorithm {
|
|
return fmt.Errorf("expected peer to use signature algorithm %04x, but got %04x", expected, connState.PeerSignatureAlgorithm)
|
|
}
|
|
|
|
if expected := expectations.curveID; expected != 0 && expected != connState.CurveID {
|
|
return fmt.Errorf("expected peer to use curve %04x, but got %04x", expected, connState.CurveID)
|
|
}
|
|
|
|
if expected := expectations.peerCertificate; expected != nil {
|
|
if len(connState.PeerCertificates) != len(expected.Certificate) {
|
|
return fmt.Errorf("expected peer to send %d certificates, but got %d", len(connState.PeerCertificates), len(expected.Certificate))
|
|
}
|
|
for i, cert := range connState.PeerCertificates {
|
|
if !bytes.Equal(cert.Raw, expected.Certificate[i]) {
|
|
return fmt.Errorf("peer certificate %d did not match", i+1)
|
|
}
|
|
}
|
|
|
|
if !bytes.Equal(connState.OCSPResponse, expected.OCSPStaple) {
|
|
return fmt.Errorf("peer OCSP response did not match")
|
|
}
|
|
|
|
if !bytes.Equal(connState.SCTList, expected.SignedCertificateTimestampList) {
|
|
return fmt.Errorf("peer SCT list did not match")
|
|
}
|
|
|
|
if expected.Type == CredentialTypeDelegated {
|
|
if connState.PeerDelegatedCredential == nil {
|
|
return fmt.Errorf("peer unexpectedly did not use delegated credentials")
|
|
}
|
|
if !bytes.Equal(expected.DelegatedCredential, connState.PeerDelegatedCredential) {
|
|
return fmt.Errorf("peer delegated credential did not match")
|
|
}
|
|
} else if connState.PeerDelegatedCredential != nil {
|
|
return fmt.Errorf("peer unexpectedly used delegated credentials")
|
|
}
|
|
}
|
|
|
|
if len(expectations.quicTransportParams) > 0 {
|
|
if !bytes.Equal(expectations.quicTransportParams, connState.QUICTransportParams) {
|
|
return errors.New("Peer did not send expected QUIC transport params")
|
|
}
|
|
}
|
|
|
|
if len(expectations.quicTransportParamsLegacy) > 0 {
|
|
if !bytes.Equal(expectations.quicTransportParamsLegacy, connState.QUICTransportParamsLegacy) {
|
|
return errors.New("Peer did not send expected legacy QUIC transport params")
|
|
}
|
|
}
|
|
|
|
if expectations.echAccepted {
|
|
if !connState.ECHAccepted {
|
|
return errors.New("tls: server did not accept ECH")
|
|
}
|
|
} else {
|
|
if connState.ECHAccepted {
|
|
return errors.New("tls: server unexpectedly accepted ECH")
|
|
}
|
|
}
|
|
|
|
if test.exportKeyingMaterial > 0 {
|
|
actual := make([]byte, test.exportKeyingMaterial)
|
|
if _, err := io.ReadFull(tlsConn, actual); err != nil {
|
|
return err
|
|
}
|
|
expected, err := tlsConn.ExportKeyingMaterial(test.exportKeyingMaterial, []byte(test.exportLabel), []byte(test.exportContext), test.useExportContext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bytes.Equal(actual, expected) {
|
|
return fmt.Errorf("keying material mismatch; got %x, wanted %x", actual, expected)
|
|
}
|
|
}
|
|
|
|
if test.exportTrafficSecrets {
|
|
secretLenBytes := make([]byte, 2)
|
|
if _, err := io.ReadFull(tlsConn, secretLenBytes); err != nil {
|
|
return err
|
|
}
|
|
secretLen := binary.LittleEndian.Uint16(secretLenBytes)
|
|
|
|
theirReadSecret := make([]byte, secretLen)
|
|
theirWriteSecret := make([]byte, secretLen)
|
|
if _, err := io.ReadFull(tlsConn, theirReadSecret); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.ReadFull(tlsConn, theirWriteSecret); err != nil {
|
|
return err
|
|
}
|
|
|
|
myReadSecret := tlsConn.in.trafficSecret
|
|
myWriteSecret := tlsConn.out.trafficSecret
|
|
if !bytes.Equal(myWriteSecret, theirReadSecret) {
|
|
return fmt.Errorf("read traffic-secret mismatch; got %x, wanted %x", theirReadSecret, myWriteSecret)
|
|
}
|
|
if !bytes.Equal(myReadSecret, theirWriteSecret) {
|
|
return fmt.Errorf("write traffic-secret mismatch; got %x, wanted %x", theirWriteSecret, myReadSecret)
|
|
}
|
|
}
|
|
|
|
if test.testTLSUnique {
|
|
var peersValue [12]byte
|
|
if _, err := io.ReadFull(tlsConn, peersValue[:]); err != nil {
|
|
return err
|
|
}
|
|
expected := tlsConn.ConnectionState().TLSUnique
|
|
if !bytes.Equal(peersValue[:], expected) {
|
|
return fmt.Errorf("tls-unique mismatch: peer sent %x, but %x was expected", peersValue[:], expected)
|
|
}
|
|
}
|
|
|
|
if test.sendHalfHelloRequest {
|
|
tlsConn.SendHalfHelloRequest()
|
|
}
|
|
|
|
shimPrefix := test.shimPrefix
|
|
if isResume {
|
|
shimPrefix = test.resumeShimPrefix
|
|
}
|
|
if test.shimWritesFirst || test.readWithUnfinishedWrite {
|
|
shimPrefix = shimInitialWrite
|
|
}
|
|
if test.renegotiate > 0 {
|
|
// If readWithUnfinishedWrite is set, the shim prefix will be
|
|
// available later.
|
|
if shimPrefix != "" && !test.readWithUnfinishedWrite {
|
|
buf := make([]byte, len(shimPrefix))
|
|
_, err := io.ReadFull(tlsConn, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if string(buf) != shimPrefix {
|
|
return fmt.Errorf("bad initial message %v vs %v", string(buf), shimPrefix)
|
|
}
|
|
shimPrefix = ""
|
|
}
|
|
|
|
if test.renegotiateCiphers != nil {
|
|
config.CipherSuites = test.renegotiateCiphers
|
|
}
|
|
for i := 0; i < test.renegotiate; i++ {
|
|
if err := tlsConn.Renegotiate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if test.renegotiateCiphers != nil {
|
|
panic("renegotiateCiphers without renegotiate")
|
|
}
|
|
|
|
if test.damageFirstWrite {
|
|
connDamage.setDamage(true)
|
|
if _, err := tlsConn.Write([]byte("DAMAGED WRITE")); err != nil {
|
|
return err
|
|
}
|
|
connDamage.setDamage(false)
|
|
}
|
|
|
|
messageLen := test.messageLen
|
|
if messageLen < 0 {
|
|
if test.protocol == dtls {
|
|
return fmt.Errorf("messageLen < 0 not supported for DTLS tests")
|
|
}
|
|
// Read until EOF.
|
|
_, err := io.Copy(io.Discard, tlsConn)
|
|
return err
|
|
}
|
|
if messageLen == 0 {
|
|
messageLen = 32
|
|
}
|
|
|
|
messageCount := test.messageCount
|
|
// shimShutsDown sets the default message count to zero.
|
|
if messageCount == 0 && !test.shimShutsDown {
|
|
messageCount = 1
|
|
}
|
|
|
|
for j := 0; j < messageCount; j++ {
|
|
for i := 0; i < test.sendKeyUpdates; i++ {
|
|
if err := tlsConn.SendKeyUpdate(test.keyUpdateRequest); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for i := 0; i < test.sendEmptyRecords; i++ {
|
|
if _, err := tlsConn.Write(nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for i := 0; i < test.sendWarningAlerts; i++ {
|
|
if err := tlsConn.SendAlert(alertLevelWarning, alertUnexpectedMessage); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for i := 0; i < test.sendUserCanceledAlerts; i++ {
|
|
if err := tlsConn.SendAlert(alertLevelWarning, alertUserCanceled); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if test.sendBogusAlertType {
|
|
if err := tlsConn.SendAlert(0x42, alertUnexpectedMessage); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if test.shimSendsKeyUpdateBeforeRead {
|
|
if err := tlsConn.ReadKeyUpdate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
testMessage := makeTestMessage(j, messageLen)
|
|
if _, err := tlsConn.Write(testMessage); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Consume the shim prefix if needed.
|
|
if shimPrefix != "" {
|
|
buf := make([]byte, len(shimPrefix))
|
|
_, err := io.ReadFull(tlsConn, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if string(buf) != shimPrefix {
|
|
return fmt.Errorf("bad initial message %v vs %v", string(buf), shimPrefix)
|
|
}
|
|
shimPrefix = ""
|
|
}
|
|
|
|
if test.shimShutsDown || test.expectMessageDropped {
|
|
// The shim will not respond.
|
|
continue
|
|
}
|
|
|
|
// Process the KeyUpdate reply. However many KeyUpdates the runner
|
|
// sends, the shim should respond only once.
|
|
if test.sendKeyUpdates > 0 && test.keyUpdateRequest == keyUpdateRequested {
|
|
if err := tlsConn.ReadKeyUpdate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
buf := make([]byte, len(testMessage))
|
|
if test.protocol == dtls {
|
|
bufTmp := make([]byte, len(buf)+1)
|
|
n, err := tlsConn.Read(bufTmp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if config.Bugs.SplitAndPackAppData {
|
|
m, err := tlsConn.Read(bufTmp[n:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n += m
|
|
}
|
|
if n != len(buf) {
|
|
return fmt.Errorf("bad reply; length mismatch (%d vs %d)", n, len(buf))
|
|
}
|
|
copy(buf, bufTmp)
|
|
} else {
|
|
_, err := io.ReadFull(tlsConn, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if expected := expectedReply(testMessage); !bytes.Equal(buf, expected) {
|
|
return fmt.Errorf("bad reply contents; got %x and wanted %x", buf, expected)
|
|
}
|
|
|
|
if seen := tlsConn.keyUpdateSeen; seen != test.expectUnsolicitedKeyUpdate {
|
|
return fmt.Errorf("keyUpdateSeen (%t) != expectUnsolicitedKeyUpdate", seen)
|
|
}
|
|
}
|
|
|
|
// The shim will end attempting to read, sending one last KeyUpdate. Consume
|
|
// the KeyUpdate before closing the connection.
|
|
if test.shimSendsKeyUpdateBeforeRead {
|
|
if err := tlsConn.ReadKeyUpdate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const xtermSize = "140x50"
|
|
|
|
func valgrindOf(dbAttach bool, path string, args ...string) *exec.Cmd {
|
|
valgrindArgs := []string{"--error-exitcode=99", "--track-origins=yes", "--leak-check=full", "--quiet"}
|
|
if dbAttach {
|
|
valgrindArgs = append(valgrindArgs, "--db-attach=yes", "--db-command=xterm -geometry "+xtermSize+" -e gdb -nw %f %p")
|
|
}
|
|
valgrindArgs = append(valgrindArgs, path)
|
|
valgrindArgs = append(valgrindArgs, args...)
|
|
|
|
return exec.Command("valgrind", valgrindArgs...)
|
|
}
|
|
|
|
func gdbOf(path string, args ...string) *exec.Cmd {
|
|
xtermArgs := []string{"-geometry", xtermSize, "-e", "gdb", "--args"}
|
|
xtermArgs = append(xtermArgs, path)
|
|
xtermArgs = append(xtermArgs, args...)
|
|
|
|
return exec.Command("xterm", xtermArgs...)
|
|
}
|
|
|
|
func lldbOf(path string, args ...string) *exec.Cmd {
|
|
xtermArgs := []string{"-geometry", xtermSize, "-e", "lldb", "--"}
|
|
xtermArgs = append(xtermArgs, path)
|
|
xtermArgs = append(xtermArgs, args...)
|
|
|
|
return exec.Command("xterm", xtermArgs...)
|
|
}
|
|
|
|
func rrOf(path string, args ...string) *exec.Cmd {
|
|
rrArgs := []string{"record", path}
|
|
rrArgs = append(rrArgs, args...)
|
|
return exec.Command("rr", rrArgs...)
|
|
}
|
|
|
|
func removeFirstLineIfSuffix(s, suffix string) string {
|
|
idx := strings.IndexByte(s, '\n')
|
|
if idx < 0 {
|
|
return s
|
|
}
|
|
if strings.HasSuffix(s[:idx], suffix) {
|
|
return s[idx+1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
var (
|
|
errMoreMallocs = errors.New("child process did not exhaust all allocation calls")
|
|
errUnimplemented = errors.New("child process does not implement needed flags")
|
|
)
|
|
|
|
type shimProcess struct {
|
|
cmd *exec.Cmd
|
|
// done is closed when the process has exited. At that point, childErr may be
|
|
// read for the result.
|
|
done chan struct{}
|
|
childErr error
|
|
listener *shimListener
|
|
stdout, stderr bytes.Buffer
|
|
}
|
|
|
|
// newShimProcess starts a new shim with the specified executable, flags, and
|
|
// environment. It internally creates a TCP listener and adds the the -port
|
|
// flag.
|
|
func newShimProcess(dispatcher *shimDispatcher, shimPath string, flags []string, env []string) (*shimProcess, error) {
|
|
listener, err := dispatcher.NewShim()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shim := &shimProcess{listener: listener}
|
|
cmdFlags := []string{
|
|
"-port", strconv.Itoa(listener.Port()),
|
|
"-shim-id", strconv.FormatUint(listener.ShimID(), 10),
|
|
}
|
|
if listener.IsIPv6() {
|
|
cmdFlags = append(cmdFlags, "-ipv6")
|
|
}
|
|
cmdFlags = append(cmdFlags, flags...)
|
|
|
|
if *useValgrind {
|
|
shim.cmd = valgrindOf(false, shimPath, cmdFlags...)
|
|
} else if *useGDB {
|
|
shim.cmd = gdbOf(shimPath, cmdFlags...)
|
|
} else if *useLLDB {
|
|
shim.cmd = lldbOf(shimPath, cmdFlags...)
|
|
} else if *useRR {
|
|
shim.cmd = rrOf(shimPath, cmdFlags...)
|
|
} else {
|
|
shim.cmd = exec.Command(shimPath, cmdFlags...)
|
|
}
|
|
shim.cmd.Stdin = os.Stdin
|
|
shim.cmd.Stdout = &shim.stdout
|
|
shim.cmd.Stderr = &shim.stderr
|
|
shim.cmd.Env = env
|
|
|
|
if err := shim.cmd.Start(); err != nil {
|
|
shim.listener.Close()
|
|
return nil, err
|
|
}
|
|
|
|
shim.done = make(chan struct{})
|
|
go func() {
|
|
shim.childErr = shim.cmd.Wait()
|
|
shim.listener.Close()
|
|
close(shim.done)
|
|
}()
|
|
return shim, nil
|
|
}
|
|
|
|
// accept returns a new TCP connection with the shim process, or returns an
|
|
// error on timeout or shim exit.
|
|
func (s *shimProcess) accept() (net.Conn, error) {
|
|
var deadline time.Time
|
|
if !useDebugger() {
|
|
deadline = time.Now().Add(*idleTimeout)
|
|
}
|
|
return s.listener.Accept(deadline)
|
|
}
|
|
|
|
// wait finishes the test and waits for the shim process to exit.
|
|
func (s *shimProcess) wait() error {
|
|
// Close the listener now. This is to avoid hangs if the shim tries to open
|
|
// more connections than expected.
|
|
s.listener.Close()
|
|
|
|
if !useDebugger() {
|
|
waitTimeout := time.AfterFunc(*idleTimeout, func() {
|
|
s.cmd.Process.Kill()
|
|
})
|
|
defer waitTimeout.Stop()
|
|
}
|
|
|
|
<-s.done
|
|
return s.childErr
|
|
}
|
|
|
|
// close releases resources associated with the shimProcess. This is safe to
|
|
// call before or after |wait|.
|
|
func (s *shimProcess) close() {
|
|
s.listener.Close()
|
|
s.cmd.Process.Kill()
|
|
}
|
|
|
|
func doExchanges(test *testCase, shim *shimProcess, resumeCount int, transcripts *[][]byte) error {
|
|
config := test.config
|
|
if *deterministic {
|
|
config.Rand = &deterministicRand{}
|
|
}
|
|
|
|
conn, err := shim.accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = doExchange(test, &config, conn, false /* not a resumption */, transcripts, 0)
|
|
conn.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nextTicketKey := config.SessionTicketKey
|
|
for i := 0; i < resumeCount; i++ {
|
|
var resumeConfig Config
|
|
if test.resumeConfig != nil {
|
|
resumeConfig = *test.resumeConfig
|
|
resumeConfig.Rand = config.Rand
|
|
if resumeConfig.Credential == nil {
|
|
resumeConfig.Credential = config.Credential
|
|
}
|
|
} else {
|
|
resumeConfig = config
|
|
}
|
|
|
|
if test.newSessionsOnResume {
|
|
resumeConfig.ClientSessionCache = nil
|
|
resumeConfig.ServerSessionCache = nil
|
|
if _, err := resumeConfig.rand().Read(resumeConfig.SessionTicketKey[:]); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
resumeConfig.ClientSessionCache = config.ClientSessionCache
|
|
resumeConfig.ServerSessionCache = config.ServerSessionCache
|
|
// Rotate the ticket keys between each connection, with each connection
|
|
// encrypting with next connection's keys. This ensures that we test
|
|
// the renewed sessions.
|
|
resumeConfig.SessionTicketKey = nextTicketKey
|
|
if _, err := resumeConfig.rand().Read(nextTicketKey[:]); err != nil {
|
|
return err
|
|
}
|
|
resumeConfig.Bugs.EncryptSessionTicketKey = &nextTicketKey
|
|
}
|
|
|
|
var connResume net.Conn
|
|
connResume, err = shim.accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = doExchange(test, &resumeConfig, connResume, true /* resumption */, transcripts, i+1)
|
|
connResume.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// translateExpectedError uses a canonical BoringSSL error to produce
|
|
// a slice of expected canonical errors in bogo_shim_config.json.
|
|
func translateExpectedError(canonical string) []string {
|
|
if translated, found := shimConfig.ErrorMap[canonical]; found {
|
|
return translated
|
|
}
|
|
|
|
// not specifying a canonical error will have the same effect as -loose-errors being true
|
|
// since the emptry string with match all error substrings.
|
|
return []string{canonical}
|
|
}
|
|
|
|
// formatErrors takes the semantic mapping from translateExpectedError
|
|
// and outputs human-readable digest of BoGo error state.
|
|
func formatErrors(expectedErrors []string, stderr, local, child, stdout, expectedLocal, expectedCanonical string) (string, string) {
|
|
got := fmt.Sprintf("\tstderr:\n\t\t%s\n\tlocal: %q\n\tchild: %q\n\tstdout: %s", stderr, local, child, stdout)
|
|
want := fmt.Sprintf("\tlocal: %q\n\tremote: %q", expectedLocal, expectedCanonical)
|
|
if slices.Equal(expectedErrors, []string{expectedCanonical}) {
|
|
return got, want
|
|
}
|
|
if len(expectedErrors) == 0 || expectedErrors == nil {
|
|
return got, want + " (no specified mapping)"
|
|
}
|
|
return got, want + " mapped to one of:\n\t\t" + strings.Join(expectedErrors, "\n\t\t")
|
|
}
|
|
|
|
// matchError plucks the relevant canonical error from the provided
|
|
// slice if found; if the slice is empty/nil, strict error checking
|
|
// is presumed to be disabled.
|
|
func matchError(expectedErrors []string, stderr string) bool {
|
|
for _, expectedError := range expectedErrors {
|
|
if strings.Contains(stderr, expectedError) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shimInitialWrite is the data we expect from the shim when the
|
|
// -shim-writes-first flag is used.
|
|
const shimInitialWrite = "hello"
|
|
|
|
func appendCredentialFlags(flags []string, cred *Credential, prefix string, newCredential bool) []string {
|
|
if newCredential {
|
|
switch cred.Type {
|
|
case CredentialTypeX509:
|
|
flags = append(flags, prefix+"-new-x509-credential")
|
|
case CredentialTypeDelegated:
|
|
flags = append(flags, prefix+"-new-delegated-credential")
|
|
case CredentialTypeSPAKE2PlusV1:
|
|
flags = append(flags, prefix+"-new-spake2plusv1-credential")
|
|
default:
|
|
panic(fmt.Errorf("unknown credential type %d", cred.Type))
|
|
}
|
|
} else if cred.Type != CredentialTypeX509 {
|
|
panic("default credential must be X.509")
|
|
}
|
|
|
|
handleBase64Field := func(flag string, value []byte) {
|
|
if len(value) != 0 {
|
|
flags = append(flags, fmt.Sprintf("%s-%s", prefix, flag), base64FlagValue(value))
|
|
}
|
|
}
|
|
|
|
if len(cred.ChainPath) != 0 {
|
|
flags = append(flags, prefix+"-cert-file", cred.ChainPath)
|
|
}
|
|
if len(cred.KeyPath) != 0 {
|
|
flags = append(flags, prefix+"-key-file", cred.KeyPath)
|
|
}
|
|
handleBase64Field("ocsp-response", cred.OCSPStaple)
|
|
handleBase64Field("signed-cert-timestamps", cred.SignedCertificateTimestampList)
|
|
for _, sigAlg := range cred.SignatureAlgorithms {
|
|
flags = append(flags, prefix+"-signing-prefs", strconv.Itoa(int(sigAlg)))
|
|
}
|
|
handleBase64Field("delegated-credential", cred.DelegatedCredential)
|
|
if cred.MustMatchIssuer {
|
|
flags = append(flags, prefix+"-must-match-issuer")
|
|
}
|
|
handleBase64Field("pake-context", cred.PAKEContext)
|
|
handleBase64Field("pake-client-id", cred.PAKEClientID)
|
|
handleBase64Field("pake-server-id", cred.PAKEServerID)
|
|
handleBase64Field("pake-password", cred.PAKEPassword)
|
|
if cred.WrongPAKERole {
|
|
flags = append(flags, prefix+"-wrong-pake-role")
|
|
}
|
|
handleBase64Field("trust-anchor-id", cred.TrustAnchorID)
|
|
return flags
|
|
}
|
|
|
|
func runTest(dispatcher *shimDispatcher, statusChan chan statusMsg, test *testCase, shimPath string, mallocNumToFail int64) error {
|
|
// Help debugging panics on the Go side.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
fmt.Fprintf(os.Stderr, "Test '%s' panicked.\n", test.name)
|
|
panic(r)
|
|
}
|
|
}()
|
|
|
|
var flags []string
|
|
if len(*shimExtraFlags) > 0 {
|
|
flags = strings.Split(*shimExtraFlags, ";")
|
|
}
|
|
if *fuzzer {
|
|
flags = append(flags, "-fuzzer-mode")
|
|
}
|
|
if test.testType == serverTest {
|
|
flags = append(flags, "-server")
|
|
}
|
|
|
|
// Configure the default credential.
|
|
shimCertificate := test.shimCertificate
|
|
if shimCertificate == nil && len(test.shimCredentials) == 0 && len(test.resumeShimCredentials) == 0 && test.testType == serverTest && len(test.config.PreSharedKey) == 0 {
|
|
shimCertificate = &rsaCertificate
|
|
}
|
|
if shimCertificate != nil {
|
|
var shimPrefix string
|
|
if test.handshakerCertificate != nil {
|
|
shimPrefix = "-on-shim"
|
|
}
|
|
flags = appendCredentialFlags(flags, shimCertificate, shimPrefix, false)
|
|
}
|
|
if test.handshakerCertificate != nil {
|
|
flags = appendCredentialFlags(flags, test.handshakerCertificate, "-on-handshaker", false)
|
|
}
|
|
|
|
// Configure any additional credentials.
|
|
for _, cred := range test.shimCredentials {
|
|
flags = appendCredentialFlags(flags, cred, "", true)
|
|
}
|
|
for _, cred := range test.resumeShimCredentials {
|
|
flags = appendCredentialFlags(flags, cred, "-on-resume", true)
|
|
}
|
|
|
|
if test.protocol == dtls {
|
|
flags = append(flags, "-dtls")
|
|
} else if test.protocol == quic {
|
|
flags = append(flags, "-quic")
|
|
if !test.skipTransportParamsConfig {
|
|
test.config.QUICTransportParams = []byte{1, 2}
|
|
test.config.QUICTransportParamsUseLegacyCodepoint = QUICUseCodepointStandard
|
|
if test.resumeConfig != nil {
|
|
test.resumeConfig.QUICTransportParams = []byte{1, 2}
|
|
test.resumeConfig.QUICTransportParamsUseLegacyCodepoint = QUICUseCodepointStandard
|
|
}
|
|
test.expectations.quicTransportParams = []byte{3, 4}
|
|
if test.resumeExpectations != nil {
|
|
test.resumeExpectations.quicTransportParams = []byte{3, 4}
|
|
}
|
|
useCodepointFlag := "0"
|
|
if test.config.QUICTransportParamsUseLegacyCodepoint == QUICUseCodepointLegacy {
|
|
useCodepointFlag = "1"
|
|
}
|
|
flags = append(flags,
|
|
"-quic-transport-params",
|
|
base64FlagValue([]byte{3, 4}),
|
|
"-expect-quic-transport-params",
|
|
base64FlagValue([]byte{1, 2}),
|
|
"-quic-use-legacy-codepoint", useCodepointFlag)
|
|
}
|
|
if !test.skipQUICALPNConfig {
|
|
flags = append(flags,
|
|
[]string{
|
|
"-advertise-alpn", "\x03foo",
|
|
"-select-alpn", "foo",
|
|
"-expect-alpn", "foo",
|
|
}...)
|
|
test.config.NextProtos = []string{"foo"}
|
|
if test.resumeConfig != nil {
|
|
test.resumeConfig.NextProtos = []string{"foo"}
|
|
}
|
|
test.expectations.nextProto = "foo"
|
|
test.expectations.nextProtoType = alpn
|
|
if test.resumeExpectations != nil {
|
|
test.resumeExpectations.nextProto = "foo"
|
|
test.resumeExpectations.nextProtoType = alpn
|
|
}
|
|
}
|
|
}
|
|
|
|
if test.earlyData {
|
|
if !test.resumeSession {
|
|
panic("earlyData set without resumeSession in " + test.name)
|
|
}
|
|
|
|
resumeConfig := test.resumeConfig
|
|
if resumeConfig == nil {
|
|
resumeConfig = &test.config
|
|
}
|
|
if test.expectEarlyDataRejected {
|
|
flags = append(flags, "-on-resume-expect-reject-early-data")
|
|
} else {
|
|
flags = append(flags, "-on-resume-expect-accept-early-data")
|
|
}
|
|
|
|
if test.protocol == quic {
|
|
// QUIC requires an early data context string.
|
|
flags = append(flags, "-quic-early-data-context", "context")
|
|
}
|
|
|
|
flags = append(flags, "-enable-early-data")
|
|
if test.testType == clientTest {
|
|
// Configure the runner with default maximum early data.
|
|
flags = append(flags, "-expect-ticket-supports-early-data")
|
|
if test.config.MaxEarlyDataSize == 0 {
|
|
test.config.MaxEarlyDataSize = 16384
|
|
}
|
|
if resumeConfig.MaxEarlyDataSize == 0 {
|
|
resumeConfig.MaxEarlyDataSize = 16384
|
|
}
|
|
|
|
// In DTLS 1.3, we're setting flags to configure the client to attempt
|
|
// sending early data, but we expect it to realize that it's incapable
|
|
// of supporting early data and not send any.
|
|
if test.protocol != dtls {
|
|
// Configure the shim to send some data in early data.
|
|
flags = append(flags, "-on-resume-shim-writes-first")
|
|
if resumeConfig.Bugs.ExpectEarlyData == nil {
|
|
resumeConfig.Bugs.ExpectEarlyData = [][]byte{[]byte(shimInitialWrite)}
|
|
}
|
|
}
|
|
} else {
|
|
// By default, send some early data and expect half-RTT data response.
|
|
if resumeConfig.Bugs.SendEarlyData == nil {
|
|
resumeConfig.Bugs.SendEarlyData = [][]byte{{1, 2, 3, 4}}
|
|
}
|
|
if resumeConfig.Bugs.ExpectHalfRTTData == nil {
|
|
resumeConfig.Bugs.ExpectHalfRTTData = [][]byte{{254, 253, 252, 251}}
|
|
}
|
|
resumeConfig.Bugs.ExpectEarlyDataAccepted = !test.expectEarlyDataRejected
|
|
}
|
|
}
|
|
|
|
var resumeCount int
|
|
if test.resumeSession {
|
|
resumeCount++
|
|
if test.resumeRenewedSession {
|
|
resumeCount++
|
|
}
|
|
}
|
|
|
|
if resumeCount > 0 {
|
|
flags = append(flags, "-resume-count", strconv.Itoa(resumeCount))
|
|
}
|
|
|
|
if test.shimWritesFirst {
|
|
flags = append(flags, "-shim-writes-first")
|
|
}
|
|
|
|
if test.readWithUnfinishedWrite {
|
|
flags = append(flags, "-read-with-unfinished-write")
|
|
}
|
|
|
|
if test.shimShutsDown {
|
|
flags = append(flags, "-shim-shuts-down")
|
|
}
|
|
|
|
if test.exportKeyingMaterial > 0 {
|
|
flags = append(flags, "-export-keying-material", strconv.Itoa(test.exportKeyingMaterial))
|
|
if test.useExportContext {
|
|
flags = append(flags, "-use-export-context")
|
|
}
|
|
}
|
|
if test.exportKeyingMaterial > 0 {
|
|
flags = append(flags, "-export-label", test.exportLabel)
|
|
flags = append(flags, "-export-context", test.exportContext)
|
|
}
|
|
|
|
if test.exportTrafficSecrets {
|
|
flags = append(flags, "-export-traffic-secrets")
|
|
}
|
|
|
|
if test.expectResumeRejected {
|
|
flags = append(flags, "-expect-session-miss")
|
|
}
|
|
|
|
if test.testTLSUnique {
|
|
flags = append(flags, "-tls-unique")
|
|
}
|
|
|
|
if test.shimSendsKeyUpdateBeforeRead {
|
|
flags = append(flags, "-key-update-before-read")
|
|
}
|
|
|
|
if *waitForDebugger {
|
|
flags = append(flags, "-wait-for-debugger")
|
|
}
|
|
|
|
var transcriptPrefix string
|
|
var transcripts [][]byte
|
|
if len(*transcriptDir) != 0 {
|
|
protocol := "tls"
|
|
if test.protocol == dtls {
|
|
protocol = "dtls"
|
|
} else if test.protocol == quic {
|
|
protocol = "quic"
|
|
}
|
|
|
|
side := "client"
|
|
if test.testType == serverTest {
|
|
side = "server"
|
|
}
|
|
|
|
dir := filepath.Join(*transcriptDir, protocol, side)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
transcriptPrefix = filepath.Join(dir, test.name+"-")
|
|
flags = append(flags, "-write-settings", transcriptPrefix)
|
|
}
|
|
|
|
if test.testType == clientTest && test.config.Credential == nil {
|
|
test.config.Credential = &rsaCertificate
|
|
}
|
|
if test.config.Credential != nil {
|
|
flags = append(flags, "-trust-cert", test.config.Credential.RootPath)
|
|
}
|
|
|
|
flags = append(flags, test.flags...)
|
|
|
|
var env []string
|
|
if mallocNumToFail >= 0 {
|
|
env = os.Environ()
|
|
env = append(env, "MALLOC_NUMBER_TO_FAIL="+strconv.FormatInt(mallocNumToFail, 10))
|
|
if *mallocTestDebug {
|
|
env = append(env, "MALLOC_BREAK_ON_FAIL=1")
|
|
}
|
|
env = append(env, "_MALLOC_CHECK=1")
|
|
}
|
|
|
|
shim, err := newShimProcess(dispatcher, shimPath, flags, env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
statusChan <- statusMsg{test: test, statusType: statusShimStarted, pid: shim.cmd.Process.Pid}
|
|
defer shim.close()
|
|
|
|
localErr := doExchanges(test, shim, resumeCount, &transcripts)
|
|
childErr := shim.wait()
|
|
|
|
// Now that the shim has exited, all the settings files have been
|
|
// written. Append the saved transcripts.
|
|
for i, transcript := range transcripts {
|
|
if err := appendTranscript(transcriptPrefix+strconv.Itoa(i), transcript); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var isValgrindError, mustFail bool
|
|
if exitError, ok := childErr.(*exec.ExitError); ok {
|
|
switch exitError.Sys().(syscall.WaitStatus).ExitStatus() {
|
|
case 88:
|
|
return errMoreMallocs
|
|
case 89:
|
|
return errUnimplemented
|
|
case 90:
|
|
mustFail = true
|
|
case 99:
|
|
isValgrindError = true
|
|
}
|
|
}
|
|
|
|
// Account for Windows line endings.
|
|
stdout := strings.Replace(shim.stdout.String(), "\r\n", "\n", -1)
|
|
stderr := strings.Replace(shim.stderr.String(), "\r\n", "\n", -1)
|
|
|
|
// Work around an NDK / Android bug. The NDK r16 sometimes generates
|
|
// binaries with the DF_1_PIE, which the runtime linker on Android N
|
|
// complains about. The next NDK revision should work around this but,
|
|
// in the meantime, strip its error out.
|
|
//
|
|
// https://github.com/android-ndk/ndk/issues/602
|
|
// https://android-review.googlesource.com/c/platform/bionic/+/259790
|
|
// https://android-review.googlesource.com/c/toolchain/binutils/+/571550
|
|
//
|
|
// Remove this after switching to the r17 NDK.
|
|
stderr = removeFirstLineIfSuffix(stderr, ": unsupported flags DT_FLAGS_1=0x8000001")
|
|
|
|
// Separate the errors from the shim and those from tools like
|
|
// AddressSanitizer.
|
|
var extraStderr string
|
|
if stderrParts := strings.SplitN(stderr, "--- DONE ---\n", 2); len(stderrParts) == 2 {
|
|
stderr = stderrParts[0]
|
|
extraStderr = stderrParts[1]
|
|
}
|
|
|
|
failed := localErr != nil || childErr != nil
|
|
expectedErrors := translateExpectedError(test.expectedError)
|
|
correctFailure := *looseErrors || matchError(expectedErrors, stderr)
|
|
|
|
localErrString := "none"
|
|
if localErr != nil {
|
|
localErrString = localErr.Error()
|
|
}
|
|
if len(test.expectedLocalError) != 0 {
|
|
correctFailure = correctFailure && strings.Contains(localErrString, test.expectedLocalError)
|
|
}
|
|
|
|
if failed != test.shouldFail || failed && !correctFailure || mustFail {
|
|
childErrString := "none"
|
|
if childErr != nil {
|
|
childErrString = childErr.Error()
|
|
}
|
|
|
|
got, want := formatErrors(expectedErrors,
|
|
stderr, localErrString, childErrString, stdout,
|
|
test.expectedLocalError, test.expectedError)
|
|
|
|
var msg string
|
|
switch {
|
|
case failed && !test.shouldFail:
|
|
msg = fmt.Sprintf("unexpected failure\ngot:\n%s\n", got)
|
|
case !failed && test.shouldFail:
|
|
msg = fmt.Sprintf("unexpected success\nwant:\n%s\n", want)
|
|
case failed && !correctFailure:
|
|
msg = fmt.Sprintf("unexpected error\ngot:\n%s\n\nwant:\n%s\n", got, want)
|
|
case mustFail:
|
|
msg = "test failure"
|
|
default:
|
|
panic("internal error")
|
|
}
|
|
|
|
return fmt.Errorf("%s\nextra stderr:\n%s", msg, extraStderr)
|
|
}
|
|
|
|
if len(extraStderr) > 0 || (!failed && len(stderr) > 0) {
|
|
return fmt.Errorf("unexpected error output:\n%s\n%s", stderr, extraStderr)
|
|
}
|
|
|
|
if *useValgrind && isValgrindError {
|
|
return fmt.Errorf("valgrind error:\n%s\n%s", stderr, extraStderr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type tlsVersion struct {
|
|
name string
|
|
// version is the protocol version.
|
|
version uint16
|
|
// excludeFlag is the legacy shim flag to disable the version.
|
|
excludeFlag string
|
|
hasDTLS bool
|
|
hasQUIC bool
|
|
// versionDTLS, if non-zero, is the DTLS-specific representation of the version.
|
|
versionDTLS uint16
|
|
// versionWire, if non-zero, is the wire representation of the
|
|
// version. Otherwise the wire version is the protocol version or
|
|
// versionDTLS.
|
|
versionWire uint16
|
|
}
|
|
|
|
func (vers tlsVersion) String() string {
|
|
return vers.name
|
|
}
|
|
|
|
func (vers tlsVersion) shimFlag(protocol protocol) string {
|
|
// The shim uses the protocol version in its public API, but uses the
|
|
// DTLS-specific version if it exists.
|
|
if protocol == dtls && vers.versionDTLS != 0 {
|
|
return strconv.Itoa(int(vers.versionDTLS))
|
|
}
|
|
return strconv.Itoa(int(vers.version))
|
|
}
|
|
|
|
func (vers tlsVersion) wire(protocol protocol) uint16 {
|
|
if protocol == dtls && vers.versionDTLS != 0 {
|
|
return vers.versionDTLS
|
|
}
|
|
if vers.versionWire != 0 {
|
|
return vers.versionWire
|
|
}
|
|
return vers.version
|
|
}
|
|
|
|
func (vers tlsVersion) supportsProtocol(protocol protocol) bool {
|
|
if protocol == dtls {
|
|
return vers.hasDTLS
|
|
}
|
|
if protocol == quic {
|
|
return vers.hasQUIC
|
|
}
|
|
return true
|
|
}
|
|
|
|
var tlsVersions = []tlsVersion{
|
|
{
|
|
name: "TLS1",
|
|
version: VersionTLS10,
|
|
excludeFlag: "-no-tls1",
|
|
hasDTLS: true,
|
|
versionDTLS: VersionDTLS10,
|
|
},
|
|
{
|
|
name: "TLS11",
|
|
version: VersionTLS11,
|
|
excludeFlag: "-no-tls11",
|
|
},
|
|
{
|
|
name: "TLS12",
|
|
version: VersionTLS12,
|
|
excludeFlag: "-no-tls12",
|
|
hasDTLS: true,
|
|
versionDTLS: VersionDTLS12,
|
|
},
|
|
{
|
|
name: "TLS13",
|
|
version: VersionTLS13,
|
|
excludeFlag: "-no-tls13",
|
|
hasQUIC: true,
|
|
hasDTLS: true,
|
|
versionDTLS: VersionDTLS13,
|
|
versionWire: VersionTLS13,
|
|
},
|
|
}
|
|
|
|
func allVersions(protocol protocol) []tlsVersion {
|
|
if protocol == tls {
|
|
return tlsVersions
|
|
}
|
|
|
|
var ret []tlsVersion
|
|
for _, vers := range tlsVersions {
|
|
if vers.supportsProtocol(protocol) {
|
|
ret = append(ret, vers)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testCase, err error) {
|
|
var stdout bytes.Buffer
|
|
var flags []string
|
|
if len(*shimExtraFlags) > 0 {
|
|
flags = strings.Split(*shimExtraFlags, ";")
|
|
}
|
|
flags = append(flags, "-is-handshaker-supported")
|
|
shim := exec.Command(*shimPath, flags...)
|
|
shim.Stdout = &stdout
|
|
if err := shim.Run(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch strings.TrimSpace(string(stdout.Bytes())) {
|
|
case "No":
|
|
return
|
|
case "Yes":
|
|
break
|
|
default:
|
|
return nil, fmt.Errorf("unknown output from shim: %q", stdout.Bytes())
|
|
}
|
|
|
|
var allowHintMismatchPattern []string
|
|
if len(*allowHintMismatch) > 0 {
|
|
allowHintMismatchPattern = strings.Split(*allowHintMismatch, ";")
|
|
}
|
|
|
|
NextTest:
|
|
for _, test := range tests {
|
|
if test.protocol != tls ||
|
|
test.testType != serverTest ||
|
|
len(test.shimCredentials) != 0 ||
|
|
len(test.resumeShimCredentials) != 0 ||
|
|
strings.Contains(test.name, "ECH-Server") ||
|
|
test.skipSplitHandshake {
|
|
continue
|
|
}
|
|
|
|
for _, flag := range test.flags {
|
|
if flag == "-implicit-handshake" {
|
|
continue NextTest
|
|
}
|
|
}
|
|
|
|
shTest := test
|
|
shTest.name += "-Split"
|
|
shTest.flags = make([]string, len(test.flags), len(test.flags)+3)
|
|
copy(shTest.flags, test.flags)
|
|
shTest.flags = append(shTest.flags, "-handoff", "-handshaker-path", *handshakerPath)
|
|
|
|
splitHandshakeTests = append(splitHandshakeTests, shTest)
|
|
}
|
|
|
|
for _, test := range tests {
|
|
if test.protocol == dtls ||
|
|
test.testType != serverTest ||
|
|
test.skipHints {
|
|
continue
|
|
}
|
|
|
|
var matched bool
|
|
if len(allowHintMismatchPattern) > 0 {
|
|
matched, err = match(allowHintMismatchPattern, nil, test.name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error matching pattern: %s", err)
|
|
}
|
|
}
|
|
|
|
shTest := test
|
|
shTest.name += "-Hints"
|
|
shTest.flags = make([]string, len(test.flags), len(test.flags)+3)
|
|
copy(shTest.flags, test.flags)
|
|
shTest.flags = append(shTest.flags, "-handshake-hints", "-handshaker-path", *handshakerPath)
|
|
if matched {
|
|
shTest.flags = append(shTest.flags, "-allow-hint-mismatch")
|
|
}
|
|
|
|
splitHandshakeTests = append(splitHandshakeTests, shTest)
|
|
}
|
|
|
|
return splitHandshakeTests, nil
|
|
}
|
|
|
|
func worker(dispatcher *shimDispatcher, statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
for test := range c {
|
|
var err error
|
|
|
|
if *mallocTest >= 0 {
|
|
for mallocNumToFail := int64(*mallocTest); ; mallocNumToFail++ {
|
|
statusChan <- statusMsg{test: test, statusType: statusStarted}
|
|
if err = runTest(dispatcher, statusChan, test, shimPath, mallocNumToFail); err != errMoreMallocs {
|
|
if err != nil {
|
|
fmt.Printf("\n\nmalloc test failed at %d: %s\n", mallocNumToFail, err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
} else if *repeatUntilFailure {
|
|
for err == nil {
|
|
statusChan <- statusMsg{test: test, statusType: statusStarted}
|
|
err = runTest(dispatcher, statusChan, test, shimPath, -1)
|
|
}
|
|
} else {
|
|
statusChan <- statusMsg{test: test, statusType: statusStarted}
|
|
err = runTest(dispatcher, statusChan, test, shimPath, -1)
|
|
}
|
|
statusChan <- statusMsg{test: test, statusType: statusDone, err: err}
|
|
}
|
|
}
|
|
|
|
type statusType int
|
|
|
|
const (
|
|
statusStarted statusType = iota
|
|
statusShimStarted
|
|
statusDone
|
|
)
|
|
|
|
type statusMsg struct {
|
|
test *testCase
|
|
statusType statusType
|
|
pid int
|
|
err error
|
|
}
|
|
|
|
func statusPrinter(doneChan chan *testresult.Results, statusChan chan statusMsg, total int) {
|
|
var started, done, failed, unimplemented, lineLen int
|
|
|
|
testOutput := testresult.NewResults()
|
|
for msg := range statusChan {
|
|
if !*pipe {
|
|
// Erase the previous status line.
|
|
var erase string
|
|
for i := 0; i < lineLen; i++ {
|
|
erase += "\b \b"
|
|
}
|
|
fmt.Print(erase)
|
|
}
|
|
|
|
if msg.statusType == statusStarted {
|
|
started++
|
|
} else if msg.statusType == statusDone {
|
|
done++
|
|
|
|
if msg.err != nil {
|
|
if msg.err == errUnimplemented {
|
|
if *pipe {
|
|
// Print each test instead of a status line.
|
|
fmt.Printf("UNIMPLEMENTED (%s)\n", msg.test.name)
|
|
}
|
|
unimplemented++
|
|
if *allowUnimplemented {
|
|
testOutput.AddSkip(msg.test.name)
|
|
} else {
|
|
testOutput.AddResult(msg.test.name, "SKIP", nil)
|
|
}
|
|
} else {
|
|
fmt.Printf("FAILED (%s)\n%s\n", msg.test.name, msg.err)
|
|
failed++
|
|
testOutput.AddResult(msg.test.name, "FAIL", msg.err)
|
|
}
|
|
} else {
|
|
if *pipe {
|
|
// Print each test instead of a status line.
|
|
fmt.Printf("PASSED (%s)\n", msg.test.name)
|
|
}
|
|
testOutput.AddResult(msg.test.name, "PASS", nil)
|
|
}
|
|
}
|
|
|
|
if !*pipe {
|
|
// Print a new status line.
|
|
line := fmt.Sprintf("%d/%d/%d/%d/%d", failed, unimplemented, done, started, total)
|
|
if msg.statusType == statusShimStarted && *waitForDebugger {
|
|
// Note -wait-for-debugger limits the test to one worker,
|
|
// otherwise some output would be skipped.
|
|
line += fmt.Sprintf(" (%s: attach to process %d to continue)", msg.test.name, msg.pid)
|
|
}
|
|
lineLen = len(line)
|
|
os.Stdout.WriteString(line)
|
|
}
|
|
}
|
|
|
|
doneChan <- testOutput
|
|
}
|
|
|
|
func match(oneOfPatternIfAny []string, noneOfPattern []string, candidate string) (matched bool, err error) {
|
|
matched = len(oneOfPatternIfAny) == 0
|
|
|
|
var didMatch bool
|
|
for _, pattern := range oneOfPatternIfAny {
|
|
didMatch, err = filepath.Match(pattern, candidate)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
matched = didMatch || matched
|
|
}
|
|
|
|
for _, pattern := range noneOfPattern {
|
|
didMatch, err = filepath.Match(pattern, candidate)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
matched = !didMatch && matched
|
|
}
|
|
|
|
return matched, nil
|
|
}
|
|
|
|
func checkTests() {
|
|
for _, test := range testCases {
|
|
if !test.shouldFail && (len(test.expectedError) > 0 || len(test.expectedLocalError) > 0) {
|
|
panic("Error expected without shouldFail in " + test.name)
|
|
}
|
|
|
|
if test.expectResumeRejected && !test.resumeSession {
|
|
panic("expectResumeRejected without resumeSession in " + test.name)
|
|
}
|
|
|
|
if !test.skipVersionNameCheck {
|
|
for _, ver := range tlsVersions {
|
|
if !strings.Contains("-"+test.name+"-", "-"+ver.name+"-") {
|
|
continue
|
|
}
|
|
|
|
found := test.config.MaxVersion == ver.version || test.config.MinVersion == ver.version || test.expectations.version == ver.version
|
|
if test.resumeConfig != nil {
|
|
found = found || test.resumeConfig.MaxVersion == ver.version || test.resumeConfig.MinVersion == ver.version
|
|
}
|
|
if test.resumeExpectations != nil {
|
|
found = found || test.resumeExpectations.version == ver.version
|
|
}
|
|
shimFlag := ver.shimFlag(test.protocol)
|
|
for _, flag := range test.flags {
|
|
if flag == shimFlag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
panic(fmt.Sprintf("The name of test %q suggests that it's version specific, but the test does not reference %s", test.name, ver.name))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, protocol := range []protocol{tls, dtls, quic} {
|
|
if strings.Contains("-"+test.name+"-", "-"+protocol.String()+"-") && test.protocol != protocol {
|
|
panic(fmt.Sprintf("The name of test %q suggests that it tests %q, but the test does not reference it", test.name, protocol))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
var err error
|
|
if tmpDir, err = os.MkdirTemp("", "testing-certs"); err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to make temporary directory: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
initKeys()
|
|
initCertificates()
|
|
|
|
if len(*shimConfigFile) != 0 {
|
|
encoded, err := os.ReadFile(*shimConfigFile)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Couldn't read config file %q: %s\n", *shimConfigFile, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := json.Unmarshal(encoded, &shimConfig); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Couldn't decode config file %q: %s\n", *shimConfigFile, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if shimConfig.AllCurves == nil {
|
|
for _, curve := range testCurves {
|
|
shimConfig.AllCurves = append(shimConfig.AllCurves, int(curve.id))
|
|
}
|
|
}
|
|
|
|
addBasicTests()
|
|
addCipherSuiteTests()
|
|
addBadECDSASignatureTests()
|
|
addCBCPaddingTests()
|
|
addCBCSplittingTests()
|
|
addClientAuthTests()
|
|
addDDoSCallbackTests()
|
|
addVersionNegotiationTests()
|
|
addMinimumVersionTests()
|
|
addExtensionTests()
|
|
addResumptionVersionTests()
|
|
addExtendedMasterSecretTests()
|
|
addRenegotiationTests()
|
|
addDTLSReplayTests()
|
|
addSignatureAlgorithmTests()
|
|
addDTLSRetransmitTests()
|
|
addDTLSReorderTests()
|
|
addExportKeyingMaterialTests()
|
|
addExportTrafficSecretsTests()
|
|
addTLSUniqueTests()
|
|
addUnknownExtensionTests()
|
|
addRSAClientKeyExchangeTests()
|
|
addCurveTests()
|
|
addSessionTicketTests()
|
|
addTLS13RecordTests()
|
|
addAllStateMachineCoverageTests()
|
|
addChangeCipherSpecTests()
|
|
addEndOfFlightTests()
|
|
addWrongMessageTypeTests()
|
|
addTrailingMessageDataTests()
|
|
addTLS13HandshakeTests()
|
|
addTLS13CipherPreferenceTests()
|
|
addPeekTests()
|
|
addRecordVersionTests()
|
|
addCertificateTests()
|
|
addRetainOnlySHA256ClientCertTests()
|
|
addECDSAKeyUsageTests()
|
|
addRSAKeyUsageTests()
|
|
addExtraHandshakeTests()
|
|
addOmitExtensionsTests()
|
|
addCertCompressionTests()
|
|
addJDK11WorkaroundTests()
|
|
addDelegatedCredentialTests()
|
|
addEncryptedClientHelloTests()
|
|
addHintMismatchTests()
|
|
addCompliancePolicyTests()
|
|
addCertificateSelectionTests()
|
|
addKeyUpdateTests()
|
|
addPAKETests()
|
|
addTrustAnchorTests()
|
|
|
|
toAppend, err := convertToSplitHandshakeTests(testCases)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error making split handshake tests: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
testCases = append(testCases, toAppend...)
|
|
|
|
checkTests()
|
|
|
|
dispatcher, err := newShimDispatcher()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error opening socket: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
defer dispatcher.Close()
|
|
|
|
numWorkers := *numWorkersFlag
|
|
if useDebugger() {
|
|
numWorkers = 1
|
|
}
|
|
|
|
statusChan := make(chan statusMsg, numWorkers)
|
|
testChan := make(chan *testCase, numWorkers)
|
|
doneChan := make(chan *testresult.Results)
|
|
|
|
go statusPrinter(doneChan, statusChan, len(testCases))
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < numWorkers; i++ {
|
|
wg.Add(1)
|
|
go worker(dispatcher, statusChan, testChan, *shimPath, &wg)
|
|
}
|
|
|
|
var oneOfPatternIfAny, noneOfPattern []string
|
|
if len(*testToRun) > 0 {
|
|
oneOfPatternIfAny = strings.Split(*testToRun, ";")
|
|
}
|
|
if len(*skipTest) > 0 {
|
|
noneOfPattern = strings.Split(*skipTest, ";")
|
|
}
|
|
|
|
shardIndex, shardTotal, err := getSharding()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if shardTotal > 0 {
|
|
fmt.Printf("This is shard %d of 0..%d (inclusive)\n", shardIndex, shardTotal-1)
|
|
}
|
|
|
|
var foundTest bool
|
|
for i := range testCases {
|
|
if shardTotal > 0 && i%shardTotal != shardIndex {
|
|
continue
|
|
}
|
|
|
|
matched, err := match(oneOfPatternIfAny, noneOfPattern, testCases[i].name)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error matching pattern: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !*includeDisabled {
|
|
for pattern := range shimConfig.DisabledTests {
|
|
isDisabled, err := filepath.Match(pattern, testCases[i].name)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error matching pattern %q from config file: %s\n", pattern, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if isDisabled {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if matched {
|
|
if foundTest && *useRR {
|
|
fmt.Fprintf(os.Stderr, "Too many matching tests. Only one test can run when RR is enabled.\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
foundTest = true
|
|
testChan <- &testCases[i]
|
|
|
|
// Only run one test if repeating until failure.
|
|
if *repeatUntilFailure {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !foundTest && shardTotal == 0 {
|
|
fmt.Fprintf(os.Stderr, "No tests run\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
close(testChan)
|
|
wg.Wait()
|
|
close(statusChan)
|
|
testOutput := <-doneChan
|
|
|
|
fmt.Printf("\n")
|
|
|
|
if *jsonOutput != "" {
|
|
if err := testOutput.WriteToFile(*jsonOutput); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
|
}
|
|
}
|
|
|
|
if *useRR {
|
|
fmt.Println("RR trace recorded. Replay with `rr replay`.")
|
|
}
|
|
|
|
if !testOutput.HasUnexpectedResults() {
|
|
os.Exit(1)
|
|
}
|
|
}
|