Implement Token Binding

Update-Note: Token Binding can no longer be configured with the custom
  extensions API. Instead, use the new built-in implementation. (The
  internal repository should be all set.)

Bug: 183

Change-Id: I007523a638dc99582ebd1d177c38619fa7e1ac38
Reviewed-on: https://boringssl-review.googlesource.com/20645
Commit-Queue: David Benjamin <davidben@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 58c9115..eadb25d 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -86,6 +86,7 @@
 SSL,168,MIXED_SPECIAL_OPERATOR_WITH_GROUPS
 SSL,169,MTU_TOO_SMALL
 SSL,170,NEGOTIATED_BOTH_NPN_AND_ALPN
+SSL,285,NEGOTIATED_TB_WITHOUT_EMS_OR_RI
 SSL,171,NESTED_GROUP
 SSL,172,NO_CERTIFICATES_RETURNED
 SSL,173,NO_CERTIFICATE_ASSIGNED
diff --git a/include/openssl/base.h b/include/openssl/base.h
index 9edaa5c..dea069a 100644
--- a/include/openssl/base.h
+++ b/include/openssl/base.h
@@ -151,7 +151,7 @@
 // A consumer may use this symbol in the preprocessor to temporarily build
 // against multiple revisions of BoringSSL at the same time. It is not
 // recommended to do so for longer than is necessary.
-#define BORINGSSL_API_VERSION 6
+#define BORINGSSL_API_VERSION 7
 
 #if defined(BORINGSSL_SHARED_LIBRARY)
 
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index b868d3f..7ae8276 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -2785,6 +2785,33 @@
     SSL *ssl, EVP_PKEY **out_pkey);
 
 
+// Token Binding.
+//
+// See draft-ietf-tokbind-protocol-16.
+
+// SSL_set_token_binding_params sets |params| as the Token Binding Key
+// parameters (section 3 of draft-ietf-tokbind-protocol-16) to negotiate on the
+// connection. If this function is not called, or if |len| is 0, then this
+// endpoint will not attempt to negotiate Token Binding. |params| are provided
+// in preference order, with the more preferred parameters at the beginning of
+// the list. This function returns 1 on success and 0 on failure.
+OPENSSL_EXPORT int SSL_set_token_binding_params(SSL *ssl, const uint8_t *params,
+                                                size_t len);
+
+// SSL_is_token_binding_negotiated returns 1 if Token Binding was negotiated
+// on this connection and 0 otherwise. On a server, it is possible for this
+// function to return 1 when the client's view of the connection is that Token
+// Binding was not negotiated. This occurs when the server indicates a version
+// of Token Binding less than the client's minimum version.
+OPENSSL_EXPORT int SSL_is_token_binding_negotiated(const SSL *ssl);
+
+// SSL_get_negotiated_token_binding_param returns the TokenBindingKeyParameters
+// enum value that was negotiated. It is only valid to call this function if
+// SSL_is_token_binding_negotiated returned 1, otherwise this function returns
+// an undefined value.
+OPENSSL_EXPORT uint8_t SSL_get_negotiated_token_binding_param(const SSL *ssl);
+
+
 // DTLS-SRTP.
 //
 // See RFC 5764.
@@ -4588,6 +4615,7 @@
 #define SSL_R_EMPTY_HELLO_RETRY_REQUEST 282
 #define SSL_R_EARLY_DATA_NOT_IN_USE 283
 #define SSL_R_HANDSHAKE_NOT_COMPLETE 284
+#define SSL_R_NEGOTIATED_TB_WITHOUT_EMS_OR_RI 285
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 4b5d226..682bb9b 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -202,6 +202,9 @@
 // ExtensionType value from RFC7627
 #define TLSEXT_TYPE_extended_master_secret 23
 
+// ExtensionType value from draft-ietf-tokbind-negotiation-10
+#define TLSEXT_TYPE_token_binding 24
+
 // ExtensionType value from RFC4507
 #define TLSEXT_TYPE_session_ticket 35
 
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 4c7012b..4e2158e 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -757,6 +757,13 @@
     return ssl_hs_error;
   }
 
