From 7c61f7c9c33c46744ef8f3816ed09d62953532ec Mon Sep 17 00:00:00 2001
From: dmiller <dmiller@e0a8ed71-7df4-0310-8962-fdc924857419>
Date: Fri, 2 Jul 2021 20:01:30 +0000
Subject: [PATCH] TLS 1.3 support for NSE. Fixes #1691

  Adapted for Ubuntu/Jammy by wdoekes.
  Patch howto:

    cd /usr/share/nmap
    sudo patch -p1 < /path/to/this.patch

--- a/nselib/sslcert.lua	2023-01-12 18:40:55.000000000 +0100
+++ b/nselib/sslcert.lua	2024-03-21 17:20:23.474772058 +0100
@@ -916,7 +916,9 @@ function getCertificate(host, port)
     end
 
     -- logic mostly lifted from ssl-enum-ciphers
-    local hello = tls.client_hello()
+    -- TODO: implement TLSv1.3 handshake encryption so we can decrypt the
+    -- Certificate message. Until then, we don't attempt TLSv1.3
+    local hello = tls.client_hello({protocol="TLSv1.2"})
     local status, err = socket:send(hello)
     if not status then
       mutex "done"
--- a/nselib/tls.lua	2023-01-12 18:40:55.000000000 +0100
+++ b/nselib/tls.lua	2024-03-21 17:20:59.766301269 +0100
@@ -28,9 +28,10 @@ PROTOCOLS = {
   ["SSLv3"]       = 0x0300,
   ["TLSv1.0"]     = 0x0301,
   ["TLSv1.1"]     = 0x0302,
-  ["TLSv1.2"]     = 0x0303
+  ["TLSv1.2"]     = 0x0303,
+  ["TLSv1.3"]     = 0x0304,
 }
-HIGHEST_PROTOCOL = "TLSv1.2"
+HIGHEST_PROTOCOL = "TLSv1.3"
 
 --
 -- TLS Record Types
@@ -82,12 +83,15 @@ TLS_ALERT_REGISTRY = {
   ["inappropriate_fallback"]              = 86,
   ["user_canceled"]                       = 90,
   ["no_renegotiation"]                    = 100,
+  ["missing_extension"]                   = 109,
   ["unsupported_extension"]               = 110,
   ["certificate_unobtainable"]            = 111,
   ["unrecognized_name"]                   = 112,
   ["bad_certificate_status_response"]     = 113,
   ["bad_certificate_hash_value"]          = 114,
-  ["unknown_psk_identity"]                = 115
+  ["unknown_psk_identity"]                = 115,
+  ["certificate_required"]                = 116,
+  ["no_application_protocol"]             = 120,
 }
 
 --
