Add tests which modify the shim ticket.

The existing tests for this codepath require us to reconfigure the shim.
This will not work when TLS 1.3 cipher configuration is detached from
the old cipher language. It also doesn't hit codepaths like sessions
containing a TLS 1.3 version but TLS 1.2 cipher.

Instead, add some logic to the runner to rewrite tickets and build tests
out of that.

Change-Id: I57ac5d49c3069497ed9aaf430afc65c631014bf6
Reviewed-on: https://boringssl-review.googlesource.com/12024
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 2d46ed9..8f204a1 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -989,6 +989,12 @@
     SSL_CTX_set_tlsext_servername_callback(ssl_ctx.get(), ServerNameCallback);
   }
 
+  if (!config->ticket_key.empty() &&
+      !SSL_CTX_set_tlsext_ticket_keys(ssl_ctx.get(), config->ticket_key.data(),
+                                      config->ticket_key.size())) {
+    return nullptr;
+  }
+
   return ssl_ctx;
 }
 
@@ -1475,11 +1481,6 @@
   if (config->max_cert_list > 0) {
     SSL_set_max_cert_list(ssl.get(), config->max_cert_list);
   }
-  if (is_resume &&
-      !config->resume_cipher.empty() &&
-      !SSL_set_cipher_list(ssl.get(), config->resume_cipher.c_str())) {
-    return false;
-  }
 
   int sock = Connect(config->port);
   if (sock == -1) {
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 921b03b..82823cc 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -661,9 +661,9 @@
 	// TLS 1.2 and 1.3 extensions.
 	SendBothTickets bool
 
-	// CorruptTicket causes a client to corrupt a session ticket before
-	// sending it in a resume handshake.
-	CorruptTicket bool
+	// FilterTicket, if not nil, causes the client to modify a session
+	// ticket before sending it in a resume handshake.
+	FilterTicket func([]byte) ([]byte, error)
 
 	// OversizedSessionId causes the session id that is sent with a ticket
 	// resumption attempt to be too large (33 bytes).
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 16c5dbd..633d749 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -234,14 +234,15 @@
 
 	if session != nil && c.config.time().Before(session.ticketExpiration) {
 		ticket := session.sessionTicket
-		if c.config.Bugs.CorruptTicket && len(ticket) > 0 {
+		if c.config.Bugs.FilterTicket != nil && len(ticket) > 0 {
+			// Copy the ticket so FilterTicket may act in-place.
 			ticket = make([]byte, len(session.sessionTicket))
 			copy(ticket, session.sessionTicket)
-			offset := 40
-			if offset >= len(ticket) {
-				offset = len(ticket) - 1
+
+			ticket, err = c.config.Bugs.FilterTicket(ticket)
+			if err != nil {
+				return err
 			}
-			ticket[offset] ^= 0x40
 		}
 
 		if session.vers >= VersionTLS13 || c.config.Bugs.SendBothTickets {
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index cca437b..5e58733 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -4896,7 +4896,10 @@
 			config: Config{
 				MaxVersion: ver.version,
 				Bugs: ProtocolBugs{
-					CorruptTicket: true,
+					FilterTicket: func(in []byte) ([]byte, error) {
+						in[len(in)-1] ^= 1
+						return in, nil
+					},
 				},
 			},
 			resumeSession:        true,
@@ -4934,7 +4937,10 @@
 			config: Config{
 				MaxVersion: ver.version,
 				Bugs: ProtocolBugs{
-					CorruptTicket: true,
+					FilterTicket: func(in []byte) ([]byte, error) {
+						in[len(in)-1] ^= 1
+						return in, nil
+					},
 				},
 			},
 			resumeSession:        true,
@@ -5380,31 +5386,149 @@
 		}
 	}
 
-	// Sessions with disabled ciphers are not resumed.
+	// Make sure shim ticket mutations are functional.
 	testCases = append(testCases, testCase{
 		testType:      serverTest,
-		name:          "Resume-Server-CipherMismatch",
+		name:          "ShimTicketRewritable",
+		resumeSession: true,
+		config: Config{
+			MaxVersion:   VersionTLS12,
+			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					in, err := SetShimTicketVersion(in, VersionTLS12)
+					if err != nil {
+						return nil, err
+					}
+					return SetShimTicketCipherSuite(in, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
+				},
+			},
+		},
+		flags: []string{
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
+	})
+
+	// Resumptions are declined if the version does not match.
+	testCases = append(testCases, testCase{
+		testType:      serverTest,
+		name:          "Resume-Server-DeclineCrossVersion",
 		resumeSession: true,
 		config: Config{
 			MaxVersion: VersionTLS12,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketVersion(in, VersionTLS13)
+				},
+			},
 		},
-		flags:                []string{"-cipher", "AES128", "-resume-cipher", "AES256"},
-		shouldFail:           false,
+		flags: []string{
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
 		expectResumeRejected: true,
 	})
 
 	testCases = append(testCases, testCase{
 		testType:      serverTest,
-		name:          "Resume-Server-CipherMismatch-TLS13",
+		name:          "Resume-Server-DeclineCrossVersion-TLS13",
 		resumeSession: true,
 		config: Config{
 			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketVersion(in, VersionTLS12)
+				},
+			},
 		},
-		flags:                []string{"-cipher", "AES128", "-resume-cipher", "AES256"},
-		shouldFail:           false,
+		flags: []string{
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
 		expectResumeRejected: true,
 	})
 
+	// Resumptions are declined if the cipher is invalid or disabled.
+	testCases = append(testCases, testCase{
+		testType:      serverTest,
+		name:          "Resume-Server-DeclineBadCipher",
+		resumeSession: true,
+		config: Config{
+			MaxVersion: VersionTLS12,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketCipherSuite(in, TLS_AES_128_GCM_SHA256)
+				},
+			},
+		},
+		flags: []string{
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
+		expectResumeRejected: true,
+	})
+
+	testCases = append(testCases, testCase{
+		testType:      serverTest,
+		name:          "Resume-Server-DeclineBadCipher-2",
+		resumeSession: true,
+		config: Config{
+			MaxVersion: VersionTLS12,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketCipherSuite(in, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
+				},
+			},
+		},
+		flags: []string{
+			"-cipher", "AES128",
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
+		expectResumeRejected: true,
+	})
+
+	testCases = append(testCases, testCase{
+		testType:      serverTest,
+		name:          "Resume-Server-DeclineBadCipher-TLS13",
+		resumeSession: true,
+		config: Config{
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketCipherSuite(in, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
+				},
+			},
+		},
+		flags: []string{
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
+		expectResumeRejected: true,
+	})
+
+	testCases = append(testCases, testCase{
+		testType:      serverTest,
+		name:          "Resume-Server-DeclineBadCipher-2-TLS13",
+		resumeSession: true,
+		config: Config{
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				FilterTicket: func(in []byte) ([]byte, error) {
+					return SetShimTicketCipherSuite(in, TLS_AES_256_GCM_SHA384)
+				},
+			},
+		},
+		flags: []string{
+			"-cipher", "AES128",
+			"-ticket-key",
+			base64.StdEncoding.EncodeToString(TestShimTicketKey),
+		},
+		expectResumeRejected: true,
+	})
+
+	// Sessions may not be resumed at a different cipher.
 	testCases = append(testCases, testCase{
 		name:          "Resume-Client-CipherMismatch",
 		resumeSession: true,
diff --git a/ssl/test/runner/shim_ticket.go b/ssl/test/runner/shim_ticket.go
new file mode 100644
index 0000000..30eb1d9
--- /dev/null
+++ b/ssl/test/runner/shim_ticket.go
@@ -0,0 +1,249 @@
+// Copyright (c) 2016, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package runner
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/asn1"
+	"errors"
+)
+
+// TestShimTicketKey is the testing key assumed for the shim.
+var TestShimTicketKey = make([]byte, 48)
+
+func DecryptShimTicket(in []byte) ([]byte, error) {
+	name := TestShimTicketKey[:16]
+	macKey := TestShimTicketKey[16:32]
+	encKey := TestShimTicketKey[32:48]
+
+	h := hmac.New(sha256.New, macKey)
+
+	block, err := aes.NewCipher(encKey)
+	if err != nil {
+		panic(err)
+	}
+
+	if len(in) < len(name)+block.BlockSize()+1+h.Size() {
+		return nil, errors.New("tls: shim ticket too short")
+	}
+
+	// Check the key name.
+	if !bytes.Equal(name, in[:len(name)]) {
+		return nil, errors.New("tls: shim ticket name mismatch")
+	}
+
+	// Check the MAC at the end of the ticket.
+	mac := in[len(in)-h.Size():]
+	in = in[:len(in)-h.Size()]
+	h.Write(in)
+	if !hmac.Equal(mac, h.Sum(nil)) {
+		return nil, errors.New("tls: shim ticket MAC mismatch")
+	}
+
+	// The MAC covers the key name, but the encryption does not.
+	in = in[len(name):]
+
+	// Decrypt in-place.
+	iv := in[:block.BlockSize()]
+	in = in[block.BlockSize():]
+	if l := len(in); l == 0 || l % block.BlockSize() != 0 {
+		return nil, errors.New("tls: ticket ciphertext not a multiple of the block size")
+	}
+	out := make([]byte, len(in))
+	cbc := cipher.NewCBCDecrypter(block, iv)
+	cbc.CryptBlocks(out, in)
+
+	// Remove the padding.
+	pad := int(out[len(out)-1])
+	if pad == 0 || pad > block.BlockSize() || pad > len(in) {
+		return nil, errors.New("tls: bad shim ticket CBC pad")
+	}
+
+	for i := 0; i < pad; i++ {
+		if out[len(out)-1-i] != byte(pad) {
+			return nil, errors.New("tls: bad shim ticket CBC pad")
+		}
+	}
+
+	return out[:len(out)-pad], nil
+}
+
+func EncryptShimTicket(in []byte) []byte {
+	name := TestShimTicketKey[:16]
+	macKey := TestShimTicketKey[16:32]
+	encKey := TestShimTicketKey[32:48]
+
+	h := hmac.New(sha256.New, macKey)
+
+	block, err := aes.NewCipher(encKey)
+	if err != nil {
+		panic(err)
+	}
+
+	// Use the zero IV for rewritten tickets.
+	iv := make([]byte, block.BlockSize())
+	cbc := cipher.NewCBCEncrypter(block, iv)
+	pad := block.BlockSize() - (len(in) % block.BlockSize())
+
+	out := make([]byte, 0, len(name)+len(iv)+len(in)+pad+h.Size())
+	out = append(out, name...)
+	out = append(out, iv...)
+	out = append(out, in...)
+	for i := 0; i < pad; i++ {
+		out = append(out, byte(pad))
+	}
+
+	ciphertext := out[len(name)+len(iv):]
+	cbc.CryptBlocks(ciphertext, ciphertext)
+
+	h.Write(out)
+	return h.Sum(out)
+}
+
+const asn1Constructed = 0x20
+
+func parseDERElement(in []byte) (tag byte, body, rest []byte, ok bool) {
+	rest = in
+	if len(rest) < 1 {
+		return
+	}
+
+	tag = rest[0]
+	rest = rest[1:]
+
+	if tag&0x1f == 0x1f {
+		// Long-form tags not supported.
+		return
+	}
+
+	if len(rest) < 1 {
+		return
+	}
+
+	length := int(rest[0])
+	rest = rest[1:]
+	if length > 0x7f {
+		lengthLength := length & 0x7f
+		length = 0
+		if lengthLength == 0 {
+			// No indefinite-length encoding.
+			return
+		}
+
+		// Decode long-form lengths.
+		for lengthLength > 0 {
+			if len(rest) < 1 || (length<<8)>>8 != length {
+				return
+			}
+			if length == 0 && rest[0] == 0 {
+				// Length not minimally-encoded.
+				return
+			}
+			length <<= 8
+			length |= int(rest[0])
+			rest = rest[1:]
+			lengthLength--
+		}
+
+		if length < 0x80 {
+			// Length not minimally-encoded.
+			return
+		}
+	}
+
+	if len(rest) < length {
+		return
+	}
+
+	body = rest[:length]
+	rest = rest[length:]
+	ok = true
+	return
+}
+
+func SetShimTicketVersion(in []byte, vers uint16) ([]byte, error) {
+	plaintext, err := DecryptShimTicket(in)
+	if err != nil {
+		return nil, err
+	}
+
+	tag, session, _, ok := parseDERElement(plaintext)
+	if !ok || tag != asn1.TagSequence|asn1Constructed {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// Skip the session version.
+	tag, _, session, ok = parseDERElement(session)
+	if !ok || tag != asn1.TagInteger {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// Next field is the protocol version.
+	tag, version, _, ok := parseDERElement(session)
+	if !ok || tag != asn1.TagInteger {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// This code assumes both old and new versions are encoded in two
+	// bytes. This isn't quite right as INTEGERs are minimally-encoded, but
+	// we do not need to support other caess for now.
+	if len(version) != 2 || vers < 0x80 || vers >= 0x8000 {
+		return nil, errors.New("tls: unsupported version in shim session")
+	}
+
+	version[0] = byte(vers >> 8)
+	version[1] = byte(vers)
+
+	return EncryptShimTicket(plaintext), nil
+}
+
+func SetShimTicketCipherSuite(in []byte, id uint16) ([]byte, error) {
+	plaintext, err := DecryptShimTicket(in)
+	if err != nil {
+		return nil, err
+	}
+
+	tag, session, _, ok := parseDERElement(plaintext)
+	if !ok || tag != asn1.TagSequence|asn1Constructed {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// Skip the session version.
+	tag, _, session, ok = parseDERElement(session)
+	if !ok || tag != asn1.TagInteger {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// Skip the protocol version.
+	tag, _, session, ok = parseDERElement(session)
+	if !ok || tag != asn1.TagInteger {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	// Next field is the cipher suite.
+	tag, cipherSuite, _, ok := parseDERElement(session)
+	if !ok || tag != asn1.TagOctetString || len(cipherSuite) != 2 {
+		return nil, errors.New("tls: could not decode shim session")
+	}
+
+	cipherSuite[0] = byte(id >> 8)
+	cipherSuite[1] = byte(id)
+
+	return EncryptShimTicket(plaintext), nil
+}
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 43b28a2..70758df 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -128,7 +128,6 @@
   { "-cipher", &TestConfig::cipher },
   { "-cipher-tls10", &TestConfig::cipher_tls10 },
   { "-cipher-tls11", &TestConfig::cipher_tls11 },
-  { "-resume-cipher", &TestConfig::resume_cipher },
   { "-export-label", &TestConfig::export_label },
   { "-export-context", &TestConfig::export_context },
 };
@@ -141,6 +140,7 @@
     &TestConfig::expected_signed_cert_timestamps },
   { "-ocsp-response", &TestConfig::ocsp_response },
   { "-signed-cert-timestamps", &TestConfig::signed_cert_timestamps },
+  { "-ticket-key", &TestConfig::ticket_key },
 };
 
 const Flag<int> kIntFlags[] = {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 8aa6785..0116f14 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -76,7 +76,6 @@
   std::string cipher;
   std::string cipher_tls10;
   std::string cipher_tls11;
-  std::string resume_cipher;
   bool handshake_never_done = false;
   int export_keying_material = 0;
   std::string export_label;
@@ -116,6 +115,7 @@
   bool peek_then_read = false;
   bool enable_grease = false;
   int max_cert_list = 0;
+  std::string ticket_key;
 };
 
 bool ParseConfig(int argc, char **argv, TestConfig *out_config);