Keep track of whether mbedtls_ssl_set_hostname() has been called

Use a special marker as ssl->hostname if mbedtls_ssl_set_hostname() has been
called with NULL. If mbedtls_ssl_set_hostname() has never been called, the
field is NULL, as before.

No behavior change apart from now emitting a different log message depending
on whether mbedtls_ssl_set_hostname() has been called with NULL or not at all.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
diff --git a/include/mbedtls/ssl.h b/include/mbedtls/ssl.h
index a7bfd7b..566ec0e 100644
--- a/include/mbedtls/ssl.h
+++ b/include/mbedtls/ssl.h
@@ -1892,6 +1892,10 @@
      * If this is \p NULL, the peer name verification is skipped,
      * the server_name extension is not sent, and the server name is ignored
      * in TLS 1.3 session resumption using tickets.
+     *
+     * This can be a special value to indicate that mbedtls_ssl_set_hostname()
+     * has been called with \p NULL, as opposed to never having been called.
+     * See `mbedtls_ssl_get_hostname_pointer()` in `ssl_tls.c`.
      */
     char *MBEDTLS_PRIVATE(hostname);
 #endif /* MBEDTLS_X509_CRT_PARSE_C */
diff --git a/library/ssl_tls.c b/library/ssl_tls.c
index c3b7497..993c69e 100644
--- a/library/ssl_tls.c
+++ b/library/ssl_tls.c
@@ -2758,6 +2758,12 @@
 
 #if defined(MBEDTLS_X509_CRT_PARSE_C)
 
+/* A magic value for `ssl->hostname` indicating that
+ * mbedtls_ssl_set_hostname() has been called with `NULL`.
+ * If mbedtls_ssl_set_hostname() has never been called on `ssl`, then
+ * `ssl->hostname == NULL`. */
+static const char *const ssl_hostname_skip_cn_verification = "";
+
 #if defined(MBEDTLS_SSL_HANDSHAKE_WITH_CERT_ENABLED)
 /** Whether mbedtls_ssl_set_hostname() has been called.
  *
@@ -2770,11 +2776,6 @@
 static int mbedtls_ssl_has_set_hostname_been_called(
     const mbedtls_ssl_context *ssl)
 {
-    /* We can't tell the difference between the case where
-     * mbedtls_ssl_set_hostname() has not been called at all, and
-     * the case where it was last called with NULL. For the time
-     * being, we assume the latter, i.e. we behave as if there had
-     * been an implicit call to mbedtls_ssl_set_hostname(ssl, NULL). */
     return ssl->hostname != NULL;
 }
 #endif
