Push the difference in chain semantics to the edge.

OpenSSL includes a leaf certificate in a certificate chain when it's a
client, but doesn't when it's a server. This is also reflected in the
serialisation of sessions.

This change makes the internal semantics consistent: the leaf is always
included in the chain in memory, and never duplicated when serialised.
To maintain the same API, SSL_get_peer_cert_chain will construct a copy
of the chain without the leaf if needed.

Since the serialised format of a client session has changed, an
|is_server| boolean is added to the ASN.1 that defaults to true. Thus
any old client sessions will be parsed as server sessions and (silently)
discarded by a client.

Change-Id: Ibcf72bc8a130cedb423bc0fd3417868e0af3ca3e
Reviewed-on: https://boringssl-review.googlesource.com/12704
Reviewed-by: Adam Langley <agl@google.com>
Commit-Queue: Adam Langley <agl@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
diff --git a/ssl/handshake_client.c b/ssl/handshake_client.c
index bae5365..59fefc4 100644
--- a/ssl/handshake_client.c
+++ b/ssl/handshake_client.c
@@ -756,7 +756,8 @@
    * version, drop it. */
   if (ssl->session != NULL) {
     uint16_t session_version;
-    if (!ssl->method->version_from_wire(&session_version,
+    if (ssl->session->is_server ||
+        !ssl->method->version_from_wire(&session_version,
                                         ssl->session->ssl_version) ||
         (session_version < TLS1_3_VERSION &&
          ssl->session->session_id_length == 0) ||
@@ -1059,10 +1060,10 @@
     goto err;
   }
 
-  /* NOTE: Unlike the server half, the client's copy of |x509_chain| includes
-   * the leaf. */
   sk_X509_pop_free(ssl->s3->new_session->x509_chain, X509_free);
   ssl->s3->new_session->x509_chain = chain;
+  sk_X509_pop_free(ssl->s3->new_session->x509_chain_without_leaf, X509_free);
+  ssl->s3->new_session->x509_chain_without_leaf = NULL;
 
   X509_free(ssl->s3->new_session->x509_peer);
   X509_up_ref(leaf);
diff --git a/ssl/handshake_server.c b/ssl/handshake_server.c
index 5121f99..cca9bd6 100644
--- a/ssl/handshake_server.c
+++ b/ssl/handshake_server.c
@@ -1483,6 +1483,8 @@
     goto err;
   }
 
+  X509 *leaf = NULL;
+
   if (sk_X509_num(chain) == 0) {
     /* No client certificate so the handshake buffer may be discarded. */
     ssl3_free_handshake_buffer(ssl);
@@ -1506,6 +1508,7 @@
      * classed by them as a bug, but it's assumed by at least NGINX. */
     ssl->s3->new_session->verify_result = X509_V_OK;
   } else {
+    leaf = sk_X509_value(chain, 0);
     /* The hash would have been filled in. */
     if (ssl->retain_only_sha256_of_client_certs) {
       ssl->s3->new_session->peer_sha256_valid = 1;
@@ -1517,13 +1520,16 @@
     }
   }
 
-  X509_free(ssl->s3->new_session->x509_peer);
-  ssl->s3->new_session->x509_peer = sk_X509_shift(chain);
-
   sk_X509_pop_free(ssl->s3->new_session->x509_chain, X509_free);
   ssl->s3->new_session->x509_chain = chain;
-  /* Inconsistency alert: x509_chain does *not* include the peer's own
-   * certificate, while we do include it in s3_clnt.c */
+  sk_X509_pop_free(ssl->s3->new_session->x509_chain_without_leaf, X509_free);
+  ssl->s3->new_session->x509_chain_without_leaf = NULL;
+
+  X509_free(ssl->s3->new_session->x509_peer);
+  if (leaf) {
+    X509_up_ref(leaf);
+  }
+  ssl->s3->new_session->x509_peer = leaf;
 
   return 1;
 
diff --git a/ssl/ssl_asn1.c b/ssl/ssl_asn1.c
index aab6052..67d9df8 100644
--- a/ssl/ssl_asn1.c
+++ b/ssl/ssl_asn1.c
@@ -122,6 +122,7 @@
  *     keyExchangeInfo         [18] INTEGER OPTIONAL,
  *     certChain               [19] SEQUENCE OF Certificate OPTIONAL,
  *     ticketAgeAdd            [21] OCTET STRING OPTIONAL,
+ *     isServer                [22] BOOLEAN DEFAULT TRUE,
  * }
  *
  * Note: historically this serialization has included other optional
@@ -170,6 +171,8 @@
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 19;
 static const int kTicketAgeAddTag =
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 21;
+static const int kIsServerTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 22;
 
 static int SSL_SESSION_to_bytes_full(const SSL_SESSION *in, uint8_t **out_data,
                                      size_t *out_len, int for_ticket) {
@@ -340,12 +343,13 @@
 
   /* The certificate chain is only serialized if the leaf's SHA-256 isn't
    * serialized instead. */
-  if (in->x509_chain != NULL && !in->peer_sha256_valid) {
+  if (in->x509_chain != NULL && !in->peer_sha256_valid &&
+      sk_X509_num(in->x509_chain) >= 2) {
     if (!CBB_add_asn1(&session, &child, kCertChainTag)) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
       goto err;
     }
-    for (size_t i = 0; i < sk_X509_num(in->x509_chain); i++) {
+    for (size_t i = 1; i < sk_X509_num(in->x509_chain); i++) {
       if (!ssl_add_cert_to_cbb(&child, sk_X509_value(in->x509_chain, i))) {
         goto err;
       }
@@ -361,6 +365,15 @@
     }
   }
 
+  if (!in->is_server) {
+    if (!CBB_add_asn1(&session, &child, kIsServerTag) ||
+        !CBB_add_asn1(&child, &child2, CBS_ASN1_BOOLEAN) ||
+        !CBB_add_u8(&child2, 0x00)) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+      goto err;
+    }
+  }
+
   if (!CBB_finish(&cbb, out_data, out_len)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     goto err;
@@ -658,8 +671,20 @@
   }
   sk_X509_pop_free(ret->x509_chain, X509_free);
   ret->x509_chain = NULL;
-  if (has_cert_chain) {
+  if (ret->x509_peer != NULL) {
     ret->x509_chain = sk_X509_new_null();
+    if (ret->x509_chain == NULL ||
+        !sk_X509_push(ret->x509_chain, ret->x509_peer)) {
+        OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+        goto err;
+    }
+    X509_up_ref(ret->x509_peer);
+  }
+  if (has_cert_chain) {
+    if (ret->x509_peer == NULL) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
+      goto err;
+    }
     if (ret->x509_chain == NULL) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
       goto err;
@@ -688,6 +713,17 @@
   }
   ret->ticket_age_add_valid = age_add_present;
 
+  int is_server;
+  if (!CBS_get_optional_asn1_bool(&session, &is_server, kIsServerTag,
+                                  1 /* default to true */)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
+    goto err;
+  }
+  /* TODO: in time we can include |is_server| for servers too, then we can
+     enforce that client and server sessions are never mixed up. */
+
+  ret->is_server = is_server;
+
   if (CBS_len(&session) != 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
     goto err;
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 336689a..8638396 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -1087,10 +1087,35 @@
     return NULL;
   }
   SSL_SESSION *session = SSL_get_session(ssl);
-  if (session == NULL) {
+  if (session == NULL ||
+      session->x509_chain == NULL) {
     return NULL;
   }
-  return session->x509_chain;
+
+  if (!ssl->server) {
+    return session->x509_chain;
+  }
+
+  /* OpenSSL historically didn't include the leaf certificate in the returned
+   * certificate chain, but only for servers. */
+  if (session->x509_chain_without_leaf == NULL) {
+    session->x509_chain_without_leaf = sk_X509_new_null();
+    if (session->x509_chain_without_leaf == NULL) {
+      return NULL;
+    }
+
+    for (size_t i = 1; i < sk_X509_num(session->x509_chain); i++) {
+      X509 *cert = sk_X509_value(session->x509_chain, i);
+      if (!sk_X509_push(session->x509_chain_without_leaf, cert)) {
+        sk_X509_pop_free(session->x509_chain_without_leaf, X509_free);
+        session->x509_chain_without_leaf = NULL;
+        return NULL;
+      }
+      X509_up_ref(cert);
+    }
+  }
+
+  return session->x509_chain_without_leaf;
 }
 
 int SSL_get_tls_unique(const SSL *ssl, uint8_t *out, size_t *out_len,
diff --git a/ssl/ssl_session.c b/ssl/ssl_session.c
index 98d101c..b0951ac 100644
--- a/ssl/ssl_session.c
+++ b/ssl/ssl_session.c
@@ -182,6 +182,7 @@
     goto err;
   }
 
+  new_session->is_server = session->is_server;
   new_session->ssl_version = session->ssl_version;
   new_session->sid_ctx_length = session->sid_ctx_length;
   memcpy(new_session->sid_ctx, session->sid_ctx, session->sid_ctx_length);
@@ -327,6 +328,7 @@
   OPENSSL_cleanse(session->session_id, sizeof(session->session_id));
   X509_free(session->x509_peer);
   sk_X509_pop_free(session->x509_chain, X509_free);
+  sk_X509_pop_free(session->x509_chain_without_leaf, X509_free);
   OPENSSL_free(session->tlsext_hostname);
   OPENSSL_free(session->tlsext_tick);
   OPENSSL_free(session->tlsext_signed_cert_timestamp_list);
@@ -462,6 +464,9 @@
     return 0;
   }
 
+  session->is_server = is_server;
+  session->ssl_version = ssl->version;
+
   /* Fill in the time from the |SSL_CTX|'s clock. */
   struct timeval now;
   ssl_get_current_time(ssl, &now);
@@ -469,8 +474,6 @@
 
   session->timeout = ssl->session_timeout;
 
-  session->ssl_version = ssl->version;
-
   if (is_server) {
     if (hs->ticket_expected) {
       /* Don't set session IDs for sessions resumed with tickets. This will keep
@@ -634,6 +637,9 @@
 
 int ssl_session_is_resumable(const SSL *ssl, const SSL_SESSION *session) {
   return ssl_session_is_context_valid(ssl, session) &&
+         /* The session must have been created by the same type of end point as
+          * we're now using it with. */
+         session->is_server == ssl->server &&
          /* The session must not be expired. */
          ssl_session_is_time_valid(ssl, session) &&
          /* Only resume if the session's version matches the negotiated
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 5e4485f..b74e51e 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -448,10 +448,9 @@
   return true;
 }
 
-// kOpenSSLSession is a serialized SSL_SESSION generated from openssl
-// s_client -sess_out.
+// kOpenSSLSession is a serialized SSL_SESSION.
 static const char kOpenSSLSession[] =
-    "MIIFpQIBAQICAwMEAsAvBCAG5Q1ndq4Yfmbeo1zwLkNRKmCXGdNgWvGT3cskV0yQ"
+    "MIIFqgIBAQICAwMEAsAvBCAG5Q1ndq4Yfmbeo1zwLkNRKmCXGdNgWvGT3cskV0yQ"
     "kAQwJlrlzkAWBOWiLj/jJ76D7l+UXoizP2KI2C7I2FccqMmIfFmmkUy32nIJ0mZH"
     "IWoJoQYCBFRDO46iBAICASyjggR6MIIEdjCCA16gAwIBAgIIK9dUvsPWSlUwDQYJ"
     "KoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMx"
@@ -481,7 +480,7 @@
     "YTcLEkXqKwOBfF9vE4KX0NxeLwjcDTpsuh3qXEaZ992r1N38VDcyS6P7I6HBYN9B"
     "sNHM362zZnY27GpTw+Kwd751CLoXFPoaMOe57dbBpXoro6Pd3BTbf/Tzr88K06yE"
     "OTDKPNj3+inbMaVigtK4PLyPq+Topyzvx9USFgRvyuoxn0Hgb+R0A3j6SLRuyOdA"
-    "i4gv7Y5oliyn";
+    "i4gv7Y5oliyntgMBAQA=";
 
 // kCustomSession is a custom serialized SSL_SESSION generated by
 // filling in missing fields from |kOpenSSLSession|. This includes
diff --git a/ssl/tls13_both.c b/ssl/tls13_both.c
index 1a1d8b9..939e530 100644
--- a/ssl/tls13_both.c
+++ b/ssl/tls13_both.c
@@ -322,6 +322,8 @@
   sk_X509_pop_free(ssl->s3->new_session->x509_chain, X509_free);
   ssl->s3->new_session->x509_chain = chain;
   chain = NULL;
+  sk_X509_pop_free(ssl->s3->new_session->x509_chain_without_leaf, X509_free);
+  ssl->s3->new_session->x509_chain_without_leaf = NULL;
 
   ret = 1;
 
diff --git a/ssl/tls13_server.c b/ssl/tls13_server.c
index 3ab9c86..367ea5a 100644
--- a/ssl/tls13_server.c
+++ b/ssl/tls13_server.c
@@ -552,12 +552,6 @@
     return ssl_hs_error;
   }
 
-  /* For historical reasons, the server's copy of the chain does not include the
-   * leaf while the client's does. */
-  if (sk_X509_num(ssl->s3->new_session->x509_chain) > 0) {
-    X509_free(sk_X509_shift(ssl->s3->new_session->x509_chain));
-  }
-
   hs->tls13_state = state_process_client_certificate_verify;
   return ssl_hs_read_message;
 }