Add tests for session-ID-based resumption.

This implements session IDs in client and server in runner.go.

Change-Id: I26655f996b7b44c7eb56340ef6a415d3f2ac3503
Reviewed-on: https://boringssl-review.googlesource.com/2350
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 01b7581..476a2a4 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -203,6 +203,7 @@
 // ClientSessionState contains the state needed by clients to resume TLS
 // sessions.
 type ClientSessionState struct {
+	sessionId            []uint8             // Session ID supplied by the server. nil if the session has a ticket.
 	sessionTicket        []uint8             // Encrypted ticket used for session resumption with server
 	vers                 uint16              // SSL/TLS version negotiated for the session
 	cipherSuite          uint16              // Ciphersuite negotiated for the session
@@ -225,6 +226,19 @@
 	Put(sessionKey string, cs *ClientSessionState)
 }
 
+// ServerSessionCache is a cache of sessionState objects that can be used by a
+// client to resume a TLS session with a given server. ServerSessionCache
+// implementations should expect to be called concurrently from different
+// goroutines.
+type ServerSessionCache interface {
+	// Get searches for a sessionState associated with the given session
+	// ID. On return, ok is true if one was found.
+	Get(sessionId string) (session *sessionState, ok bool)
+
+	// Put adds the sessionState to the cache with the given session ID.
+	Put(sessionId string, session *sessionState)
+}
+
 // A Config structure is used to configure a TLS client or server.
 // After one has been passed to a TLS function it must not be
 // modified. A Config may be reused; the tls package will also not
@@ -311,10 +325,14 @@
 	// connections using that key are compromised.
 	SessionTicketKey [32]byte
 
-	// SessionCache is a cache of ClientSessionState entries for TLS session
-	// resumption.
+	// ClientSessionCache is a cache of ClientSessionState entries
+	// for TLS session resumption.
 	ClientSessionCache ClientSessionCache
 
+	// ServerSessionCache is a cache of sessionState entries for TLS session
+	// resumption.
+	ServerSessionCache ServerSessionCache
+
 	// MinVersion contains the minimum SSL/TLS version that is acceptable.
 	// If zero, then SSLv3 is taken as the minimum.
 	MinVersion uint16
@@ -732,8 +750,8 @@
 	unmarshal([]byte) bool
 }
 
