Support setting per-connection default session lifetime value

Due to recent changes, changing the SSL session timeout from cert_cb is
not possible anymore since the new |SSL_SESSION| is initialized *after*
cert_cb is run. The alternative would be using |SSL_CTX_set_timeout| but
the specific |SSL_CTX| could be shared by multiple |SSL|s.

Setting a value on a per-connection basis is useful in case timeouts
need to be calculated dynamically based on specific certificate/domain
information that would be retrieved from inside cert_cb (or other
callbacks).

It would also be possible to set the value to 0 to prevent session
resumption, which is not otherwise doable in the handshake callbacks.

Change-Id: I730a528c647f83f7f77f59b5b21d7e060e4c9843
Reviewed-on: https://boringssl-review.googlesource.com/12440
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 0de4967..f588f46 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -1721,6 +1721,15 @@
  * |ctx|. */
 OPENSSL_EXPORT long SSL_CTX_get_timeout(const SSL_CTX *ctx);
 
+/* SSL_set_session_timeout sets the default lifetime, in seconds, of the
+ * session created in |ssl| to |timeout|, and returns the old value.
+ *
+ * By default the value |SSL_DEFAULT_SESSION_TIMEOUT| is used, which can be
+ * overridden at the context level by calling |SSL_CTX_set_timeout|.
+ *
+ * If |timeout| is zero the newly created session will not be resumable. */
+OPENSSL_EXPORT long SSL_set_session_timeout(SSL *ssl, long timeout);
+
 /* SSL_CTX_set_session_id_context sets |ctx|'s session ID context to |sid_ctx|.
  * It returns one on success and zero on error. The session ID context is an
  * application-defined opaque byte string. A session will not be used in a
@@ -4215,6 +4224,10 @@
 
   /* TODO(agl): remove once node.js not longer references this. */
   int tlsext_status_type;
+
+  /* session_timeout is the default lifetime in seconds of the session
+   * created in this connection. */
+  long session_timeout;
 };
 
 
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 70a39ea..aafad33 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -477,6 +477,13 @@
       ssl->ctx->signed_cert_timestamps_enabled;
   ssl->ocsp_stapling_enabled = ssl->ctx->ocsp_stapling_enabled;
 
+  ssl->session_timeout = SSL_DEFAULT_SESSION_TIMEOUT;
+
+  /* If the context has a default timeout, use it over the default. */
+  if (ctx->session_timeout != 0) {
+    ssl->session_timeout = ctx->session_timeout;
+  }
+
   return ssl;
 
 err:
diff --git a/ssl/ssl_session.c b/ssl/ssl_session.c
index a6b669d..a452d32 100644
--- a/ssl/ssl_session.c
+++ b/ssl/ssl_session.c
@@ -465,10 +465,7 @@
   ssl_get_current_time(ssl, &now);
   session->time = now.tv_sec;
 
-  /* If the context has a default timeout, use it over the default. */
-  if (ssl->initial_ctx->session_timeout != 0) {
-    session->timeout = ssl->initial_ctx->session_timeout;
-  }
+  session->timeout = ssl->session_timeout;
 
   session->ssl_version = ssl->version;
 
@@ -875,6 +872,12 @@
   return ctx->session_timeout;
 }
 
+long SSL_set_session_timeout(SSL *ssl, long timeout) {
+  long old_timeout = ssl->session_timeout;
+  ssl->session_timeout = timeout;
+  return old_timeout;
+}
+
 typedef struct timeout_param_st {
   SSL_CTX *ctx;
   long time;
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index ae9d5ca..283b737 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -2319,6 +2319,131 @@
   return true;
 }
 