+  if (ssl->token_binding_negotiated &&
+      (!hs->extended_master_secret || !ssl->s3->send_connection_binding)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_NEGOTIATED_TB_WITHOUT_EMS_OR_RI);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNSUPPORTED_EXTENSION);
+    return ssl_hs_error;
+  }
+
   ssl->method->next_message(ssl);
 
   if (ssl->session != NULL) {
diff --git a/ssl/internal.h b/ssl/internal.h
index 91ca1f7..fb99101 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1376,6 +1376,12 @@
   // peer_key is the peer's ECDH key for a TLS 1.2 client.
   Array<uint8_t> peer_key;
 
+  // negotiated_token_binding_version is used by a server to store the
+  // on-the-wire encoding of the Token Binding protocol version to advertise in
+  // the ServerHello/EncryptedExtensions if the Token Binding extension is to be
+  // sent.
+  uint16_t negotiated_token_binding_version;
+
   // server_params, in a TLS 1.2 server, stores the ServerKeyExchange
   // parameters. It has client and server randoms prepended for signing
   // convenience.
@@ -2606,6 +2612,14 @@
   uint8_t *alpn_client_proto_list;
   unsigned alpn_client_proto_list_len;
 
+  // Contains a list of supported Token Binding key parameters.
+  uint8_t *token_binding_params;
+  size_t token_binding_params_len;
+
+  // The negotiated Token Binding key parameter. Only valid if
+  // |token_binding_negotiated| is set.
+  uint8_t negotiated_token_binding_param;
+
   // renegotiate_mode controls how peer renegotiation attempts are handled.
   enum ssl_renegotiate_mode_t renegotiate_mode;
 
@@ -2633,6 +2647,9 @@
   // we'll advertise support.
   bool tlsext_channel_id_enabled:1;
 
+  // token_binding_negotiated is set if Token Binding was negotiated.
+  bool token_binding_negotiated:1;
+
   // retain_only_sha256_of_client_certs is true if we should compute the SHA256
   // hash of the peer's certificate and then discard it to save memory and
   // session space. Only effective on the server side.
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 8f53dcd..75e438d 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -771,6 +771,7 @@
   SSL_CTX_free(ssl->session_ctx);
   OPENSSL_free(ssl->supported_group_list);
   OPENSSL_free(ssl->alpn_client_proto_list);
+  OPENSSL_free(ssl->token_binding_params);
   EVP_PKEY_free(ssl->tlsext_channel_id_private);
   OPENSSL_free(ssl->psk_identity_hint);
   sk_CRYPTO_BUFFER_pop_free(ssl->client_CA, CRYPTO_BUFFER_free);
@@ -2122,6 +2123,28 @@
   return 64;
 }
 