-// lruSessionCache is a ClientSessionCache implementation that uses an LRU
-// caching strategy.
+// lruSessionCache is a client or server session cache implementation
+// that uses an LRU caching strategy.
 type lruSessionCache struct {
 	sync.Mutex
 
@@ -744,27 +762,11 @@
 
 type lruSessionCacheEntry struct {
 	sessionKey string
-	state      *ClientSessionState
-}
-
-// NewLRUClientSessionCache returns a ClientSessionCache with the given
-// capacity that uses an LRU strategy. If capacity is < 1, a default capacity
-// is used instead.
-func NewLRUClientSessionCache(capacity int) ClientSessionCache {
-	const defaultSessionCacheCapacity = 64
-
-	if capacity < 1 {
-		capacity = defaultSessionCacheCapacity
-	}
-	return &lruSessionCache{
-		m:        make(map[string]*list.Element),
-		q:        list.New(),
-		capacity: capacity,
-	}
+	state      interface{}
 }
 
 // Put adds the provided (sessionKey, cs) pair to the cache.
-func (c *lruSessionCache) Put(sessionKey string, cs *ClientSessionState) {
+func (c *lruSessionCache) Put(sessionKey string, cs interface{}) {
 	c.Lock()
 	defer c.Unlock()
 
@@ -790,9 +792,9 @@
 	c.m[sessionKey] = elem
 }
 
-// Get returns the ClientSessionState value associated with a given key. It
-// returns (nil, false) if no value is found.
-func (c *lruSessionCache) Get(sessionKey string) (*ClientSessionState, bool) {
+// Get returns the value associated with a given key. It returns (nil,
+// false) if no value is found.
+func (c *lruSessionCache) Get(sessionKey string) (interface{}, bool) {
 	c.Lock()
 	defer c.Unlock()
 
@@ -803,6 +805,78 @@
 	return nil, false
 }
 
+// lruClientSessionCache is a ClientSessionCache implementation that
+// uses an LRU caching strategy.
+type lruClientSessionCache struct {
+	lruSessionCache
+}
+
+func (c *lruClientSessionCache) Put(sessionKey string, cs *ClientSessionState) {
+	c.lruSessionCache.Put(sessionKey, cs)
+}
+
+func (c *lruClientSessionCache) Get(sessionKey string) (*ClientSessionState, bool) {
+	cs, ok := c.lruSessionCache.Get(sessionKey)
+	if !ok {
+		return nil, false
+	}
+	return cs.(*ClientSessionState), true
+}
+
+// lruServerSessionCache is a ServerSessionCache implementation that
+// uses an LRU caching strategy.
+type lruServerSessionCache struct {
+	lruSessionCache
+}
+
+func (c *lruServerSessionCache) Put(sessionId string, session *sessionState) {
+	c.lruSessionCache.Put(sessionId, session)
+}
+
+func (c *lruServerSessionCache) Get(sessionId string) (*sessionState, bool) {
+	cs, ok := c.lruSessionCache.Get(sessionId)
+	if !ok {
+		return nil, false
+	}
+	return cs.(*sessionState), true
+}
+
+// NewLRUClientSessionCache returns a ClientSessionCache with the given
+// capacity that uses an LRU strategy. If capacity is < 1, a default capacity
+// is used instead.
+func NewLRUClientSessionCache(capacity int) ClientSessionCache {
+	const defaultSessionCacheCapacity = 64
+
+	if capacity < 1 {
+		capacity = defaultSessionCacheCapacity
+	}
+	return &lruClientSessionCache{
+		lruSessionCache{
+			m:        make(map[string]*list.Element),
+			q:        list.New(),
+			capacity: capacity,
+		},
+	}
+}
+
+// NewLRUServerSessionCache returns a ServerSessionCache with the given
+// capacity that uses an LRU strategy. If capacity is < 1, a default capacity
+// is used instead.
+func NewLRUServerSessionCache(capacity int) ServerSessionCache {
+	const defaultSessionCacheCapacity = 64
+
+	if capacity < 1 {
+		capacity = defaultSessionCacheCapacity
+	}
+	return &lruServerSessionCache{
+		lruSessionCache{
+			m:        make(map[string]*list.Element),
+			q:        list.New(),
+			capacity: capacity,
+		},
+	}
+}
+
 // TODO(jsing): Make these available to both crypto/x509 and crypto/tls.
 type dsaSignature struct {
 	R, S *big.Int
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index c4dff89..1b15605 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -136,18 +136,17 @@
 	var session *ClientSessionState
 	var cacheKey string
 	sessionCache := c.config.ClientSessionCache
-	if c.config.SessionTicketsDisabled {
-		sessionCache = nil
-	}
 
 	if sessionCache != nil {
-		hello.ticketSupported = true
+		hello.ticketSupported = !c.config.SessionTicketsDisabled
 
 		// Try to resume a previously negotiated TLS session, if
 		// available.
 		cacheKey = clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
 		candidateSession, ok := sessionCache.Get(cacheKey)
 		if ok {
+			ticketOk := !c.config.SessionTicketsDisabled || candidateSession.sessionTicket == nil
+
 			// Check that the ciphersuite/version used for the
 			// previous session are still valid.
 			cipherSuiteOk := false
@@ -160,36 +159,40 @@
 
 			versOk := candidateSession.vers >= c.config.minVersion() &&
 				candidateSession.vers <= c.config.maxVersion()
-			if versOk && cipherSuiteOk {
+			if ticketOk && versOk && cipherSuiteOk {
 				session = candidateSession
 			}
 		}
 	}
 
 	if session != nil {
-		hello.sessionTicket = session.sessionTicket
-		if c.config.Bugs.CorruptTicket {
-			hello.sessionTicket = make([]byte, len(session.sessionTicket))
-			copy(hello.sessionTicket, session.sessionTicket)
-			if len(hello.sessionTicket) > 0 {
-				offset := 40
-				if offset > len(hello.sessionTicket) {
-					offset = len(hello.sessionTicket) - 1
+		if session.sessionTicket != nil {
+			hello.sessionTicket = session.sessionTicket
+			if c.config.Bugs.CorruptTicket {
+				hello.sessionTicket = make([]byte, len(session.sessionTicket))
+				copy(hello.sessionTicket, session.sessionTicket)
+				if len(hello.sessionTicket) > 0 {
+					offset := 40
+					if offset > len(hello.sessionTicket) {
+						offset = len(hello.sessionTicket) - 1
+					}
+					hello.sessionTicket[offset] ^= 0x40
 				}
-				hello.sessionTicket[offset] ^= 0x40
 			}
-		}
-		// A random session ID is used to detect when the
-		// server accepted the ticket and is resuming a session
-		// (see RFC 5077).
-		sessionIdLen := 16
-		if c.config.Bugs.OversizedSessionId {
-			sessionIdLen = 33
-		}
-		hello.sessionId = make([]byte, sessionIdLen)
-		if _, err := io.ReadFull(c.config.rand(), hello.sessionId); err != nil {
-			c.sendAlert(alertInternalError)
-			return errors.New("tls: short read from Rand: " + err.Error())
+			// A random session ID is used to detect when the
+			// server accepted the ticket and is resuming a session
+			// (see RFC 5077).
+			sessionIdLen := 16
+			if c.config.Bugs.OversizedSessionId {
+				sessionIdLen = 33
+			}
+			hello.sessionId = make([]byte, sessionIdLen)
+			if _, err := io.ReadFull(c.config.rand(), hello.sessionId); err != nil {
+				c.sendAlert(alertInternalError)
+				return errors.New("tls: short read from Rand: " + err.Error())
+			}
+		} else {
+			hello.sessionId = session.sessionId
 		}
 	}
 
@@ -730,11 +733,26 @@
 }
 
 func (hs *clientHandshakeState) readSessionTicket() error {
+	c := hs.c
+
+	// Create a session with no server identifier. Either a
+	// session ID or session ticket will be attached.
+	session := &ClientSessionState{
+		vers:               c.vers,
+		cipherSuite:        hs.suite.id,
+		masterSecret:       hs.masterSecret,
+		handshakeHash:      hs.finishedHash.server.Sum(nil),
+		serverCertificates: c.peerCertificates,
+	}
+
 	if !hs.serverHello.ticketSupported {
+		if hs.session == nil && len(hs.serverHello.sessionId) > 0 {
+			session.sessionId = hs.serverHello.sessionId
+			hs.session = session
+		}
 		return nil
 	}
 
-	c := hs.c
 	msg, err := c.readHandshake()
 	if err != nil {
 		return err
@@ -745,14 +763,8 @@
 		return unexpectedMessageError(sessionTicketMsg, msg)
 	}
 
-	hs.session = &ClientSessionState{
-		sessionTicket:      sessionTicketMsg.ticket,
-		vers:               c.vers,
-		cipherSuite:        hs.suite.id,
-		masterSecret:       hs.masterSecret,
-		handshakeHash:      hs.finishedHash.server.Sum(nil),
-		serverCertificates: c.peerCertificates,
-	}
+	session.sessionTicket = sessionTicketMsg.ticket
+	hs.session = session
 
 	hs.writeServerHash(sessionTicketMsg.marshal())
 
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 32814d3..b087b46 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -335,13 +335,25 @@
 func (hs *serverHandshakeState) checkForResumption() bool {
 	c := hs.c
 
-	if c.config.SessionTicketsDisabled {
-		return false
-	}
+	if len(hs.clientHello.sessionTicket) > 0 {
+		if c.config.SessionTicketsDisabled {
+			return false
+		}
 
-	var ok bool
-	if hs.sessionState, ok = c.decryptTicket(hs.clientHello.sessionTicket); !ok {
-		return false
+		var ok bool
+		if hs.sessionState, ok = c.decryptTicket(hs.clientHello.sessionTicket); !ok {
+			return false
+		}
+	} else {
+		if c.config.ServerSessionCache == nil {
+			return false
+		}
+
+		var ok bool
+		sessionId := string(hs.clientHello.sessionId)
+		if hs.sessionState, ok = c.config.ServerSessionCache.Get(sessionId); !ok {
+			return false
+		}
 	}
 
 	// Never resume a session for a different SSL version.
@@ -416,10 +428,19 @@
 		hs.hello.ocspStapling = true
 	}
 
-	hs.hello.ticketSupported = hs.clientHello.ticketSupported && !config.SessionTicketsDisabled
+	hs.hello.ticketSupported = hs.clientHello.ticketSupported && !config.SessionTicketsDisabled && c.vers > VersionSSL30
 	hs.hello.cipherSuite = hs.suite.id
 	c.extendedMasterSecret = hs.hello.extendedMasterSecret
 
+	// Generate a session ID if we're to save the session.
+	if !hs.hello.ticketSupported && config.ServerSessionCache != nil {
+		hs.hello.sessionId = make([]byte, 32)
+		if _, err := io.ReadFull(config.rand(), hs.hello.sessionId); err != nil {
+			c.sendAlert(alertInternalError)
+			return errors.New("tls: short read from Rand: " + err.Error())
+		}
+	}
+
 	hs.finishedHash = newFinishedHash(c.vers, hs.suite)
 	hs.writeClientHash(hs.clientHello.marshal())
 	hs.writeServerHash(hs.hello.marshal())
@@ -733,14 +754,7 @@
 }
 
 func (hs *serverHandshakeState) sendSessionTicket() error {
-	if !hs.hello.ticketSupported || hs.c.config.Bugs.SkipNewSessionTicket {
-		return nil
-	}
-
 	c := hs.c
-	m := new(newSessionTicketMsg)
-
-	var err error
 	state := sessionState{
 		vers:          c.vers,
 		cipherSuite:   hs.suite.id,
@@ -748,6 +762,17 @@
 		certificates:  hs.certsFromClient,
 		handshakeHash: hs.finishedHash.server.Sum(nil),
 	}
+
+	if !hs.hello.ticketSupported || hs.c.config.Bugs.SkipNewSessionTicket {
+		if c.config.ServerSessionCache != nil && len(hs.hello.sessionId) != 0 {
+			c.config.ServerSessionCache.Put(string(hs.hello.sessionId), &state)
+		}
+		return nil
+	}
+
+	m := new(newSessionTicketMsg)
+
+	var err error
 	m.ticket, err = c.encryptTicket(&state)
 	if err != nil {
 		return err
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index cf1b1f9..1ee6362 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -148,10 +148,14 @@
 	// which attempts to resume the first session.
 	resumeSession bool
 	// resumeConfig, if not nil, points to a Config to be used on
-	// resumption. SessionTicketKey and ClientSessionCache are copied from
-	// the initial connection's config. If nil, the initial connection's
-	// config is used.
+	// 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
 	// sendPrefix sends a prefix on the socket before actually performing a
 	// handshake.
 	sendPrefix string
@@ -787,6 +791,7 @@
 
 	config := test.config
 	config.ClientSessionCache = NewLRUClientSessionCache(1)
+	config.ServerSessionCache = NewLRUServerSessionCache(1)
 	if test.testType == clientTest {
 		if len(config.Certificates) == 0 {
 			config.Certificates = []Certificate{getRSACertificate()}
@@ -814,8 +819,11 @@
 			if len(resumeConfig.Certificates) == 0 {
 				resumeConfig.Certificates = []Certificate{getRSACertificate()}
 			}
-			resumeConfig.SessionTicketKey = config.SessionTicketKey
-			resumeConfig.ClientSessionCache = config.ClientSessionCache
+			if !test.newSessionsOnResume {
+				resumeConfig.SessionTicketKey = config.SessionTicketKey
+				resumeConfig.ClientSessionCache = config.ClientSessionCache
+				resumeConfig.ServerSessionCache = config.ServerSessionCache
+			}
 		} else {
 			resumeConfig = config
 		}
@@ -957,11 +965,6 @@
 				continue
 			}
 
-			// Go's TLS implementation only implements session
-			// resumption with tickets, so SSLv3 cannot resume
-			// sessions.
-			resumeSession := ver.version != VersionSSL30
-
 			testCases = append(testCases, testCase{
 				testType: clientTest,
 				name:     ver.name + "-" + suite.name + "-client",
@@ -974,7 +977,7 @@
 					PreSharedKeyIdentity: pskIdentity,
 				},
 				flags:         flags,
-				resumeSession: resumeSession,
+				resumeSession: true,
 			})
 
 			testCases = append(testCases, testCase{
@@ -991,7 +994,7 @@
 				certFile:      certFile,
 				keyFile:       keyFile,
 				flags:         flags,
-				resumeSession: resumeSession,
+				resumeSession: true,
 			})
 
 			// TODO(davidben): Fix DTLS 1.2 support and test that.
@@ -1009,7 +1012,7 @@
 						PreSharedKeyIdentity: pskIdentity,
 					},
 					flags:         flags,
-					resumeSession: resumeSession,
+					resumeSession: true,
 				})
 				testCases = append(testCases, testCase{
 					testType: serverTest,
@@ -1026,7 +1029,7 @@
 					certFile:      certFile,
 					keyFile:       keyFile,
 					flags:         flags,
-					resumeSession: resumeSession,
+					resumeSession: true,
 				})
 			}
 		}
@@ -1265,7 +1268,8 @@
 		maxHandshakeRecordLength = 1
 	}
 
-	// Basic handshake, with resumption. Client and server.
+	// Basic handshake, with resumption. Client and server,
+	// session ID and session ticket.
 	testCases = append(testCases, testCase{
 		protocol: protocol,
 		name:     "Basic-Client" + suffix,
@@ -1291,6 +1295,18 @@
 	})
 	testCases = append(testCases, testCase{
 		protocol: protocol,
+		name:     "Basic-Client-NoTicket" + suffix,
+		config: Config{
+			SessionTicketsDisabled: true,
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags:         flags,
+		resumeSession: true,
+	})
+	testCases = append(testCases, testCase{
+		protocol: protocol,
 		testType: serverTest,
 		name:     "Basic-Server" + suffix,
 		config: Config{
@@ -1301,6 +1317,19 @@
 		flags:         flags,
 		resumeSession: true,
 	})
+	testCases = append(testCases, testCase{
+		protocol: protocol,
+		testType: serverTest,
+		name:     "Basic-Server-NoTickets" + suffix,
+		config: Config{
+			SessionTicketsDisabled: true,
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags:         flags,
+		resumeSession: true,
+	})
 
 	// TLS client auth.
 	testCases = append(testCases, testCase{
@@ -1854,15 +1883,7 @@
 func addResumptionVersionTests() {
 	// TODO(davidben): Once DTLS 1.2 is working, test that as well.
 	for _, sessionVers := range tlsVersions {
-		// TODO(davidben): SSLv3 is omitted here because runner does not
-		// support resumption with session IDs.
-		if sessionVers.version == VersionSSL30 {
-			continue
-		}
 		for _, resumeVers := range tlsVersions {
-			if resumeVers.version == VersionSSL30 {
-				continue
-			}
 			suffix := "-" + sessionVers.name + "-" + resumeVers.name
 
 			testCases = append(testCases, testCase{
@@ -1896,10 +1917,10 @@
 				},
 				expectedVersion: sessionVers.version,
 				resumeConfig: &Config{
-					MaxVersion:             resumeVers.version,
-					CipherSuites:           []uint16{TLS_RSA_WITH_RC4_128_SHA},
-					SessionTicketsDisabled: true,
+					MaxVersion:   resumeVers.version,
+					CipherSuites: []uint16{TLS_RSA_WITH_RC4_128_SHA},
 				},
+				newSessionsOnResume:   true,
 				expectedResumeVersion: resumeVers.version,
 			})