diff --git a/CHANGELOG b/CHANGELOG index f70082fb4..4be01d468 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#1691] TLS 1.3 now supported by most scripts for which it is + relevant, such as ssl-enum-ciphers. Some functions like ssl tunnel + connections and certificate parsing will require OpenSSL 1.1.1 or later to + fully support TLS 1.3. [Daniel Miller] + o Setting --host-timeout=0 will disable the host timeout, which is set by -T5 to 15 minutes. Earlier versions of Nmap require the user to specify a very long timeout instead. diff --git a/nselib/sslcert.lua b/nselib/sslcert.lua index 88fef6083..a8f84c19f 100644 --- a/nselib/sslcert.lua +++ b/nselib/sslcert.lua @@ -942,7 +942,9 @@ end local function handshake_cert (socket) -- 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 return false, "Failed to send to server" diff --git a/nselib/tls.lua b/nselib/tls.lua index 8e84c2d58..9da79a2a1 100644 --- a/nselib/tls.lua +++ b/nselib/tls.lua @@ -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" local TLS_PROTOCOL_VERSIONS = tableaux.invert(PROTOCOLS) -- @@ -85,12 +86,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, } -- @@ -102,6 +106,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, @@ -112,7 +119,9 @@ TLS_HANDSHAKETYPE_REGISTRY = { ["certificate_url"] = 21, ["certificate_status"] = 22, ["supplemental_data"] = 23, + ["key_update"] = 24, ["next_protocol"] = 67, + ["message_hash"] = 254, } -- @@ -156,13 +165,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, } @@ -173,6 +193,7 @@ DEFAULT_ELLIPTIC_CURVES = { "secp384r1", "secp521r1", "ecdh_x25519", + "ffdhe2048", -- added for TLSv1.3 } --- @@ -204,6 +225,35 @@ SignatureAlgorithms = { ed448 = 8, } +--- +-- 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 @@ -224,6 +274,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, @@ -237,6 +290,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, } @@ -279,6 +344,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 @@ -287,6 +359,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, } -- @@ -654,6 +733,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 @@ -664,21 +749,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) @@ -747,6 +860,24 @@ local function unpack_dhparams (blob, pos) 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) @@ -783,14 +914,8 @@ local function unpack_ecdhparams (blob, pos) 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 @@ -1064,7 +1189,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 @@ -1076,7 +1245,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 @@ -1088,7 +1256,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) @@ -1104,34 +1279,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" @@ -1158,15 +1315,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 @@ -1203,6 +1373,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, @@ -1395,6 +1581,27 @@ function record_read(buffer, i, fragment) return j, h, true 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.) @@ -1406,7 +1613,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) }) @@ -1435,6 +1642,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 @@ -1459,10 +1686,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)) @@ -1471,11 +1699,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 @@ -1501,27 +1742,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 @@ -1548,9 +1837,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 @@ -1624,4 +1918,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 2f4db4570..666630514 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 458242017..bf2bde57e 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. diff --git a/scripts/ssl-enum-ciphers.nse b/scripts/ssl-enum-ciphers.nse index 64c8ac02d..881b6bdcb 100644 --- a/scripts/ssl-enum-ciphers.nse +++ b/scripts/ssl-enum-ciphers.nse @@ -400,7 +400,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 @@ -544,7 +547,7 @@ local function score_cipher (kex_strength, cipher_info) 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 @@ -558,7 +561,7 @@ local function score_cipher (kex_strength, cipher_info) 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 @@ -589,14 +592,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 @@ -616,14 +632,15 @@ local function find_ciphers_group(host, port, protocol, group, scores) 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 @@ -680,98 +697,97 @@ local function find_ciphers_group(host, port, protocol, group, scores) 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 - 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 + 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 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] = { @@ -791,11 +807,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 @@ -809,7 +822,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={}} @@ -830,11 +853,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 = {} @@ -852,7 +872,7 @@ local function find_compressors(host, port, protocol, good_ciphers) 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 @@ -908,11 +928,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 @@ -1010,14 +1027,18 @@ local function try_protocol(host, port, protocol, upresults) 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 75d764e4e..e2d79e7b9 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 9a71b49a7..f9d1b9d0c 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 9fb005d1f..ada12fe19 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 2ce62c561..33736a7f7 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"] = "", },