+int SSL_set_token_binding_params(SSL *ssl, const uint8_t *params, size_t len) {
+  if (len > 256) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_OVERFLOW);
+    return 0;
+  }
+  OPENSSL_free(ssl->token_binding_params);
+  ssl->token_binding_params = (uint8_t *)BUF_memdup(params, len);
+  if (!ssl->token_binding_params) {
+    return 0;
+  }
+  ssl->token_binding_params_len = len;
+  return 1;
+}
+
+int SSL_is_token_binding_negotiated(const SSL *ssl) {
+  return ssl->token_binding_negotiated;
+}
+
+uint8_t SSL_get_negotiated_token_binding_param(const SSL *ssl) {
+  return ssl->negotiated_token_binding_param;
+}
+
 size_t SSL_get0_certificate_types(SSL *ssl, const uint8_t **out_types) {
   if (ssl->server || ssl->s3->hs == NULL) {
     *out_types = NULL;
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index a8833e0..dafb885 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -2439,6 +2439,153 @@
   return true;
 }
 
+// Token Binding
+//
+// https://tools.ietf.org/html/draft-ietf-tokbind-negotiation-10
+
+// The Token Binding version number currently matches the draft number of
+// draft-ietf-tokbind-protocol, and when published as an RFC it will be 0x0100.
+// Since there are no wire changes to the protocol from draft 13 through the
+// current draft (16), this implementation supports all versions in that range.
+static uint16_t kTokenBindingMaxVersion = 16;
+static uint16_t kTokenBindingMinVersion = 13;
+
+static bool ext_token_binding_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
+  SSL *const ssl = hs->ssl;
+  if (ssl->token_binding_params == nullptr || SSL_is_dtls(ssl)) {
+    return true;
+  }
+
+  CBB contents, params;
+  if (!CBB_add_u16(out, TLSEXT_TYPE_token_binding) ||
+      !CBB_add_u16_length_prefixed(out, &contents) ||
+      !CBB_add_u16(&contents, kTokenBindingMaxVersion) ||
+      !CBB_add_u8_length_prefixed(&contents, &params) ||
+      !CBB_add_bytes(&params, ssl->token_binding_params,
+                     ssl->token_binding_params_len) ||
+      !CBB_flush(out)) {
+    return false;
+  }
+
+  return true;
+}
+
+static bool ext_token_binding_parse_serverhello(SSL_HANDSHAKE *hs,
+                                                uint8_t *out_alert,
+                                                CBS *contents) {
+  SSL *const ssl = hs->ssl;
+  if (contents == nullptr) {
+    return true;
+  }
+
+  CBS params_list;
+  uint16_t version;
+  uint8_t param;
+  if (!CBS_get_u16(contents, &version) ||
+      !CBS_get_u8_length_prefixed(contents, &params_list) ||
+      !CBS_get_u8(&params_list, &param) ||
+      CBS_len(&params_list) > 0 ||
+      CBS_len(contents) > 0) {
+    *out_alert = SSL_AD_DECODE_ERROR;
+    return false;
+  }
+
+  // The server-negotiated version must be less than or equal to our version.
+  if (version > kTokenBindingMaxVersion) {
+    *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+    return false;
+  }
+
+  // If the server-selected version is less than what we support, then Token
+  // Binding wasn't negotiated (but the extension was parsed successfully).
+  if (version < kTokenBindingMinVersion) {
+    return true;
+  }
+
+  for (size_t i = 0; i < ssl->token_binding_params_len; ++i) {
+    if (param == ssl->token_binding_params[i]) {
+      ssl->negotiated_token_binding_param = param;
+      ssl->token_binding_negotiated = true;
+      return true;
+    }
+  }
+
+  *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+  return false;
+}
+
+// select_tb_param looks for the first token binding param in
+// |ssl->token_binding_params| that is also in |params| and puts it in
+// |ssl->negotiated_token_binding_param|. It returns true if a token binding
+// param is found, and false otherwise.
+static bool select_tb_param(SSL *ssl, Span<const uint8_t> peer_params) {
+  for (size_t i = 0; i < ssl->token_binding_params_len; ++i) {
+    uint8_t tb_param = ssl->token_binding_params[i];
+    for (uint8_t peer_param : peer_params) {
+      if (tb_param == peer_param) {
+        ssl->negotiated_token_binding_param = tb_param;
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+static bool ext_token_binding_parse_clienthello(SSL_HANDSHAKE *hs,
+                                                uint8_t *out_alert,
+                                                CBS *contents) {
+  SSL *const ssl = hs->ssl;
+  if (contents == nullptr || ssl->token_binding_params == nullptr) {
+    return true;
+  }
+
+  CBS params;
+  uint16_t version;
+  if (!CBS_get_u16(contents, &version) ||
+      !CBS_get_u8_length_prefixed(contents, &params) ||
+      CBS_len(&params) == 0 ||
+      CBS_len(contents) > 0) {
+    *out_alert = SSL_AD_DECODE_ERROR;
+    return false;
+  }
+
+  // If the client-selected version is less than what we support, then Token
+  // Binding wasn't negotiated (but the extension was parsed successfully).
+  if (version < kTokenBindingMinVersion) {
+    return true;
+  }
+
+  // If the client-selected version is higher than we support, use our max
+  // version. Otherwise, use the client's version.
+  hs->negotiated_token_binding_version =
+      std::min(version, kTokenBindingMaxVersion);
+  if (!select_tb_param(ssl, params)) {
+    return true;
+  }
+
+  ssl->token_binding_negotiated = true;
+  return true;
+}
+
+static bool ext_token_binding_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  SSL *const ssl = hs->ssl;
+
+  if (!ssl->token_binding_negotiated) {
+    return true;
+  }
+
+  CBB contents, params;
+  if (!CBB_add_u16(out, TLSEXT_TYPE_token_binding) ||
+      !CBB_add_u16_length_prefixed(out, &contents) ||
+      !CBB_add_u16(&contents, hs->negotiated_token_binding_version) ||
+      !CBB_add_u8_length_prefixed(&contents, &params) ||
+      !CBB_add_u8(&params, ssl->negotiated_token_binding_param) ||
+      !CBB_flush(out)) {
+    return false;
+  }
+
+  return true;
+}
 
 // kExtensions contains all the supported extensions.
 static const struct tls_extension kExtensions[] = {
@@ -2608,6 +2755,14 @@
     ext_supported_groups_parse_clienthello,
     dont_add_serverhello,
   },
+  {
+    TLSEXT_TYPE_token_binding,
+    NULL,
+    ext_token_binding_add_clienthello,
+    ext_token_binding_parse_serverhello,
+    ext_token_binding_parse_clienthello,
+    ext_token_binding_add_serverhello,
+  },
 };
 
 #define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))
@@ -2970,6 +3125,15 @@
 
 static int ssl_check_clienthello_tlsext(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+
+  if (ssl->token_binding_negotiated &&
+      !(SSL_get_secure_renegotiation_support(ssl) &&
+        SSL_get_extms_support(ssl))) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_NEGOTIATED_TB_WITHOUT_EMS_OR_RI);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNSUPPORTED_EXTENSION);
+    return -1;
+  }
+
   int ret = SSL_TLSEXT_ERR_NOACK;
   int al = SSL_AD_UNRECOGNIZED_NAME;
 
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 0123df7..50182b1 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -1725,6 +1725,18 @@
     }
   }
 