@@ -99,6 +103,9 @@ TLS_HANDSHAKETYPE_REGISTRY = {
   ["server_hello"]                = 2,
   ["hello_verify_request"]        = 3,
   ["NewSessionTicket"]            = 4,
+  ["end_of_early_data"]           = 5,
+  ["hello_retry_request"]         = 6,
+  ["encrypted_extensions"]        = 8,
   ["certificate"]                 = 11,
   ["server_key_exchange"]         = 12,
   ["certificate_request"]         = 13,
@@ -109,7 +116,9 @@ TLS_HANDSHAKETYPE_REGISTRY = {
   ["certificate_url"]             = 21,
   ["certificate_status"]          = 22,
   ["supplemental_data"]           = 23,
+  ["key_update"]                  = 24,
   ["next_protocol"]               = 67,
+  ["message_hash"]                = 254,
 }
 
 --
@@ -153,13 +162,24 @@ ELLIPTIC_CURVES = {
   brainpoolP256r1 = 26, --RFC7027
   brainpoolP384r1 = 27,
   brainpoolP512r1 = 28,
-  ecdh_x25519 = 29, -- draft rfc4492
-  ecdh_x448 = 30, --draft rfc4492
-  ffdhe2048 = 256, --RFC7919
-  ffdhe3072 = 257, --RFC7919
-  ffdhe4096 = 258, --RFC7919
-  ffdhe6144 = 259, --RFC7919
-  ffdhe8192 = 260, --RFC7919
+  ecdh_x25519 = 29, -- rfc8422
+  ecdh_x448 = 30, -- rfc8422
+  brainpoolP256r1tls13 = 31, --RFC8734
+  brainpoolP384r1tls13 = 32,
+  brainpoolP512r1tls13 = 33,
+  GC256A = 34, -- draft-smyshlyaev-tls12-gost-suites
+  GC256B = 35,
+  GC256C = 36,
+  GC256D = 37,
+  GC512A = 38,
+  GC512B = 39,
+  GC512C = 40,
+  curveSM2 = 41, -- RFC 8998
+  ffdhe2048 = 0x0100, --RFC7919
+  ffdhe3072 = 0x0101, --RFC7919
+  ffdhe4096 = 0x0102, --RFC7919
+  ffdhe6144 = 0x0103, --RFC7919
+  ffdhe8192 = 0x0104, --RFC7919
   arbitrary_explicit_prime_curves = 0xFF01,
   arbitrary_explicit_char2_curves = 0xFF02,
 }
@@ -170,6 +190,7 @@ DEFAULT_ELLIPTIC_CURVES = {
   "secp384r1",
   "secp521r1",
   "ecdh_x25519",
+  "ffdhe2048", -- added for TLSv1.3
 }
 
 ---
@@ -202,6 +223,35 @@ SignatureAlgorithms = {
 }
 
 ---
+-- TLS v1.3 Signature Algorithms
+SignatureSchemes = {
+  -- RSASSA-PKCS1-v1_5 algorithms
+  rsa_pkcs1_sha256 = 0x0401,
+  rsa_pkcs1_sha384 = 0x0501,
+  rsa_pkcs1_sha512 = 0x0601,
+  -- ECDSA algorithms
+  ecdsa_secp256r1_sha256 = 0x0403,
+  ecdsa_secp384r1_sha384 = 0x0503,
+  ecdsa_secp521r1_sha512 = 0x0603,
+  -- RSASSA-PSS algorithms with public key OID rsaEncryption
+  rsa_pss_rsae_sha256 = 0x0804,
+  rsa_pss_rsae_sha384 = 0x0805,
+  rsa_pss_rsae_sha512 = 0x0806,
+  -- EdDSA algorithms
+  ed25519 = 0x0807,
+  ed448   = 0x0808,
+  -- RSASSA-PSS algorithms with public key OID RSASSA-PSS
+  rsa_pss_pss_sha256 = 0x0809,
+  rsa_pss_pss_sha384 = 0x080a,
+  rsa_pss_pss_sha512 = 0x080b,
+  -- Legacy algorithms
+  rsa_pkcs1_sha1 = 0x0201,
+  ecdsa_sha1     = 0x0203,
+  -- RFC 8998
+  sm2sig_sm3 = 0x0708,
+}
+
+---
 -- Extensions
 -- RFC 6066, draft-agl-tls-nextprotoneg-03
 -- https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml
@@ -221,6 +271,9 @@ EXTENSIONS = {
   ["ec_point_formats"] = 11,
   ["srp"] = 12,
   ["signature_algorithms"] = 13,
+  -- TLSv1.3 changed the format for this extension. It's just more convenient
+  -- to call it something else.
+  ["signature_algorithms_13"] = 13,
   ["use_srtp"] = 14,
   ["heartbeat"] = 15,
   ["application_layer_protocol_negotiation"] = 16,
@@ -234,6 +287,18 @@ EXTENSIONS = {
   ["token_binding"] = 24, -- Temporary, expires 2018-02-04
   ["cached_info"] = 25, -- rfc7924
   ["SessionTicket TLS"] = 35,
+  -- TLSv1.3
+  ["pre_shared_key"] = 41,
+  ["early_data"] = 42,
+  ["supported_versions"] = 43,
+  ["cookie"] = 44,
+  ["psk_key_exchange_modes"] = 45,
+  ["certificate_authorities"] = 47,
+  ["oid_filters"] = 48,
+  ["post_handshake_auth"] = 49,
+  ["signature_algorithms_cert"] = 50,
+  ["key_share"] = 51,
+  --
   ["next_protocol_negotiation"] = 13172,
   ["renegotiation_info"] = 65281,
 }
@@ -276,6 +341,13 @@ EXTENSION_HELPERS = {
     end
     return pack(">s2", table.concat(list))
   end,
+  ["signature_algorithms_13"] = function (signature_schemes)
+    local list = {}
+    for _, name in ipairs(signature_schemes) do
+      list[#list+1] = pack(">I2", SignatureSchemes[name])
+    end
+    return pack(">s2", table.concat(list))
+  end,
   ["application_layer_protocol_negotiation"] = function(protocols)
     local list = {}
     for _, proto in ipairs(protocols) do
@@ -284,6 +356,13 @@ EXTENSION_HELPERS = {
     return pack(">s2", table.concat(list))
   end,
   ["next_protocol_negotiation"] = tostring,
+  ["supported_versions"] = function(versions)
+    local list = {}
+    for _, name in ipairs(versions) do
+      list[#list+1] = pack(">I2", PROTOCOLS[name])
+    end
+    return pack(">s1", table.concat(list))
+  end,
 }
 
 --
@@ -651,6 +730,12 @@ CIPHERS = {
 ["TLS_ECDHE_ECDSA_WITH_AES_256_CCM"]               =  0xC0AD,
 ["TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8"]             =  0xC0AE,
 ["TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8"]             =  0xC0AF,
+["TLS_ECCPWD_WITH_AES_128_GCM_SHA256"]             =  0xC0B0, -- RFC8492
+["TLS_ECCPWD_WITH_AES_256_GCM_SHA384"]             =  0xC0B1, -- RFC8492
+["TLS_ECCPWD_WITH_AES_128_CCM_SHA256"]             =  0xC0B2, -- RFC8492
+["TLS_ECCPWD_WITH_AES_256_CCM_SHA384"]             =  0xC0B3, -- RFC8492
+["TLS_AKE_WITH_NULL_SHA256"]             =  0xC0B4, -- draft-camwinget-tls-ts13-macciphersuites
+["TLS_AKE_WITH_NULL_SHA384"]             =  0xC0B5, -- draft-camwinget-tls-ts13-macciphersuites
 ["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256-draft"]    =  0xCC13, -- RFC7905 superseded
 ["TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256-draft"]  =  0xCC14, -- RFC7905 superseded
 ["TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256-draft"]      =  0xCC15, -- RFC7905 superseded
@@ -661,21 +746,49 @@ CIPHERS = {
 ["TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256"]    =  0xCCAC,
 ["TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256"]      =  0xCCAD,
 ["TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256"]      =  0xCCAE,
-["TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256"]          = 0xD001, -- draft-ietf-tls-ecdhe-psk-aead-05
-["TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384"]          = 0xD002, -- draft-ietf-tls-ecdhe-psk-aead-05
-["TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256"]        = 0xD003, -- draft-ietf-tls-ecdhe-psk-aead-05
-["TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256"]          = 0xD005, -- draft-ietf-tls-ecdhe-psk-aead-05
+["TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256"]          = 0xD001, -- RFC 8442
+["TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384"]          = 0xD002, -- RFC 8442
+["TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256"]        = 0xD003, -- RFC 8442
+["TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256"]          = 0xD005, -- RFC 8442
 ["SSL_RSA_FIPS_WITH_DES_CBC_SHA"]                  =  0xFEFE,
 ["SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA"]             =  0xFEFF,
+-- TLSv1.3:
+-- "Although TLS 1.3 uses the same cipher suite space as previous versions of
+-- TLS, TLS 1.3 cipher suites are defined differently, only specifying the
+-- symmetric ciphers, and cannot be used for TLS 1.2.  Similarly, TLS 1.2 and
+-- lower cipher suites cannot be used with TLS 1.3."
+-- We designate these as AKE (Authenticated Key Exchange) ciphersuites, in
+-- order to simplify use of the cipher_info function.
+TLS_AKE_WITH_AES_128_GCM_SHA256       = 0x1301,
+TLS_AKE_WITH_AES_256_GCM_SHA384       = 0x1302,
+TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 = 0x1303,
+TLS_AKE_WITH_AES_128_CCM_SHA256       = 0x1304,
+TLS_AKE_WITH_AES_128_CCM_8_SHA256     = 0x1305,
+TLS_AKE_WITH_SM4_GCM_SM3 = 0x00C6, -- RFC 8998
+TLS_AKE_WITH_SM4_CCM_SM3 = 0x00C7, -- RFC 8998
 }
 
-DEFAULT_CIPHERS = {
+-- Default ciphers sent by tls.client_hello for TLSv1.2 or earlier
+DEFAULT_TLS12_CIPHERS = {
   "TLS_RSA_WITH_AES_128_CBC_SHA", -- mandatory TLSv1.2
   "TLS_RSA_WITH_3DES_EDE_CBC_SHA", -- mandatory TLSv1.1
   "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", -- mandatory TLSv1.0
   "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", -- DHE with strong AES
   "TLS_RSA_WITH_RC4_128_MD5", -- Weak and old, but likely supported on old stuff
 }
+-- Same, but for TLSv1.3
+DEFAULT_TLS13_CIPHERS = {
+  "TLS_AKE_WITH_AES_128_GCM_SHA256", -- mandatory TLSv1.3
+  "TLS_AKE_WITH_AES_256_GCM_SHA384", -- stronger TLSv1.3
+  "TLS_AKE_WITH_CHACHA20_POLY1305_SHA256", -- alternate TLSv1.3
+}
+-- Same, but for handshakes compatible with any TLS version
+local DEFAULT_CIPHERS = {
+  table.unpack(DEFAULT_TLS13_CIPHERS)
+}
+for _, c in ipairs(DEFAULT_TLS12_CIPHERS) do
+  table.insert(DEFAULT_CIPHERS, c)
+end
 
 local function find_key(t, value)
   local found, v = tableaux.contains(t, value)
@@ -744,6 +857,24 @@ local function unpack_dhparams (blob, po
   return pos, {p=p, g=g, y=y}, #p * 8
 end
 
+local function named_group_info (group)
+  if group:match("^arbitrary") then
+    return "ec"
+  end
+  local ktype, size = group:match("^(%D+)(%d+)")
+  assert(ktype and size, ("Invalid named group: %s"):format(group))
+  size = tonumber(size)
+  if ktype == "ffdhe" then
+    ktype = "dh"
+  else
+    if group == "ecdh_x25519" or group == "curveSM2" then
+      size = 256
+    end
+    ktype = "ec"
+  end
+  return ktype, size
+end
+
 local function unpack_ecdhparams (blob, pos)
   local eccurvetype
   eccurvetype, pos = unpack("B", blob, pos)
@@ -780,14 +911,8 @@ local function unpack_ecdhparams (blob,
       ec_curve_type = "namedcurve",
       curve = find_key(ELLIPTIC_CURVES, curve)
     }
-    local size = ret.curve_params.curve:match("(%d+)[rk]%d$")
-    if size then
-      strength = tonumber(size)
-    elseif ret.curve_params.curve == "ecdh_x25519" then
-      strength = 256
-    elseif ret.curve_params.curve == "ecdh_x448" then
-      strength = 448
-    end
+    local _
+    _, strength = named_group_info(ret.curve_params.curve)
   end
   ret.public, pos = unpack("s1", blob, pos)
   return pos, ret, strength
@@ -1061,7 +1186,51 @@ KEX_ALGORITHMS.KRB5_EXPORT={
   export=true,
 }
 
-
+-- TLSv1.3
+KEX_ALGORITHMS.AKE = {
+	tls13ok=true,
+	tls13only=true,
+	pfs=true,
+	-- TLSv1.3 swaps the ServerKeyExchange message for the key_share extension.
+	-- We'll just pretend that's what this is:
+  server_key_exchange = function (blob, protocol)
+    local named_group, pos = unpack(">I2", blob)
+    stdnse.debug1("named_group = %d", named_group)
+    named_group = find_key(ELLIPTIC_CURVES, named_group)
+    local gtype, strength = named_group_info(named_group)
+    return {
+      type = gtype,
+      strength = strength,
+      ecdhparams={ -- Not always ECC, but reusing structure simplifies things
+        curve_params={
+          ec_curve_type = "namedcurve",
+          curve = named_group,
+        }
+      }
+    }
+  end,
+}
+-- RFC 8492
+KEX_ALGORITHMS.ECCPWD = {
+	tls13ok=true,
+	tls13only=false,
+}
+
+local algorithms = {
+  ["3DES"] = {s=112, b=64}, --NIST SP 800-57
+  CHACHA20 = {s=256, b=128},
+  IDEA = {s=128, b=64},
+  SEED = {s=128, b=128},
+  FORTEZZA = {s=80, b=64},
+  DES = {s=56, b=64},
+  RC2 = {s=40, b=64},
+  DES40 = {s=40, b=64},
+  NULL = {s=0},
+  CAMELLIA = {b=128},
+  ARIA = {b=128},
+  AES = {b=128},
+  SM4 = {s=128, b=128},
+}
 --- Get info about a cipher suite
 --
 --  Returned table has "kex", "cipher", "mode", "size", and
@@ -1073,7 +1242,6 @@ KEX_ALGORITHMS.KRB5_EXPORT={
 function cipher_info (c)
   local info = cipher_info_cache[c]
   if info then return info end
-  info = {}
   local tokens = stringaux.strsplit("_", c)
   local i = 1
   if tokens[i] ~= "TLS" and tokens[i] ~= "SSL" then
@@ -1085,7 +1253,14 @@ function cipher_info (c)
   while tokens[i] and tokens[i] ~= "WITH" do
     i = i + 1
   end
-  info.kex = table.concat(tokens, "_", 2, i-1)
+  local kex = table.concat(tokens, "_", 2, i-1)
+  info = KEX_ALGORITHMS[kex]
+  if info then
+    info = tableaux.tcopy(info)
+    info.kex = kex
+  else
+    info = {kex = kex}
+  end
 
   if tokens[i] and tokens[i] ~= "WITH" then
     stdnse.debug2("cipher_info: Can't parse (no WITH): %s", c)
@@ -1101,34 +1276,16 @@ function cipher_info (c)
   end
 
   -- key size
-  if t == "3DES" then -- NIST SP 800-57
-    info.size = 112
-  elseif t == "CHACHA20" then
-    info.size = 256
-  elseif t == "IDEA" then
-    info.size = 128
-  elseif t == "SEED" then
-    info.size = 128
-  elseif t == "FORTEZZA" then
-    info.size = 80
-  elseif t == "DES" then
-    info.size = 56
-  elseif t == "RC2" or t == "DES40" then
-    info.size = 40
-  elseif t == "NULL" then
-    info.size = 0
-  else
+  local tmp = algorithms[t]
+  if tmp then
+    info.size = tmp.s
+    info.block_size = tmp.b
+  end
+  if info.size == nil then
     i = i + 1
     info.size = tonumber(tokens[i])
   end
 
-  -- block size (bits)
-  if t == "3DES" or t == "RC2" or t == "IDEA" or t == "DES" or t == "FORTEZZA" or t == "DES40" then
-    info.block_size = 64
-  elseif t == "AES" or t == "CAMELLIA" or t == "ARIA" or t == "SEED" then
-    info.block_size = 128
-  end
-
   -- stream ciphers don't have a mode
   if info.cipher == "RC4" then
     info.mode = "stream"
@@ -1155,15 +1312,28 @@ function cipher_info (c)
   -- hash
   if info.mode == "CCM" then
     info.hash = "SHA256"
-  else
-    i = i + 1
-    t = (tokens[i]):match("(.*)%-draft$")
-    if t then
-      info.draft = true
-    else
-      t = tokens[i]
+  end
+  i = i + 1
+  if i <= #tokens then
+    if tokens[i] == "8" and info.mode == "CCM" then
+      info.mode = "CCM_8"
+      i = i + 1
+    elseif info.export and (tokens[i]):match("^%d+$") then
+      info.size = tonumber(tokens[i])
+      i = i + 1
+    end
+    if i <= #tokens then
+      local t, w = (tokens[i]):match("(.+)%-([a-z]+)")
+      if t then
+        if w == "draft" then
+          info.draft = true
+        end
+        -- else "or"
+      else
+        t = tokens[i]
+      end
+      info.hash = t
     end
-    info.hash = t
   end
 
   cipher_info_cache[c] = info
@@ -1200,6 +1370,22 @@ handshake_parse = {
         b["cipher"] = find_key(CIPHERS, b["cipher"])
         b["compressor"] = find_key(COMPRESSORS, b["compressor"])
 
+        -- RFC 8446: HelloRetryRequest message uses the same structure as the
+        -- ServerHello, but with Random set to the special value of the SHA-256
+        -- of "HelloRetryRequest"
+        if b.protocol == "TLSv1.2" -- TLSv1.3 legacy version
+          and b.random == "\xCF\x21\xAD\x74\xE5\x9A\x61\x11\xBE\x1D\x8C\x02\x1E\x65\xB8\x91\xC2\xA2\x11\x16\x7A\xBB\x8C\x5E\x07\x9E\x09\xE2\xC8\xA8\x33\x9C"
+          then
+          b.helloretry = true
+        end
+
+        -- RFC 8446: "the legacy_version field MUST be set to 0x0303,
+        -- which is the version number for TLS 1.2"
+        if (b.protocol == "TLSv1.2" and b.extensions
+            and b.extensions.supported_versions == "\x03\x04") then
+          b.protocol = "TLSv1.3"
+        end
+
         return b, j
       end,
 
@@ -1392,6 +1578,27 @@ function record_read(buffer, i, fragment
 end
 
 ---
+-- Get the record version field appropriate for the protocol version
+--
+-- TLSv1.3 introduced a change in the interpretation of the record version
+-- field. Previously, this was an indication of the TLS protocol, but now it is
+-- frozen at TLSv1.2.
+-- @param proto_version The numeric value of the protocol, e.g. 0x0303 for TLSv1.2
+-- @return The numeric value that should be used in the record layer version field
+local function legacy_version (proto_version)
+  -- TLSv1.2 was the last version where protocol version was negotiated via the
+  -- record layer version. Later versions use the supported_versions extension
+  return proto_version <= 0x0303 and proto_version or 0x0303
+end
+
+function record_version_ok(received_version, proto_version)
+  if proto_version == "TLSv1.3" then
+    return received_version == "TLSv1.2"
+  end
+  return proto_version == received_version
+end
+
+---
 -- Build a SSL/TLS record
 -- @param type The type of record ("handshake", "change_cipher_spec", etc.)
 -- @param protocol The protocol and version ("SSLv3", "TLSv1.0", etc.)
@@ -1402,7 +1609,7 @@ function record_write(type, protocol, b)
     -- Set the header as a handshake.
     pack("B", TLS_CONTENTTYPE_REGISTRY[type]),
     -- Set the protocol.
-    pack(">I2", PROTOCOLS[protocol]),
+    pack(">I2", legacy_version(PROTOCOLS[protocol])),
     -- Set the length of the header body.
     pack(">s2", b)
   })
@@ -1431,6 +1638,26 @@ do
   }
   DEFAULT_SIGALGS = EXTENSION_HELPERS["signature_algorithms"](sigalgs)
 end
+-- Equivalent for TLSv1.3 is SignatureScheme
+-- We'll offer all the sha256 and sha512 variants, plus a few extra
+local DEFAULT_SIGSCHEMES
+do
+  local sigalgs = {
+  "rsa_pkcs1_sha256",
+  "rsa_pkcs1_sha512",
+  "ecdsa_secp256r1_sha256",
+  "ecdsa_secp521r1_sha512",
+  "rsa_pss_rsae_sha256",
+  "rsa_pss_rsae_sha512",
+  "ed25519",
+  "ed448",
+  "rsa_pss_pss_sha256",
+  "rsa_pss_pss_sha512",
+  "rsa_pkcs1_sha1",
+  "ecdsa_sha1",
+  }
+  DEFAULT_SIGSCHEMES = EXTENSION_HELPERS["signature_algorithms_13"](sigalgs)
+end
 
 ---
 -- Build a client_hello message
@@ -1455,10 +1682,11 @@ function client_hello(t)
   -- Set the protocol.
   local protocol = t["protocol"] or HIGHEST_PROTOCOL
   table.insert(b, pack(">I2 I4",
-    PROTOCOLS[protocol],
+    legacy_version(PROTOCOLS[protocol]),
     -- Set the random data.
     os.time()
   ))
+  local record_proto = t.record_protocol
 
   -- Set the random data.
   table.insert(b, rand.random_string(28))
@@ -1467,11 +1695,24 @@ function client_hello(t)
   local sid = t["session_id"] or ""
   table.insert(b, pack(">s1", sid))
 
+  local eccpwd = false
+  local shangmi = false
   -- Cipher suites.
   ciphers = {}
   -- Add specified ciphers.
-  for _, cipher in pairs(t["ciphers"] or DEFAULT_CIPHERS) do
+  for _, cipher in pairs(t.ciphers -- user-specified list
+    or (record_proto == "TLSv1.3" and DEFAULT_TLS13_CIPHERS) -- TLSv1.3 only
+    or (PROTOCOLS[protocol] < PROTOCOLS["TLSv1.3"] and DEFAULT_TLS12_CIPHERS) -- non-TLSv1.3
+    or DEFAULT_CIPHERS) -- combined/compatible handshake
+  do
     if type(cipher) == "string" then
+      if cipher:match("^TLS_ECCPWD_") then
+        -- RFC 8492 has specific requirements
+        eccpwd = true
+      elseif protocol == "TLSv1.3" and cipher:match("_SM3$") then
+        -- RFC 8998 has specific requirements
+        shangmi = true
+      end
       cipher = CIPHERS[cipher] or SCSVS[cipher]
     end
     if type(cipher) == "number" and cipher >= 0 and cipher <= 0xffff then
@@ -1497,27 +1738,75 @@ function client_hello(t)
   table.insert(b, pack("s1", table.concat(compressors)))
 
   -- TLS extensions
-  if PROTOCOLS[protocol] and protocol ~= "SSLv3" then
+  local proto_ver = PROTOCOLS[protocol]
+  if proto_ver and protocol ~= "SSLv3" then
     local extensions = {}
-    if t["extensions"] ~= nil then
-      -- Do we need to add the signature_algorithms extension?
-      local need_sigalg = (protocol == "TLSv1.2")
-      -- Add specified extensions.
+    -- TLSv1.3 requires supported_versions and key_share extensions
+    -- OpenSSL also appears to want supported_groups in some cases?
+    local need_supported_versions = (proto_ver >= PROTOCOLS["TLSv1.3"])
+    local need_key_share = need_supported_versions
+    local need_elliptic_curves = need_supported_versions
+    -- Do we need to add the signature_algorithms extension?
+    local need_sigalg = (proto_ver >= PROTOCOLS["TLSv1.2"])
+    -- Add specified extensions.
+    if t.extensions then
       for extension, data in pairs(t["extensions"]) do
         if type(extension) == "number" then
           table.insert(extensions, pack(">I2", extension))
         else
-          if extension == "signature_algorithms" then
+          if extension == "signature_algorithms" or extension == "signature_algorithms_13" then
             need_sigalg = false
+            if shangmi then
+              local sm2sig_sm3 = pack(">I2", SignatureSchemes.sm2sig_sm3)
+              if not data:match("^..(..)*" .. sm2sig_sm3) then
+                data = pack(">s2", data:sub(3) .. sm2sig_sm3)
+              end
+            end
+          elseif extension == "supported_versions" then
+            need_supported_versions = false
+          elseif extension == "key_share" then
+            need_key_share = false
+          elseif extension == "elliptic_curves" then
+            need_elliptic_curves = false
+            if shangmi then
+              -- For now, RFC 8998 is the only one that enforces particular curves
+              local curveSM2 = pack(">I2", ELLIPTIC_CURVES.curveSM2)
+              if not data:match("^..(..)*" .. curveSM2) then
+                data = pack(">s2", data:sub(3) .. curveSM2)
+              end
+            end
           end
           table.insert(extensions, pack(">I2", EXTENSIONS[extension]))
         end
         table.insert(extensions, pack(">s2", data))
       end
-      if need_sigalg then
-        table.insert(extensions, pack(">I2", EXTENSIONS["signature_algorithms"]))
-        table.insert(extensions, pack(">s2", DEFAULT_SIGALGS))
+    end
+    if need_supported_versions then
+      table.insert(extensions, pack(">I2", EXTENSIONS["supported_versions"]))
+      -- We'd prefer TLS 1.2 or 1.1, since we've tested our scripts on those.
+      table.insert(extensions, pack(">s2", EXTENSION_HELPERS["supported_versions"]({"TLSv1.2", "TLSv1.1", "TLSv1.3", "SSLv3"})))
+    end
+    if need_sigalg then
+      table.insert(extensions, pack(">I2", EXTENSIONS["signature_algorithms"]))
+      local data = proto_ver >= PROTOCOLS["TLSv1.3"] and DEFAULT_SIGSCHEMES or DEFAULT_SIGALGS
+      if shangmi then
+        data = pack(">s2", data:sub(3) .. pack(">I2", SignatureSchemes.sm2sig_sm3))
+      end
+      table.insert(extensions, pack(">s2", data))
+    end
+    if need_key_share then
+      -- RFC 8446: Clients MAY send an empty client_shares vector in order to request
+      -- group selection from the server, at the cost of an additional round trip
+      table.insert(extensions, pack(">I2", EXTENSIONS["key_share"]))
+      table.insert(extensions, pack(">s2", "\0\0"))
+    end
+    if need_elliptic_curves then
+      local curves = {table.unpack(DEFAULT_ELLIPTIC_CURVES)}
+      if shangmi then
+        curves[#curves+1] = "curveSM2"
       end
+      table.insert(extensions, pack(">I2", EXTENSIONS["elliptic_curves"]))
+      table.insert(extensions, pack(">s2", EXTENSION_HELPERS["elliptic_curves"](curves)))
     end
     -- Extensions are optional
     if #extensions ~= 0 then
@@ -1544,9 +1833,14 @@ function client_hello(t)
   -- be downgraded by a MITM to SSLv3. So we use TLSv1.0 unless the caller
   -- explicitly tries to set SSLv3.0 somewhere (t.record_protocol or
   -- t.protocol)
-  local record_proto = t.record_protocol
   if not record_proto then
     record_proto = (t.protocol == "SSLv3") and "SSLv3" or "TLSv1.0"
+  elseif record_proto == "TLSv1.3" then
+    -- RFC 8446: "MUST be set to 0x0303 for all records generated by a TLS 1.3
+    -- implementation other than an initial ClientHello (i.e., one not generated
+    -- after a HelloRetryRequest), where it MAY also be 0x0301 for compatibility
+    -- purposes.
+    record_proto = "TLSv1.2"
   end
   return record_write("handshake", record_proto, table.concat(h))
 end
@@ -1617,4 +1911,12 @@ function servername(host)
     end
 end
 
+local unittest = require "unittest"
+if not unittest.testing() then
+  return _ENV
+end
+test_suite = unittest.TestSuite:new()
+for name, code in pairs(CIPHERS) do
+  test_suite:add_test(unittest.not_nil(cipher_info(name).kex), name .. ".kex")
+end
 return _ENV;
diff --git a/scripts/ssl-date.nse b/scripts/ssl-date.nse
index 2f4db4570a..6666305143 100644
--- a/scripts/ssl-date.nse
+++ b/scripts/ssl-date.nse
@@ -75,7 +75,7 @@ local client_hello = function(host, port)
     status, err = sock:connect(host, port)
     if not status then
       sock:close()
-      stdnse.debug("Can't send: %s", err)
+      stdnse.debug("Can't connect: %s", err)
       return false
     end
   else
@@ -86,23 +86,24 @@ local client_hello = function(host, port)
   end


-  -- Send Client Hello to the target server
-  status, err = sock:send(cli_h)
-  if not status then
-    stdnse.debug("Couldn't send: %s", err)
-    sock:close()
-    return false
-  end
+  repeat -- only once
+    -- Send Client Hello to the target server
+    status, err = sock:send(cli_h)
+    if not status then
+      stdnse.debug("Couldn't send: %s", err)
+      break
+    end

-  -- Read response
-  status, response, err = tls.record_buffer(sock)
-  if not status then
-    stdnse.debug("Couldn't receive: %s", err)
-    sock:close()
-    return false
-  end
+    -- Read response
+    status, response, err = tls.record_buffer(sock)
+    if not status then
+      stdnse.debug("Couldn't receive: %s", err)
+      break
+    end
+  until true

-  return true, response
+  sock:close()
+  return status, response
 end

 -- extract time from ServerHello response
diff --git a/scripts/ssl-dh-params.nse b/scripts/ssl-dh-params.nse
index 4582420174..bf2bde57e6 100644
--- a/scripts/ssl-dh-params.nse
+++ b/scripts/ssl-dh-params.nse
@@ -883,6 +883,11 @@ parameters.]],
   }

   for protocol in pairs(tls.PROTOCOLS) do
+    if protocol == "TLSv1.3" then
+      -- TLSv1.3 does not allow anonymous key exchange and only allows specific
+      -- DHE groups named in RFC 7919
+      goto NEXT_PROTOCOL
+    end
     -- Try anonymous DH ciphersuites
     cipher, dhparams = get_dhe_params(host, port, protocol, dh_anons)
     -- Explicit test for false needed because nil just means no ciphers supported.
--- a/scripts/ssl-enum-ciphers.nse	2023-01-12 18:40:55.000000000 +0100
+++ b/scripts/ssl-enum-ciphers.nse	2024-03-21 17:20:23.478772007 +0100
@@ -399,7 +399,10 @@ local function try_params(host, port, t)
     for j = 1, #record.body do -- no ipairs because we append below
       local b = record.body[j]
       done = ((record.type == "alert" and b.level == "fatal") or
-        (record.type == "handshake" and b.type == "server_hello_done"))
+        (record.type == "handshake" and (b.type == "server_hello_done" or
+            -- TLSv1.3 does not have server_hello_done
+          (t.protocol == "TLSv1.3" and b.type == "server_hello")))
+        )
       table.insert(records[record.type].body, b)
     end
     if done then
@@ -537,7 +540,7 @@ local function score_cipher (kex_strengt
   if not kex_strength or not cipher_info.size then
     return "unknown"
   end
-  if kex_strength == 0 then
+  if kex_strength <= 0 then
     return 0
   elseif kex_strength < 512 then
     kex_score = 0.2
@@ -551,7 +554,7 @@ local function score_cipher (kex_strengt
     kex_score = 1.0
   end
 
-  if cipher_info.size == 0 then
+  if cipher_info.size <= 0 then
     return 0
   elseif cipher_info.size < 128 then
     cipher_score = 0.2
@@ -582,14 +585,27 @@ local function letter_grade (score)
   end
 end
 
+local tls13proto = tls.PROTOCOLS["TLSv1.3"]
+local tls13supported = tls.EXTENSION_HELPERS.supported_versions({"TLSv1.3"})
+local function get_hello_table(host, protocol)
+  local t = {
+    protocol = protocol,
+    record_protocol = protocol, -- improve chances of immediate rejection
+    extensions = base_extensions(host),
+  }
+
+  -- supported_versions extension required for TLSv1.3
+  if (tls.PROTOCOLS[protocol] >= tls13proto) then
+    t.extensions.supported_versions = tls13supported
+  end
+
+  return t
+end
+
 -- Find which ciphers out of group are supported by the server.
 local function find_ciphers_group(host, port, protocol, group, scores)
   local results = {}
-  local t = {
-    ["protocol"] = protocol,
-    ["record_protocol"] = protocol, -- improve chances of immediate rejection
-    ["extensions"] = base_extensions(host),
-  }
+  local t = get_hello_table(host, protocol)
 
   -- This is a hacky sort of tristate variable. There are three conditions:
   -- 1. false = either ciphers or protocol is bad. Keep trying with new ciphers
@@ -609,14 +625,15 @@ local function find_ciphers_group(host,
       local alert = records.alert
       if alert then
         ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
-        if alert["protocol"] ~= protocol then
+        if not tls.record_version_ok(alert["protocol"], protocol) then
           ctx_log(1, protocol, "Protocol mismatch (received %s)", alert.protocol)
           -- Sometimes this is not an actual rejection of the protocol. Check specifically:
           if get_body(alert, "description", "protocol_version") then
             protocol_worked = nil
           end
           break
-        elseif get_body(alert, "description", "handshake_failure") then
+        elseif get_body(alert, "description", "handshake_failure")
+          or get_body(alert, "description", "insufficient_security") then
           protocol_worked = true
           ctx_log(2, protocol, "%d ciphers rejected.", #group)
           break
@@ -673,98 +690,97 @@ local function find_ciphers_group(host,
             elseif info.cipher == "RC4" then
               scores.warnings["Broken cipher RC4 is deprecated by RFC 7465"] = true
             end
+            if protocol == "TLSv1.3" and not info.tls13ok then
+              scores.warnings["Non-TLSv1.3 ciphersuite chosen for TLSv1.3"] = true
+            end
             local kex = tls.KEX_ALGORITHMS[info.kex]
             scores.any_pfs_ciphers = kex.pfs or scores.any_pfs_ciphers
             local extra, kex_strength
-            if kex.anon then
-              kex_strength = 0
-            elseif kex.export then
+            if kex.export then
+              scores.warnings["Export key exchange"] = true
               if info.kex:find("1024$") then
                 kex_strength = 1024
               else
                 kex_strength = 512
               end
-            else
-              if have_ssl and kex.pubkey then
-                local certs = get_body(handshake, "type", "certificate")
-                -- Assume RFC compliance:
-                -- "The sender's certificate MUST come first in the list."
-                -- This may not always be the case, so
-                -- TODO: reorder certificates and validate entire chain
-                -- TODO: certificate validation (date, self-signed, etc)
-                local c, err
-                if certs == nil then
-                  err = "no certificate message"
-                else
-                   c, err = sslcert.parse_ssl_certificate(certs.certificates[1])
+            end
+            if kex.anon then
+              scores.warnings["Anonymous key exchange, score capped at F"] = true
+              kex_strength = 0
+            elseif have_ssl and kex.pubkey then
+              local certs = get_body(handshake, "type", "certificate")
+              -- Assume RFC compliance:
+              -- "The sender's certificate MUST come first in the list."
+              -- This may not always be the case, so
+              -- TODO: reorder certificates and validate entire chain
+              -- TODO: certificate validation (date, self-signed, etc)
+              local c, err
+              if certs == nil then
+                err = "no certificate message"
+              else
+                c, err = sslcert.parse_ssl_certificate(certs.certificates[1])
+              end
+              if not c then
+                ctx_log(1, protocol, "Failed to parse certificate: %s", err)
+              elseif c.pubkey.type == kex.pubkey then
+                local sigalg = c.sig_algorithm:match("([mM][dD][245])") or c.sig_algorithm:match("([sS][hH][aA]1)")
+                if sigalg then
+                  kex_strength = 0
+                  scores.warnings[("Insecure certificate signature (%s), score capped at F"):format(string.upper(sigalg))] = true
                 end
-                if not c then
-                  stdnse.debug1("Failed to parse certificate: %s", err)
-                elseif c.pubkey.type == kex.pubkey then
-                  local sigalg = c.sig_algorithm:match("([mM][dD][245])")
-                  if sigalg then
-                    -- MD2 and MD5 are broken
+                local rsa_bits = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
+                kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
+                if c.pubkey.exponent then
+                  if openssl.bignum_bn2dec(c.pubkey.exponent) == "1" then
                     kex_strength = 0
-                    scores.warnings["Insecure certificate signature: " .. string.upper(sigalg)] = true
+                    scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
+                  end
+                end
+                if c.pubkey.ecdhparams then
+                  if c.pubkey.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
+                    extra = c.pubkey.ecdhparams.curve_params.curve
                   else
-                    sigalg = c.sig_algorithm:match("([sS][hH][aA]1)")
-                    if sigalg then
-                      -- TODO: Update this when SHA-1 is fully deprecated in 2017
-                      if type(c.notBefore) == "table" and c.notBefore.year >= 2016 then
-                        kex_strength = 0
-                        scores.warnings["Deprecated SHA1 signature in certificate issued after January 1, 2016"] = true
-                      end
-                      scores.warnings["Weak certificate signature: SHA1"] = true
-                    end
-                    kex_strength = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
-                    if c.pubkey.exponent then
-                      if openssl.bignum_bn2dec(c.pubkey.exponent) == "1" then
-                        kex_strength = 0
-                        scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
-                      end
-                    end
-                    if c.pubkey.ecdhparams then
-                      if c.pubkey.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
-                        extra = c.pubkey.ecdhparams.curve_params.curve
-                      else
-                        extra = string.format("%s %d", c.pubkey.ecdhparams.curve_params.ec_curve_type, c.pubkey.bits)
-                      end
-                    else
-                      extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
-                    end
+                    extra = string.format("%s %d", c.pubkey.ecdhparams.curve_params.ec_curve_type, c.pubkey.bits)
                   end
+                else
+                  extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
                 end
               end
-              local ske = get_body(handshake, "type", "server_key_exchange")
-              if kex.server_key_exchange and ske then
-                local kex_info = kex.server_key_exchange(ske.data, protocol)
-                if kex_info.strength then
-                  local rsa_bits = tls.rsa_equiv(kex.type, kex_info.strength)
-                  local low_strength_warning = false
-                  if kex_strength and kex_strength > rsa_bits then
-                    kex_strength = rsa_bits
-                    low_strength_warning = true
-                  end
-                  kex_strength = kex_strength or rsa_bits
-                  if kex_info.ecdhparams then
-                    if kex_info.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
-                      extra = kex_info.ecdhparams.curve_params.curve
-                    else
-                      extra = string.format("%s %d", kex_info.ecdhparams.curve_params.ec_curve_type, kex_info.strength)
-                    end
+            end
+            local ske
+            if protocol == "TLSv1.3" then
+              ske = server_hello.extensions.key_share
+            elseif kex.server_key_exchange then
+              ske = get_body(handshake, "type", "server_key_exchange")
+              if ske then
+                ske = ske.data
+              end
+            end
+            if ske then
+              local kex_info = kex.server_key_exchange(ske, protocol)
+              if kex_info.strength then
+                local kex_type = kex_info.type or kex.type
+                if kex_info.ecdhparams then
+                  if kex_info.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
+                    extra = kex_info.ecdhparams.curve_params.curve
                   else
-                    extra = string.format("%s %d", kex.type, kex_info.strength)
-                  end
-                  if low_strength_warning then
-                    scores.warnings[(
-                        "Key exchange (%s) of lower strength than certificate key"
-                      ):format(extra)] = true
+                    extra = string.format("%s %d", kex_info.ecdhparams.curve_params.ec_curve_type, kex_info.strength)
                   end
+                else
+                  extra = string.format("%s %d", kex_type, kex_info.strength)
                 end
-                if kex_info.rsa and kex_info.rsa.exponent == 1 then
-                  kex_strength = 0
-                  scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
+                local rsa_bits = tls.rsa_equiv(kex_type, kex_info.strength)
+                if kex_strength and kex_strength > rsa_bits then
+                  kex_strength = rsa_bits
+                  scores.warnings[(
+                      "Key exchange (%s) of lower strength than certificate key"
+                    ):format(extra)] = true
                 end
+                kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
+              end
+              if kex_info.rsa and kex_info.rsa.exponent == 1 then
+                kex_strength = 0
+                scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
               end
             end
             scores[name] = {
@@ -784,11 +800,8 @@ end
 local function get_chunk_size(host, protocol)
   -- Try to make sure we don't send too big of a handshake
   -- https://github.com/ssllabs/research/wiki/Long-Handshake-Intolerance
-  local len_t = {
-    protocol = protocol,
-    ciphers = {},
-    extensions = base_extensions(host),
-  }
+  local len_t = get_hello_table(host, protocol)
+  len_t.ciphers = {}
   local cipher_len_remaining = 255 - #tls.client_hello(len_t)
   -- if we're over 255 anyway, just go for it.
   -- Each cipher adds 2 bytes
@@ -802,7 +815,17 @@ end
 -- each chunk.
 local function find_ciphers(host, port, protocol)
 
-  local ciphers = in_chunks(sorted_keys(tls.CIPHERS), get_chunk_size(host, protocol))
+  local candidates = {}
+  -- TLSv1.3 ciphers are different, though some are shared (ECCPWD)
+  local tls13 = protocol == "TLSv1.3"
+  for _, c in ipairs(sorted_keys(tls.CIPHERS)) do
+    local info = tls.cipher_info(c)
+    if (not tls13 and not info.tls13only)
+      or (tls13 and info.tls13ok) then
+      candidates[#candidates+1] = c
+    end
+  end
+  local ciphers = in_chunks(candidates, get_chunk_size(host, protocol))
 
   local results = {}
   local scores = {warnings={}}
@@ -823,11 +846,8 @@ end
 
 local function find_compressors(host, port, protocol, good_ciphers)
   local compressors = sorted_keys(tls.COMPRESSORS)
-  local t = {
-    ["protocol"] = protocol,
-    ["ciphers"] = good_ciphers,
-    ["extensions"] = base_extensions(host),
-  }
+  local t = get_hello_table(host, protocol)
+  t.ciphers = good_ciphers
 
   local results = {}
 
@@ -845,7 +865,7 @@ local function find_compressors(host, po
       local alert = records.alert
       if alert then
         ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
-        if alert["protocol"] ~= protocol then
+        if not tls.record_version_ok(alert["protocol"], protocol) then
           ctx_log(1, protocol, "Protocol rejected.")
           protocol_worked = nil
           break
@@ -901,11 +921,8 @@ end
 -- Offer two ciphers and return the one chosen by the server. Returns nil and
 -- an error message in case of a server error.
 local function compare_ciphers(host, port, protocol, cipher_a, cipher_b)
-  local t = {
-    ["protocol"] = protocol,
-    ["ciphers"] = {cipher_a, cipher_b},
-    ["extensions"] = base_extensions(host),
-  }
+  local t = get_hello_table(host, protocol)
+  t.ciphers = {cipher_a, cipher_b}
   local records = try_params(host, port, t)
   local server_hello = records.handshake and get_body(records.handshake, "type", "server_hello")
   if server_hello then
@@ -1003,14 +1020,18 @@ local function try_protocol(host, port,
   end
   -- Find all valid compression methods.
   local compressors
-  -- Reduce chunk size by 1 to allow extra room for the extra compressors (2 bytes)
-  for _, c in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol) - 1)) do
-    compressors = find_compressors(host, port, protocol, c)
-    -- I observed a weird interaction between ECDSA ciphers and DEFLATE compression.
-    -- Some servers would reject the handshake if no non-ECDSA ciphers were available.
-    -- Sending 64 ciphers at a time should be sufficient, but we'll try them all if necessary.
-    if compressors and #compressors ~= 0 then
-      break
+  -- RFC 8446: "For every TLS 1.3 ClientHello, this vector MUST contain exactly
+  -- one byte, set to zero"
+  if (tls.PROTOCOLS[protocol] < tls13proto) then
+    -- Reduce chunk size by 1 to allow extra room for the extra compressors (2 bytes)
+    for _, c in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol) - 1)) do
+      compressors = find_compressors(host, port, protocol, c)
+      -- I observed a weird interaction between ECDSA ciphers and DEFLATE compression.
+      -- Some servers would reject the handshake if no non-ECDSA ciphers were available.
+      -- Sending 64 ciphers at a time should be sufficient, but we'll try them all if necessary.
+      if compressors and #compressors ~= 0 then
+        break
+      end
     end
   end
 
diff --git a/scripts/ssl-heartbleed.nse b/scripts/ssl-heartbleed.nse
index 75d764e4e6..e2d79e7b9c 100644
--- a/scripts/ssl-heartbleed.nse
+++ b/scripts/ssl-heartbleed.nse
@@ -43,6 +43,7 @@ license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
 categories = { "vuln", "safe" }
 dependencies = {"https-redirect"}

+-- TLSv1.3 was not implemented by affected versions of OpenSSL.
 local arg_protocols = stdnse.get_script_args(SCRIPT_NAME .. ".protocols") or {'TLSv1.0', 'TLSv1.1', 'TLSv1.2'}

 portrule = function(host, port)
diff --git a/scripts/ssl-poodle.nse b/scripts/ssl-poodle.nse
index 9a71b49a7a..f9d1b9d0c2 100644
--- a/scripts/ssl-poodle.nse
+++ b/scripts/ssl-poodle.nse
@@ -48,6 +48,7 @@ your TLS ciphersuites.
 -- |       https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3566
 -- |_      https://www.openssl.org/~bodo/ssl-poodle.pdf
 --
+-- @see ssl-enum-ciphers.nse

 author = "Daniel Miller"

diff --git a/scripts/tls-alpn.nse b/scripts/tls-alpn.nse
index 9fb005d1f7..ada12fe191 100644
--- a/scripts/tls-alpn.nse
+++ b/scripts/tls-alpn.nse
@@ -56,10 +56,13 @@ local client_hello = function(host, port, protos)
   local sock, status, response, err, cli_h

   cli_h = tls.client_hello({
-    ["extensions"] = {
-      [ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos)
-    },
-  })
+      -- TLSv1.3 does not send this extension plaintext.
+      -- TODO: implement key exchange crypto to retrieve encrypted extensions
+      protocol = "TLSv1.2",
+      ["extensions"] = {
+        [ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos)
+      },
+    })

   -- Connect to the target server
   local status, err
diff --git a/scripts/tls-nextprotoneg.nse b/scripts/tls-nextprotoneg.nse
index 2ce62c5612..33736a7f7c 100644
--- a/scripts/tls-nextprotoneg.nse
+++ b/scripts/tls-nextprotoneg.nse
@@ -58,6 +58,9 @@ local client_hello = function(host, port)
   local sock, status, response, err, cli_h

   cli_h = tls.client_hello({
+      -- TLSv1.3 does not send this extension plaintext.
+      -- TODO: implement key exchange crypto to retrieve encrypted extensions
+      protocol = "TLSv1.2",
     ["extensions"] = {
       ["next_protocol_negotiation"] = "",
     },