+static int SetSessionTimeoutCallback(SSL *ssl, void *arg) {
+  long timeout = *(long *) arg;
+  SSL_set_session_timeout(ssl, timeout);
+  return 1;
+}
+
+static bool TestSessionTimeoutCertCallback(bool is_dtls,
+                                           const SSL_METHOD *method,
+                                           uint16_t version) {
+  static const int kStartTime = 1000;
+  g_current_time.tv_sec = kStartTime;
+
+  bssl::UniquePtr<X509> cert = GetTestCertificate();
+  bssl::UniquePtr<EVP_PKEY> key = GetTestKey();
+  if (!cert || !key) {
+    return false;
+  }
+
+  bssl::UniquePtr<SSL_CTX> server_ctx(SSL_CTX_new(method));
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(method));
+  if (!server_ctx || !client_ctx ||
+      !SSL_CTX_use_certificate(server_ctx.get(), cert.get()) ||
+      !SSL_CTX_use_PrivateKey(server_ctx.get(), key.get()) ||
+      !SSL_CTX_set_min_proto_version(client_ctx.get(), version) ||
+      !SSL_CTX_set_max_proto_version(client_ctx.get(), version) ||
+      !SSL_CTX_set_min_proto_version(server_ctx.get(), version) ||
+      !SSL_CTX_set_max_proto_version(server_ctx.get(), version)) {
+    return false;
+  }
+
+  SSL_CTX_set_session_cache_mode(client_ctx.get(), SSL_SESS_CACHE_BOTH);
+  SSL_CTX_set_session_cache_mode(server_ctx.get(), SSL_SESS_CACHE_BOTH);
+
+  SSL_CTX_set_current_time_cb(server_ctx.get(), CurrentTimeCallback);
+
+  long timeout = 25;
+  SSL_CTX_set_cert_cb(server_ctx.get(), SetSessionTimeoutCallback, &timeout);
+
+  bssl::UniquePtr<SSL_SESSION> session =
+      CreateClientSession(client_ctx.get(), server_ctx.get());
+  if (!session) {
+    fprintf(stderr, "Error getting session.\n");
+    return false;
+  }
+
+  // Advance the clock just behind the timeout.
+  g_current_time.tv_sec += timeout - 1;
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(), session.get(),
+                           true /* expect session reused */)) {
+    fprintf(stderr, "Error resuming session.\n");
+    return false;
+  }
+
+  // Advance the clock one more second.
+  g_current_time.tv_sec++;
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(), session.get(),
+                           false /* expect session not reused */)) {
+    fprintf(stderr, "Error resuming session.\n");
+    return false;
+  }
+
+  // Set session timeout to 0 to disable resumption.
+  timeout = 0;
+  g_current_time.tv_sec = kStartTime;
+
+  bssl::UniquePtr<SSL_SESSION> not_resumable_session =
+      CreateClientSession(client_ctx.get(), server_ctx.get());
+  if (!not_resumable_session) {
+    fprintf(stderr, "Error getting session.\n");
+    return false;
+  }
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(),
+                           not_resumable_session.get(),
+                           false /* expect session not reused */)) {
+    fprintf(stderr, "Error resuming session with timeout of 0.\n");
+    return false;
+  }
+
+  // Set both context and connection (via callback) default session timeout.
+  // The connection one is the one that ends up being used.
+  timeout = 25;
+  g_current_time.tv_sec = kStartTime;
+
+  SSL_CTX_set_timeout(server_ctx.get(), timeout - 10);
+
+  bssl::UniquePtr<SSL_SESSION> ctx_and_cb_session =
+      CreateClientSession(client_ctx.get(), server_ctx.get());
+  if (!ctx_and_cb_session) {
+    fprintf(stderr, "Error getting session.\n");
+    return false;
+  }
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(),
+                           ctx_and_cb_session.get(),
+                           true /* expect session reused */)) {
+    fprintf(stderr, "Error resuming session with timeout of 0.\n");
+    return false;
+  }
+
+  // Advance the clock just behind the timeout.
+  g_current_time.tv_sec += timeout - 1;
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(),
+                           ctx_and_cb_session.get(),
+                           true /* expect session reused */)) {
+    fprintf(stderr, "Error resuming session.\n");
+    return false;
+  }
+
+  // Advance the clock one more second.
+  g_current_time.tv_sec++;
+
+  if (!ExpectSessionReused(client_ctx.get(), server_ctx.get(),
+                           ctx_and_cb_session.get(),
+                           false /* expect session not reused */)) {
+    fprintf(stderr, "Error resuming session.\n");
+    return false;
+  }
+
+  return true;
+}
+
 static int SwitchContext(SSL *ssl, int *out_alert, void *arg) {
   SSL_CTX *ctx = reinterpret_cast<SSL_CTX*>(arg);
   SSL_set_SSL_CTX(ssl, ctx);
@@ -2664,6 +2789,7 @@
       !TestClientHello() ||
       !ForEachVersion(TestSessionIDContext) ||
       !ForEachVersion(TestSessionTimeout) ||
+      !ForEachVersion(TestSessionTimeoutCertCallback) ||
       !ForEachVersion(TestSNICallback) ||
       !TestEarlyCallbackVersionSwitch() ||
       !TestSetVersion() ||