+  if (config->expected_token_binding_param != -1) {
+    if (!SSL_is_token_binding_negotiated(ssl)) {
+      fprintf(stderr, "no Token Binding negotiated\n");
+      return false;
+    }
+    if (SSL_get_negotiated_token_binding_param(ssl) !=
+        static_cast<uint8_t>(config->expected_token_binding_param)) {
+      fprintf(stderr, "Token Binding param mismatch\n");
+      return false;
+    }
+  }
+
   if (config->expect_extended_master_secret && !SSL_get_extms_support(ssl)) {
     fprintf(stderr, "No EMS for connection when expected\n");
     return false;
@@ -1970,6 +1982,12 @@
       }
     }
   }
+  if (!config->send_token_binding_params.empty()) {
+    SSL_set_token_binding_params(ssl.get(),
+                                 reinterpret_cast<const uint8_t *>(
+                                     config->send_token_binding_params.data()),
+                                 config->send_token_binding_params.length());
+  }
   if (!config->host_name.empty() &&
       !SSL_set_tlsext_host_name(ssl.get(), config->host_name.c_str())) {
     return false;
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 15e42ff..a543e81 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -122,6 +122,7 @@
 	extensionSignedCertificateTimestamp uint16 = 18
 	extensionPadding                    uint16 = 21
 	extensionExtendedMasterSecret       uint16 = 23
+	extensionTokenBinding               uint16 = 24
 	extensionSessionTicket              uint16 = 35
 	extensionOldKeyShare                uint16 = 40    // draft-ietf-tls-tls13-16
 	extensionPreSharedKey               uint16 = 41    // draft-ietf-tls-tls13-16
@@ -261,6 +262,8 @@
 	PeerCertificates           []*x509.Certificate   // certificate chain presented by remote peer
 	VerifiedChains             [][]*x509.Certificate // verified chains built from PeerCertificates
 	ChannelID                  *ecdsa.PublicKey      // the channel ID for this connection
+	TokenBindingNegotiated     bool                  // whether Token Binding was negotiated
+	TokenBindingParam          uint8                 // the negotiated Token Binding key parameter
 	SRTPProtectionProfile      uint16                // the negotiated DTLS-SRTP protection profile
 	TLSUnique                  []byte                // the tls-unique channel binding
 	SCTList                    []byte                // signed certificate timestamp list
@@ -453,6 +456,20 @@
 	// returned in the ConnectionState.
 	RequestChannelID bool
 
+	// TokenBindingParams contains a list of TokenBindingKeyParameters
+	// (draft-ietf-tokbind-protocol-16) to attempt to negotiate. If
+	// nil, Token Binding will not be negotiated.
+	TokenBindingParams []byte
+
+	// TokenBindingVersion contains the serialized ProtocolVersion to
+	// use when negotiating Token Binding.
+	TokenBindingVersion uint16
+
+	// ExpectTokenBindingParams is checked by a server that the client
+	// sent ExpectTokenBindingParams as its list of Token Binding
+	// paramters.
+	ExpectTokenBindingParams []byte
+
 	// PreSharedKey, if not nil, is the pre-shared key to use with
 	// the PSK cipher suites.
 	PreSharedKey []byte
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 6493aa7..c38fba9 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -78,6 +78,9 @@
 
 	channelID *ecdsa.PublicKey
 
+	tokenBindingNegotiated bool
+	tokenBindingParam      uint8
+
 	srtpProtectionProfile uint16
 
 	clientVersion uint16
@@ -1812,6 +1815,8 @@
 		state.VerifiedChains = c.verifiedChains
 		state.ServerName = c.serverName
 		state.ChannelID = c.channelID
+		state.TokenBindingNegotiated = c.tokenBindingNegotiated
+		state.TokenBindingParam = c.tokenBindingParam
 		state.SRTPProtectionProfile = c.srtpProtectionProfile
 		state.TLSUnique = c.firstFinished[:]
 		state.SCTList = c.sctList
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index e477edf..ab41e12 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -89,6 +89,8 @@
 		alpnProtocols:           c.config.NextProtos,
 		duplicateExtension:      c.config.Bugs.DuplicateExtension,
 		channelIDSupported:      c.config.ChannelID != nil,
+		tokenBindingParams:      c.config.TokenBindingParams,
+		tokenBindingVersion:     c.config.TokenBindingVersion,
 		npnAfterAlpn:            c.config.Bugs.SwapNPNAndALPN,
 		extendedMasterSecret:    maxVersion >= VersionTLS10,
 		srtpProtectionProfiles:  c.config.SRTPProtectionProfiles,
@@ -1451,6 +1453,29 @@
 		return errors.New("server advertised unrequested Channel ID extension")
 	}
 
