[tls] Detect version downgrade attacks

RFC 8446 defines a mechanism that allows (but does not guarantee) the
detection of version downgrade attacks, based on magic signature
values placed within the ServerHello random bytes.  The magic
signature will be present if the server supports any version higher
than the negotiated version, and so may be present if the server
supports a higher version than we are offering.

The last byte of the magic signature is non-constant and is defined to
match the server's negotiated protocol version, encoded as a delta
from the value 0x0302 representing TLS version 1.1 (or lower).  Since
the server random bytes are always used in the construction of
verify_data (even in older versions of TLS without the extended master
secret), this encoding of the negotiated version cannot be forged by
an attacker.

If the version that is negotiated is lower than the version that we
offered (i.e. if a downgrade attack could possibly be happening), then
check for the range of magic signatures that could indicate a
downgrade attack, and terminate the connection if applicable.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
diff --git a/src/include/ipxe/tls.h b/src/include/ipxe/tls.h
index 2dab6cc..2c6b00d 100644
--- a/src/include/ipxe/tls.h
+++ b/src/include/ipxe/tls.h
@@ -41,6 +41,24 @@
 	uint16_t length;
 } __attribute__ (( packed ));
 
+/** TLS server random data */
+union tls_server_random {
+	/** Random nonce (as used by the key schedule) */
+	struct tls_random random;
+	/** Version downgrade detection */
+	struct {
+		/** Unused */
+		uint8_t unused[24];
+		/** Magic signature */
+		uint8_t magic[7];
+		/** Negotiated version (as a delta from TLSv1.1) */
+		uint8_t version;
+	} __attribute__ (( packed )) downgrade;
+};
+
+/** TLS server downgrade detection magic signature */
+#define TLS_SERVER_DOWNGRADE_MAGIC "DOWNGRD"
+
 /** TLS version 1.1 */
 #define TLS_VERSION_TLS_1_1 0x0302
 
diff --git a/src/net/tls.c b/src/net/tls.c
index e53f997..f31fbbf 100644
--- a/src/net/tls.c
+++ b/src/net/tls.c
@@ -190,6 +190,10 @@
 #define EINFO_EPERM_SAVE						\
 	__einfo_uniqify ( EINFO_EPERM, 0x07,				\
 			  "Pre-shared key was not established" )
+#define EPERM_DOWNGRADE __einfo_error ( EINFO_EPERM_DOWNGRADE )
+#define EINFO_EPERM_DOWNGRADE						\
+	__einfo_uniqify ( EINFO_EPERM, 0x08,				\
+			  "Downgrade attack detected" )
 #define EPROTO_VERSION __einfo_error ( EINFO_EPROTO_VERSION )
 #define EINFO_EPROTO_VERSION						\
 	__einfo_uniqify ( EINFO_EPROTO, 0x01,				\
@@ -2314,10 +2318,11 @@
  */
 static int tls_new_server_hello ( struct tls_connection *tls,
 				  const void *data, size_t len ) {
+	static const uint8_t downgrade_magic[7] = TLS_SERVER_DOWNGRADE_MAGIC;
 	struct tls_session *session = tls->session;
 	const struct {
 		uint16_t version;
-		struct tls_random random;
+		union tls_server_random random;
 		uint8_t session_id_len;
 		uint8_t session_id[0];
 	} __attribute__ (( packed )) *hello_a = data;
@@ -2444,6 +2449,18 @@
 	DBGC ( tls, "TLS %p using protocol version %d.%d\n",
 	       tls, ( version >> 8 ), ( version & 0xff ) );
 
+	/* Check for downgrade attacks */
+	if ( ( version < TLS_VERSION_MAX ) &&
+	     ( memcmp ( hello_a->random.downgrade.magic, downgrade_magic,
+			sizeof ( hello_a->random.downgrade.magic ) ) == 0 ) &&
+	     ( hello_a->random.downgrade.version <
+	       ( TLS_VERSION_MAX - TLS_VERSION_TLS_1_1 ) ) ) {
+		DBGC ( tls, "TLS %p detected downgrade attack:\n", tls );
+		DBGC_HDA ( tls, 0, &hello_a->random.downgrade,
+			   sizeof ( hello_a->random.downgrade ) );
+		return -EPERM_DOWNGRADE;
+	}
+
 	/* Select cipher suite */
 	if ( ( rc = tls_select_cipher ( tls, hello_b->cipher_suite ) ) != 0 )
 		return rc;