th5/handshake_client_test.go
Adam Langley 98968dca72 crypto/tls: rework reference tests.
The practice of storing reference connections for testing has worked
reasonably well, but the large blocks of literal data in the .go files
is ugly and updating the tests is a real problem because their number
has grown.

This CL changes the way that reference tests work. It's now possible to
automatically update the tests and the test data is now stored in
testdata/. This should make it easier to implement changes that affect
all connections, like implementing the renegotiation extension.

R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/42060044
2013-12-20 11:37:05 -05:00

356 lines
9.3 KiB
Go

// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tls
import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
)
// Note: see comment in handshake_test.go for details of how the reference
// tests work.
// blockingSource is an io.Reader that blocks a Read call until it's closed.
type blockingSource chan bool
func (b blockingSource) Read([]byte) (n int, err error) {
<-b
return 0, io.EOF
}
// clientTest represents a test of the TLS client handshake against a reference
// implementation.
type clientTest struct {
// name is a freeform string identifying the test and the file in which
// the expected results will be stored.
name string
// command, if not empty, contains a series of arguments for the
// command to run for the reference server.
command []string
// config, if not nil, contains a custom Config to use for this test.
config *Config
// cert, if not empty, contains a DER-encoded certificate for the
// reference server.
cert []byte
// key, if not nil, contains either a *rsa.PrivateKey or
// *ecdsa.PrivateKey which is the private key for the reference server.
key interface{}
}
var defaultServerCommand = []string{"openssl", "s_server"}
// connFromCommand starts the reference server process, connects to it and
// returns a recordingConn for the connection. The stdin return value is a
// blockingSource for the stdin of the child process. It must be closed before
// Waiting for child.
func (test *clientTest) connFromCommand() (conn *recordingConn, child *exec.Cmd, stdin blockingSource, err error) {
cert := testRSACertificate
if len(test.cert) > 0 {
cert = test.cert
}
certPath := tempFile(string(cert))
defer os.Remove(certPath)
var key interface{} = testRSAPrivateKey
if test.key != nil {
key = test.key
}
var pemType string
var derBytes []byte
switch key := key.(type) {
case *rsa.PrivateKey:
pemType = "RSA"
derBytes = x509.MarshalPKCS1PrivateKey(key)
case *ecdsa.PrivateKey:
pemType = "EC"
var err error
derBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
panic(err)
}
default:
panic("unknown key type")
}
var pemOut bytes.Buffer
pem.Encode(&pemOut, &pem.Block{Type: pemType + " PRIVATE KEY", Bytes: derBytes})
keyPath := tempFile(string(pemOut.Bytes()))
defer os.Remove(keyPath)
var command []string
if len(test.command) > 0 {
command = append(command, test.command...)
} else {
command = append(command, defaultServerCommand...)
}
command = append(command, "-cert", certPath, "-certform", "DER", "-key", keyPath)
// serverPort contains the port that OpenSSL will listen on. OpenSSL
// can't take "0" as an argument here so we have to pick a number and
// hope that it's not in use on the machine. Since this only occurs
// when -update is given and thus when there's a human watching the
// test, this isn't too bad.
const serverPort = 24323
command = append(command, "-accept", strconv.Itoa(serverPort))
cmd := exec.Command(command[0], command[1:]...)
stdin = blockingSource(make(chan bool))
cmd.Stdin = stdin
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Start(); err != nil {
return nil, nil, nil, err
}
// OpenSSL does print an "ACCEPT" banner, but it does so *before*
// opening the listening socket, so we can't use that to wait until it
// has started listening. Thus we are forced to poll until we get a
// connection.
var tcpConn net.Conn
for i := uint(0); i < 5; i++ {
var err error
tcpConn, err = net.DialTCP("tcp", nil, &net.TCPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: serverPort,
})
if err == nil {
break
}
time.Sleep((1 << i) * 5 * time.Millisecond)
}
if tcpConn == nil {
close(stdin)
out.WriteTo(os.Stdout)
cmd.Process.Kill()
return nil, nil, nil, cmd.Wait()
}
record := &recordingConn{
Conn: tcpConn,
}
return record, cmd, stdin, nil
}
func (test *clientTest) dataPath() string {
return filepath.Join("testdata", "Client-"+test.name)
}
func (test *clientTest) loadData() (flows [][]byte, err error) {
in, err := os.Open(test.dataPath())
if err != nil {
return nil, err
}
defer in.Close()
return parseTestData(in)
}
func (test *clientTest) run(t *testing.T, write bool) {
var clientConn, serverConn net.Conn
var recordingConn *recordingConn
var childProcess *exec.Cmd
var stdin blockingSource
if write {
var err error
recordingConn, childProcess, stdin, err = test.connFromCommand()
if err != nil {
t.Fatalf("Failed to start subcommand: %s", err)
}
clientConn = recordingConn
} else {
clientConn, serverConn = net.Pipe()
}
config := test.config
if config == nil {
config = testConfig
}
client := Client(clientConn, config)
doneChan := make(chan bool)
go func() {
if _, err := client.Write([]byte("hello\n")); err != nil {
t.Logf("Client.Write failed: %s", err)
}
client.Close()
clientConn.Close()
doneChan <- true
}()
if !write {
flows, err := test.loadData()
if err != nil {
t.Fatalf("%s: failed to load data from %s", test.name, test.dataPath())
}
for i, b := range flows {
if i%2 == 1 {
serverConn.Write(b)
continue
}
bb := make([]byte, len(b))
_, err := io.ReadFull(serverConn, bb)
if err != nil {
t.Fatalf("%s #%d: %s", test.name, i, err)
}
if !bytes.Equal(b, bb) {
t.Fatalf("%s #%d: mismatch on read: got:%x want:%x", test.name, i, bb, b)
}
}
serverConn.Close()
}
<-doneChan
if write {
path := test.dataPath()
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
t.Fatalf("Failed to create output file: %s", err)
}
defer out.Close()
recordingConn.Close()
close(stdin)
childProcess.Process.Kill()
childProcess.Wait()
if len(recordingConn.flows) < 3 {
childProcess.Stdout.(*bytes.Buffer).WriteTo(os.Stdout)
t.Fatalf("Client connection didn't work")
}
recordingConn.WriteTo(out)
fmt.Printf("Wrote %s\n", path)
}
}
func runClientTestForVersion(t *testing.T, template *clientTest, prefix, option string) {
test := *template
test.name = prefix + test.name
if len(test.command) == 0 {
test.command = defaultClientCommand
}
test.command = append([]string(nil), test.command...)
test.command = append(test.command, option)
test.run(t, *update)
}
func runClientTestTLS10(t *testing.T, template *clientTest) {
runClientTestForVersion(t, template, "TLSv10-", "-tls1")
}
func runClientTestTLS11(t *testing.T, template *clientTest) {
runClientTestForVersion(t, template, "TLSv11-", "-tls1_1")
}
func runClientTestTLS12(t *testing.T, template *clientTest) {
runClientTestForVersion(t, template, "TLSv12-", "-tls1_2")
}
func TestHandshakeClientRSARC4(t *testing.T) {
test := &clientTest{
name: "RSA-RC4",
command: []string{"openssl", "s_server", "-cipher", "RC4-SHA"},
}
runClientTestTLS10(t, test)
runClientTestTLS11(t, test)
runClientTestTLS12(t, test)
}
func TestHandshakeClientECDHERSAAES(t *testing.T) {
test := &clientTest{
name: "ECDHE-RSA-AES",
command: []string{"openssl", "s_server", "-cipher", "ECDHE-RSA-AES128-SHA"},
}
runClientTestTLS10(t, test)
runClientTestTLS11(t, test)
runClientTestTLS12(t, test)
}
func TestHandshakeClientECDHEECDSAAES(t *testing.T) {
test := &clientTest{
name: "ECDHE-ECDSA-AES",
command: []string{"openssl", "s_server", "-cipher", "ECDHE-ECDSA-AES128-SHA"},
cert: testECDSACertificate,
key: testECDSAPrivateKey,
}
runClientTestTLS10(t, test)
runClientTestTLS11(t, test)
runClientTestTLS12(t, test)
}
func TestHandshakeClientECDHEECDSAAESGCM(t *testing.T) {
test := &clientTest{
name: "ECDHE-ECDSA-AES-GCM",
command: []string{"openssl", "s_server", "-cipher", "ECDHE-ECDSA-AES128-GCM-SHA256"},
cert: testECDSACertificate,
key: testECDSAPrivateKey,
}
runClientTestTLS12(t, test)
}
func TestHandshakeClientCertRSA(t *testing.T) {
config := *testConfig
cert, _ := X509KeyPair([]byte(clientCertificatePEM), []byte(clientKeyPEM))
config.Certificates = []Certificate{cert}
test := &clientTest{
name: "ClientCert-RSA-RSA",
command: []string{"openssl", "s_server", "-cipher", "RC4-SHA", "-verify", "1"},
config: &config,
}
runClientTestTLS10(t, test)
runClientTestTLS12(t, test)
test = &clientTest{
name: "ClientCert-RSA-ECDSA",
command: []string{"openssl", "s_server", "-cipher", "ECDHE-ECDSA-AES128-SHA", "-verify", "1"},
config: &config,
cert: testECDSACertificate,
key: testECDSAPrivateKey,
}
runClientTestTLS10(t, test)
runClientTestTLS12(t, test)
}
func TestHandshakeClientCertECDSA(t *testing.T) {
config := *testConfig
cert, _ := X509KeyPair([]byte(clientECDSACertificatePEM), []byte(clientECDSAKeyPEM))
config.Certificates = []Certificate{cert}
test := &clientTest{
name: "ClientCert-ECDSA-RSA",
command: []string{"openssl", "s_server", "-cipher", "RC4-SHA", "-verify", "1"},
config: &config,
}
runClientTestTLS10(t, test)
runClientTestTLS12(t, test)
test = &clientTest{
name: "ClientCert-ECDSA-ECDSA",
command: []string{"openssl", "s_server", "-cipher", "ECDHE-ECDSA-AES128-SHA", "-verify", "1"},
config: &config,
cert: testECDSACertificate,
key: testECDSAPrivateKey,
}
runClientTestTLS10(t, test)
runClientTestTLS12(t, test)
}