+	if len(serverExtensions.tokenBindingParams) == 1 {
+		found := false
+		for _, p := range c.config.TokenBindingParams {
+			if p == serverExtensions.tokenBindingParams[0] {
+				c.tokenBindingParam = p
+				found = true
+				break
+			}
+		}
+		if !found {
+			return errors.New("tls: server advertised unsupported Token Binding key param")
+		}
+		if serverExtensions.tokenBindingVersion > c.config.TokenBindingVersion {
+			return errors.New("tls: server's Token Binding version is too new")
+		}
+		if c.vers < VersionTLS13 {
+			if !serverExtensions.extendedMasterSecret || serverExtensions.secureRenegotiation == nil {
+				return errors.New("server sent Token Binding without EMS or RI")
+			}
+		}
+		c.tokenBindingNegotiated = true
+	}
+
 	if serverExtensions.extendedMasterSecret && c.vers >= VersionTLS13 {
 		return errors.New("tls: server advertised extended master secret over TLS 1.3")
 	}
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 16d858f..c80b1cf 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -281,6 +281,8 @@
 	alpnProtocols           []string
 	duplicateExtension      bool
 	channelIDSupported      bool
+	tokenBindingParams      []byte
+	tokenBindingVersion     uint16
 	npnAfterAlpn            bool
 	extendedMasterSecret    bool
 	srtpProtectionProfiles  []uint16
@@ -331,6 +333,8 @@
 		eqStrings(m.alpnProtocols, m1.alpnProtocols) &&
 		m.duplicateExtension == m1.duplicateExtension &&
 		m.channelIDSupported == m1.channelIDSupported &&
+		bytes.Equal(m.tokenBindingParams, m1.tokenBindingParams) &&
+		m.tokenBindingVersion == m1.tokenBindingVersion &&
 		m.npnAfterAlpn == m1.npnAfterAlpn &&
 		m.extendedMasterSecret == m1.extendedMasterSecret &&
 		eqUint16s(m.srtpProtectionProfiles, m1.srtpProtectionProfiles) &&
@@ -519,6 +523,13 @@
 		extensions.addU16(extensionChannelID)
 		extensions.addU16(0) // Length is always 0
 	}
+	if m.tokenBindingParams != nil {
+		extensions.addU16(extensionTokenBinding)
+		tokbindExtension := extensions.addU16LengthPrefixed()
+		tokbindExtension.addU16(m.tokenBindingVersion)
+		tokbindParams := tokbindExtension.addU8LengthPrefixed()
+		tokbindParams.addBytes(m.tokenBindingParams)
+	}
 	if m.nextProtoNeg && m.npnAfterAlpn {
 		extensions.addU16(extensionNextProtoNeg)
 		extensions.addU16(0) // Length is always 0
@@ -826,6 +837,12 @@
 				return false
 			}
 			m.channelIDSupported = true