@@ -2786,12 +2787,16 @@
 #endif
 const char *mbedtls_ssl_get_hostname_pointer(const mbedtls_ssl_context *ssl)
 {
+    if (ssl->hostname == ssl_hostname_skip_cn_verification) {
+        return NULL;
+    }
     return ssl->hostname;
 }
 
 static void mbedtls_ssl_free_hostname(mbedtls_ssl_context *ssl)
 {
-    if (ssl->hostname != NULL) {
+    if (ssl->hostname != NULL &&
+        ssl->hostname != ssl_hostname_skip_cn_verification) {
         mbedtls_zeroize_and_free(ssl->hostname, strlen(ssl->hostname));
     }
     ssl->hostname = NULL;
@@ -2816,13 +2821,19 @@
      * so we can free it safely */
     mbedtls_ssl_free_hostname(ssl);
 
-    /* Passing NULL as hostname shall clear the old one */
-
     if (hostname == NULL) {
-        ssl->hostname = NULL;
+        /* Passing NULL as hostname clears the old one, but leaves a
+         * special marker to indicate that mbedtls_ssl_set_hostname()
+         * has been called. */
+        /* ssl->hostname should be const, but isn't. We won't actually
+         * write to the buffer, so it's ok to cast away the const. */
+        ssl->hostname = (char *) ssl_hostname_skip_cn_verification;
     } else {
         ssl->hostname = mbedtls_calloc(1, hostname_len + 1);
         if (ssl->hostname == NULL) {
+            /* mbedtls_ssl_set_hostname() has been called, but unsuccessfully.
+             * Leave ssl->hostname in the same state as if the function had
+             * not been called, i.e. a null pointer. */
             return MBEDTLS_ERR_SSL_ALLOC_FAILED;
         }
 
diff --git a/tests/ssl-opt.sh b/tests/ssl-opt.sh
index 73d17f0..a88ff3b 100755
--- a/tests/ssl-opt.sh
+++ b/tests/ssl-opt.sh
@@ -6080,9 +6080,11 @@
 
 run_test "Authentication: hostname match, client required" \
          "$P_SRV" \
-         "$P_CLI auth_mode=required server_name=localhost debug_level=1" \
+         "$P_CLI auth_mode=required server_name=localhost debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "! mbedtls_ssl_handshake returned" \
          -C "X509 - Certificate verification failed"
@@ -6134,7 +6136,7 @@
 
 run_test "Authentication: hostname mismatch, client optional" \
          "$P_SRV" \
-         "$P_CLI auth_mode=optional server_name=wrong-name debug_level=1" \
+         "$P_CLI auth_mode=optional server_name=wrong-name debug_level=2" \
          0 \
          -c "does not match with the expected CN" \
          -c "x509_verify_cert() returned -" \
@@ -6142,93 +6144,115 @@
 
 run_test "Authentication: hostname mismatch, client none" \
          "$P_SRV" \
-         "$P_CLI auth_mode=none server_name=wrong-name debug_level=1" \
+         "$P_CLI auth_mode=none server_name=wrong-name debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname null, client required" \
          "$P_SRV" \
-         "$P_CLI auth_mode=required set_hostname=NULL debug_level=1" \
+         "$P_CLI auth_mode=required set_hostname=NULL debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "! mbedtls_ssl_handshake returned" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname null, client optional" \
          "$P_SRV" \
-         "$P_CLI auth_mode=optional set_hostname=NULL debug_level=1" \
+         "$P_CLI auth_mode=optional set_hostname=NULL debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname null, client none" \
          "$P_SRV" \
-         "$P_CLI auth_mode=none set_hostname=NULL debug_level=1" \
+         "$P_CLI auth_mode=none set_hostname=NULL debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname unset, client required" \
          "$P_SRV" \
-         "$P_CLI auth_mode=required set_hostname=no debug_level=1" \
+         "$P_CLI auth_mode=required set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -c "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "! mbedtls_ssl_handshake returned" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname unset, client optional" \
          "$P_SRV" \
-         "$P_CLI auth_mode=optional set_hostname=no debug_level=1" \
+         "$P_CLI auth_mode=optional set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -c "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname unset, client none" \
          "$P_SRV" \
-         "$P_CLI auth_mode=none set_hostname=no debug_level=1" \
+         "$P_CLI auth_mode=none set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname unset, client default, server picks cert, 1.2" \
          "$P_SRV force_version=tls12 force_ciphersuite=TLS-ECDHE-ECDSA-WITH-AES-128-CCM-8" \
-         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=1" \
+         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -c "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 requires_config_enabled MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED
 run_test "Authentication: hostname unset, client default, server picks cert, 1.3" \
          "$P_SRV force_version=tls13 tls13_kex_modes=ephemeral" \
-         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=1" \
+         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -c "Certificate verification without having set hostname" \
+         -c "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 run_test "Authentication: hostname unset, client default, server picks PSK, 1.2" \
          "$P_SRV force_version=tls12 force_ciphersuite=TLS-PSK-WITH-AES-128-CCM-8 psk=73776f726466697368 psk_identity=foo" \
-         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=1" \
+         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"
 
 requires_config_enabled MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_ENABLED
 run_test "Authentication: hostname unset, client default, server picks PSK, 1.3" \
          "$P_SRV force_version=tls13 tls13_kex_modes=psk psk=73776f726466697368 psk_identity=foo" \
-         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=1" \
+         "$P_CLI psk=73776f726466697368 psk_identity=foo set_hostname=no debug_level=2" \
          0 \
          -C "does not match with the expected CN" \
+         -C "Certificate verification without having set hostname" \
+         -C "Certificate verification without CN verification" \
          -C "x509_verify_cert() returned -" \
          -C "X509 - Certificate verification failed"