+		case extensionTokenBinding:
+			if !body.readU16(&m.tokenBindingVersion) ||
+				!body.readU8LengthPrefixedBytes(&m.tokenBindingParams) ||
+				len(body) != 0 {
+				return false
+			}
 		case extensionExtendedMasterSecret:
 			if len(body) != 0 {
 				return false
@@ -1116,6 +1133,8 @@
 	alpnProtocolEmpty       bool
 	duplicateExtension      bool
 	channelIDRequested      bool
+	tokenBindingParams      []byte
+	tokenBindingVersion     uint16
 	extendedMasterSecret    bool
 	srtpProtectionProfile   uint16
 	srtpMasterKeyIdentifier string
@@ -1175,6 +1194,13 @@
 		extensions.addU16(extensionChannelID)
 		extensions.addU16(0)
 	}
+	if m.tokenBindingParams != nil {
+		extensions.addU16(extensionTokenBinding)
+		tokbindExtension := extensions.addU16LengthPrefixed()
+		tokbindExtension.addU16(m.tokenBindingVersion)
+		tokbindParams := tokbindExtension.addU8LengthPrefixed()
+		tokbindParams.addBytes(m.tokenBindingParams)
+	}
 	if m.duplicateExtension {
 		// Add a duplicate bogus extension at the beginning and end.
 		extensions.addU16(0xffff)
@@ -1303,6 +1329,13 @@
 				return false
 			}
 			m.channelIDRequested = true
+		case extensionTokenBinding:
+			if !body.readU16(&m.tokenBindingVersion) ||
+				!body.readU8LengthPrefixedBytes(&m.tokenBindingParams) ||
+				len(m.tokenBindingParams) != 1 ||
+				len(body) != 0 {
+				return false
+			}
 		case extensionExtendedMasterSecret:
 			if len(body) != 0 {
 				return false
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index d317d01..dc74259 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -1381,6 +1381,21 @@
 		serverExtensions.channelIDRequested = true
 	}
 
+	if config.TokenBindingParams != nil {
+		if !bytes.Equal(config.ExpectTokenBindingParams, hs.clientHello.tokenBindingParams) {
+			return errors.New("client did not send expected token binding params")
+		}
+
+		// For testing, blindly send whatever is set in config, even if
+		// it is invalid.
+		serverExtensions.tokenBindingParams = config.TokenBindingParams
+		serverExtensions.tokenBindingVersion = config.TokenBindingVersion
+	}
+
+	if len(hs.clientHello.tokenBindingParams) > 0 && (!hs.clientHello.extendedMasterSecret || hs.clientHello.secureRenegotiation == nil) {
+		return errors.New("client sent Token Binding without EMS and/or RI")
+	}
+
 	if hs.clientHello.srtpProtectionProfiles != nil {
 	SRTPLoop:
 		for _, p1 := range c.config.SRTPProtectionProfiles {
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 1f2a20a..9255012 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -343,6 +343,12 @@
 	// expectChannelID controls whether the connection should have
 	// negotiated a Channel ID with channelIDKey.
 	expectChannelID bool
+	// expectTokenBinding controls whether the connection should have
+	// negotiated Token Binding.
+	expectTokenBinding bool
+	// expectedTokenBindingParam is the Token Binding parameter that should
+	// have been negotiated (if expectTokenBinding is true).
+	expectedTokenBindingParam uint8
 	// expectedNextProto controls whether the connection should
 	// negotiate a next protocol via NPN or ALPN.
 	expectedNextProto string
@@ -648,6 +654,17 @@
 		return fmt.Errorf("channel ID unexpectedly negotiated")
 	}
 
+	if test.expectTokenBinding {
+		if !connState.TokenBindingNegotiated {
+			return errors.New("no Token Binding negotiated")
+		}
+		if connState.TokenBindingParam != test.expectedTokenBindingParam {
+			return fmt.Errorf("expected param %02x, but got %02x", test.expectedTokenBindingParam, connState.TokenBindingParam)
+		}
+	} else if connState.TokenBindingNegotiated {
+		return errors.New("Token Binding unexpectedly negotiated")
+	}
+
 	if expected := test.expectedNextProto; expected != "" {
 		if actual := connState.NegotiatedProtocol; actual != expected {
 			return fmt.Errorf("next proto mismatch: got %s, wanted %s", actual, expected)
@@ -6149,6 +6166,404 @@
 			})
 		}
 
+		// Test Token Binding.
+
+		const maxTokenBindingVersion = 16
+		const minTokenBindingVersion = 13
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{0, 1, 2},
+				TokenBindingVersion: maxTokenBindingVersion,
+			},
+			expectTokenBinding: true,
+			expectedTokenBindingParam: 2,
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				"-expected-token-binding-param",
+				"2",
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-UnsupportedParam-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{3},
+				TokenBindingVersion: maxTokenBindingVersion,
+			},
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-OldVersion-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{0, 1, 2},
+				TokenBindingVersion: minTokenBindingVersion - 1,
+			},
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-NewVersion-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{0, 1, 2},
+				TokenBindingVersion: maxTokenBindingVersion + 1,
+			},
+			expectTokenBinding: true,
+			expectedTokenBindingParam: 2,
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				"-expected-token-binding-param",
+				"2",
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-NoParams-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{},
+				TokenBindingVersion: maxTokenBindingVersion,
+			},
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+			},
+			shouldFail: true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     "TokenBinding-Server-RepeatedParam" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{0, 1, 2, 2},
+				TokenBindingVersion: maxTokenBindingVersion,
+			},
+			expectTokenBinding: true,
+			expectedTokenBindingParam: 2,
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				"-expected-token-binding-param",
+				"2",
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2},
+				TokenBindingVersion: maxTokenBindingVersion,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			tls13Variant: ver.tls13Variant,
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+				"-expected-token-binding-param",
+				"2",
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-Unexpected-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2},
+				TokenBindingVersion: maxTokenBindingVersion,
+			},
+			tls13Variant: ver.tls13Variant,
+			shouldFail: true,
+			expectedError: ":UNEXPECTED_EXTENSION:",
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-ExtraParams-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2, 1},
+				TokenBindingVersion: maxTokenBindingVersion,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+				"-expected-token-binding-param",
+				"2",
+			},
+			tls13Variant: ver.tls13Variant,
+			shouldFail: true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-NoParams-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{},
+				TokenBindingVersion: maxTokenBindingVersion,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+				"-expected-token-binding-param",
+				"2",
+			},
+			tls13Variant: ver.tls13Variant,
+			shouldFail: true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-WrongParam-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{3},
+				TokenBindingVersion: maxTokenBindingVersion,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+				"-expected-token-binding-param",
+				"2",
+			},
+			tls13Variant: ver.tls13Variant,
+			shouldFail: true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-OldVersion-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2},
+				TokenBindingVersion: minTokenBindingVersion - 1,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+			},
+			tls13Variant: ver.tls13Variant,
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-MinVersion-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2},
+				TokenBindingVersion: minTokenBindingVersion,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+				"-expected-token-binding-param",
+				"2",
+			},
+			tls13Variant: ver.tls13Variant,
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     "TokenBinding-Client-VersionTooNew-" + ver.name,
+
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				TokenBindingParams: []byte{2},
+				TokenBindingVersion: maxTokenBindingVersion + 1,
+				ExpectTokenBindingParams: []byte{0, 1, 2},
+			},
+			flags: []string{
+				"-token-binding-params",
+				base64.StdEncoding.EncodeToString([]byte{0, 1, 2}),
+			},
+			tls13Variant: ver.tls13Variant,
+			shouldFail: true,
+			expectedError: "ERROR_PARSING_EXTENSION",
+		})
+		if ver.version < VersionTLS13 {
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				name:     "TokenBinding-Client-NoEMS-" + ver.name,
+
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					ExpectTokenBindingParams: []byte{2, 1, 0},
+					Bugs: ProtocolBugs{
+						NoExtendedMasterSecret: true,
+					},
+				},
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+				shouldFail: true,
+				expectedError: ":NEGOTIATED_TB_WITHOUT_EMS_OR_RI:",
+			})
+			testCases = append(testCases, testCase{
+				testType: serverTest,
+				name:     "TokenBinding-Server-NoEMS-" + ver.name,
+
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{0, 1, 2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					Bugs: ProtocolBugs{
+						NoExtendedMasterSecret: true,
+					},
+				},
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+				shouldFail: true,
+				expectedError: ":NEGOTIATED_TB_WITHOUT_EMS_OR_RI:",
+			})
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				name:     "TokenBinding-Client-NoRI-" + ver.name,
+
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					ExpectTokenBindingParams: []byte{2, 1, 0},
+					Bugs: ProtocolBugs{
+						NoRenegotiationInfo: true,
+					},
+				},
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+				shouldFail: true,
+				expectedError: ":NEGOTIATED_TB_WITHOUT_EMS_OR_RI:",
+			})
+			testCases = append(testCases, testCase{
+				testType: serverTest,
+				name:     "TokenBinding-Server-NoRI-" + ver.name,
+
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{0, 1, 2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					Bugs: ProtocolBugs{
+						NoRenegotiationInfo: true,
+					},
+				},
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+				shouldFail:    true,
+				expectedError: ":NEGOTIATED_TB_WITHOUT_EMS_OR_RI:",
+			})
+		} else {
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				name:     "TokenBinding-WithEarlyDataFails-" + ver.name,
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					ExpectTokenBindingParams: []byte{2, 1, 0},
+					MaxEarlyDataSize: 16384,
+				},
+				resumeSession: true,
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-enable-early-data",
+					"-expect-ticket-supports-early-data",
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+				shouldFail: true,
+				expectedError: ":UNEXPECTED_EXTENSION_ON_EARLY_DATA:",
+			})
+			testCases = append(testCases, testCase{
+				testType: serverTest,
+				name:     "TokenBinding-EarlyDataRejected-" + ver.name,
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					TokenBindingParams: []byte{0, 1, 2},
+					TokenBindingVersion: maxTokenBindingVersion,
+					MaxEarlyDataSize: 16384,
+				},
+				resumeSession: true,
+				expectTokenBinding: true,
+				expectedTokenBindingParam: 2,
+				tls13Variant: ver.tls13Variant,
+				flags: []string{
+					"-enable-early-data",
+					"-expect-ticket-supports-early-data",
+					"-token-binding-params",
+					base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
+				},
+			})
+		}
+
 		// Test ticket behavior.
 
 		// Resume with a corrupt ticket.
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 0d11f90..7c660c8 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -163,6 +163,7 @@
 const Flag<std::string> kBase64Flags[] = {
   { "-expect-certificate-types", &TestConfig::expected_certificate_types },
   { "-expect-channel-id", &TestConfig::expected_channel_id },
+  { "-token-binding-params", &TestConfig::send_token_binding_params },
   { "-expect-ocsp-response", &TestConfig::expected_ocsp_response },
   { "-expect-signed-cert-timestamps",
     &TestConfig::expected_signed_cert_timestamps },
@@ -174,6 +175,8 @@
 const Flag<int> kIntFlags[] = {
   { "-port", &TestConfig::port },
   { "-resume-count", &TestConfig::resume_count },
+  { "-expected-token-binding-param",
+    &TestConfig::expected_token_binding_param },
   { "-min-version", &TestConfig::min_version },
   { "-max-version", &TestConfig::max_version },
   { "-expect-version", &TestConfig::expect_version },
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index fe107bc..7574337 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -49,6 +49,8 @@
   std::string expected_channel_id;
   bool enable_channel_id = false;
   std::string send_channel_id;
+  int expected_token_binding_param = -1;
+  std::string send_token_binding_params;
   bool shim_writes_first = false;
   std::string host_name;
   std::string advertise_alpn;
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index f437519..f013afd 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -476,7 +476,8 @@
       OPENSSL_PUT_ERROR(SSL, SSL_R_ALPN_MISMATCH_ON_EARLY_DATA);
       return ssl_hs_error;
     }
-    if (ssl->s3->tlsext_channel_id_valid || hs->received_custom_extension) {
+    if (ssl->s3->tlsext_channel_id_valid || hs->received_custom_extension ||
+        ssl->token_binding_negotiated) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION_ON_EARLY_DATA);
       return ssl_hs_error;
     }
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 72273da..331e0d5 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -398,6 +398,8 @@
           hs->early_data_offered &&
           // Channel ID is incompatible with 0-RTT.
           !ssl->s3->tlsext_channel_id_valid &&
+          // If Token Binding is negotiated, reject 0-RTT.
+          !ssl->token_binding_negotiated &&
           // Custom extensions is incompatible with 0-RTT.
           hs->custom_extensions.received == 0 &&
           // The negotiated ALPN must match the one in the ticket.