diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 8e300c76..9a9e7804 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -231,6 +231,7 @@
"VarInt Decode",
"JA3 Fingerprint",
"JA3S Fingerprint",
+ "JA4 Fingerprint",
"HASSH Client Fingerprint",
"HASSH Server Fingerprint",
"Format MAC addresses",
diff --git a/src/core/lib/JA4.mjs b/src/core/lib/JA4.mjs
new file mode 100644
index 00000000..b0b423a2
--- /dev/null
+++ b/src/core/lib/JA4.mjs
@@ -0,0 +1,166 @@
+/**
+ * JA4 resources.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ *
+ * JA4 Copyright 2023 FoxIO, LLC.
+ * @license BSD-3-Clause
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import { parseTLSRecord, parseHighestSupportedVersion, parseFirstALPNValue } from "./TLS.mjs";
+import { toHexFast } from "./Hex.mjs";
+import { runHash } from "./Hash.mjs";
+import Utils from "../Utils.mjs";
+
+
+/**
+ * Calculate the JA4 from a given TLS Client Hello Stream
+ * @param {Uint8Array} bytes
+ * @returns {string}
+ */
+export function toJA4(bytes) {
+ let tlsr = {};
+ try {
+ tlsr = parseTLSRecord(bytes);
+ } catch (err) {
+ throw new OperationError("Data is not a valid TLS Client Hello. QUIC is not yet supported.\n" + err);
+ }
+
+ /* QUIC
+ “q” or “t”, which denotes whether the hello packet is for QUIC or TCP.
+ TODO: Implement QUIC
+ */
+ const ptype = "t";
+
+ /* TLS Version
+ TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
+ is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then
+ the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
+ should be ignored.
+ */
+ let version = tlsr.version.value;
+ for (const ext of tlsr.handshake.value.extensions.value) {
+ if (ext.type.value === "supported_versions") {
+ version = parseHighestSupportedVersion(ext.value.data);
+ break;
+ }
+ }
+ switch (version) {
+ case 0x0304: version = "13"; break; // TLS 1.3
+ case 0x0303: version = "12"; break; // TLS 1.2
+ case 0x0302: version = "11"; break; // TLS 1.1
+ case 0x0301: version = "10"; break; // TLS 1.0
+ case 0x0300: version = "s3"; break; // SSL 3.0
+ case 0x0200: version = "s2"; break; // SSL 2.0
+ case 0x0100: version = "s1"; break; // SSL 1.0
+ default: version = "00"; // Unknown
+ }
+
+ /* SNI
+ If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or “d” in the fingerprint.
+ If the SNI does not exist, then the destination is an IP address, or “i”.
+ */
+ let sni = "i";
+ for (const ext of tlsr.handshake.value.extensions.value) {
+ if (ext.type.value === "server_name") {
+ sni = "d";
+ break;
+ }
+ }
+
+ /* Number of Ciphers
+ 2 character number of cipher suites, so if there’s 6 cipher suites in the hello packet, then the value should be “06”.
+ If there’s > 99, which there should never be, then output “99”. Remember, ignore GREASE values. They don’t count.
+ */
+ let cipherLen = 0;
+ for (const cs of tlsr.handshake.value.cipherSuites.value) {
+ if (cs.value !== "GREASE") cipherLen++;
+ }
+ cipherLen = cipherLen > 99 ? "99" : cipherLen.toString().padStart(2, "0");
+
+ /* Number of Extensions
+ Same as counting ciphers. Ignore GREASE. Include SNI and ALPN.
+ */
+ let extLen = 0;
+ for (const ext of tlsr.handshake.value.extensions.value) {
+ if (ext.type.value !== "GREASE") extLen++;
+ }
+ extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
+
+ /* ALPN Extension Value
+ The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
+ If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint.
+ */
+ let alpn = "00";
+ for (const ext of tlsr.handshake.value.extensions.value) {
+ if (ext.type.value === "application_layer_protocol_negotiation") {
+ alpn = parseFirstALPNValue(ext.value.data);
+ alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
+ break;
+ }
+ }
+
+ /* Cipher hash
+ A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, first 12 characters.
+ The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE.
+ */
+ const originalCiphersList = [];
+ for (const cs of tlsr.handshake.value.cipherSuites.value) {
+ if (cs.value !== "GREASE") {
+ originalCiphersList.push(toHexFast(cs.data));
+ }
+ }
+ const sortedCiphersList = [...originalCiphersList].sort();
+ const sortedCiphersRaw = sortedCiphersList.join(",");
+ const originalCiphersRaw = originalCiphersList.join(",");
+ const sortedCiphers = runHash(
+ "sha256",
+ Utils.strToArrayBuffer(sortedCiphersRaw)
+ ).substring(0, 12);
+ const originalCiphers = runHash(
+ "sha256",
+ Utils.strToArrayBuffer(originalCiphersRaw)
+ ).substring(0, 12);
+
+ /* Extension hash
+ A 12 character truncated sha256 hash of the list of extensions, sorted by hex value, followed by the list of signature
+ algorithms, in the order that they appear (not sorted).
+ The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted
+ (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured
+ them in the a section of the fingerprint. These values are omitted so that the same application would have the same b
+ section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs.
+ */
+ const originalExtensionsList = [];
+ let signatureAlgorithms = "";
+ for (const ext of tlsr.handshake.value.extensions.value) {
+ if (ext.type.value !== "GREASE") {
+ originalExtensionsList.push(toHexFast(ext.type.data));
+ }
+ if (ext.type.value === "signature_algorithms") {
+ signatureAlgorithms = toHexFast(ext.value.data.slice(2));
+ signatureAlgorithms = signatureAlgorithms.replace(/(.{4})/g, "$1,");
+ signatureAlgorithms = signatureAlgorithms.substring(0, signatureAlgorithms.length - 1);
+ }
+ }
+ const sortedExtensionsList = [...originalExtensionsList].filter(e => e !== "0000" && e !== "0010").sort();
+ const sortedExtensionsRaw = sortedExtensionsList.join(",") + "_" + signatureAlgorithms;
+ const originalExtensionsRaw = originalExtensionsList.join(",") + "_" + signatureAlgorithms;
+ const sortedExtensions = runHash(
+ "sha256",
+ Utils.strToArrayBuffer(sortedExtensionsRaw)
+ ).substring(0, 12);
+ const originalExtensions = runHash(
+ "sha256",
+ Utils.strToArrayBuffer(originalExtensionsRaw)
+ ).substring(0, 12);
+
+ return {
+ "JA4": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphers}_${sortedExtensions}`,
+ "JA4_o": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphers}_${originalExtensions}`,
+ "JA4_r": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphersRaw}_${sortedExtensionsRaw}`,
+ "JA4_ro": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphersRaw}_${originalExtensionsRaw}`,
+ };
+}
diff --git a/src/core/lib/Stream.mjs b/src/core/lib/Stream.mjs
index 18ce71c3..8253c4cf 100644
--- a/src/core/lib/Stream.mjs
+++ b/src/core/lib/Stream.mjs
@@ -18,12 +18,23 @@ export default class Stream {
* Stream constructor.
*
* @param {Uint8Array} input
+ * @param {number} pos
+ * @param {number} bitPos
*/
- constructor(input) {
+ constructor(input, pos=0, bitPos=0) {
this.bytes = input;
this.length = this.bytes.length;
- this.position = 0;
- this.bitPos = 0;
+ this.position = pos;
+ this.bitPos = bitPos;
+ }
+
+ /**
+ * Clone this Stream returning a new identical Stream.
+ *
+ * @returns {Stream}
+ */
+ clone() {
+ return new Stream(this.bytes, this.position, this.bitPos);
}
/**
diff --git a/src/core/lib/TLS.mjs b/src/core/lib/TLS.mjs
new file mode 100644
index 00000000..e3f18eb3
--- /dev/null
+++ b/src/core/lib/TLS.mjs
@@ -0,0 +1,776 @@
+/**
+ * TLS resources.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import Stream from "../lib/Stream.mjs";
+
+/**
+ * Parse a TLS Record
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+export function parseTLSRecord(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const r = {};
+
+ // Content type
+ r.contentType = {
+ description: "Content Type",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+ if (r.contentType.value !== 0x16)
+ throw new OperationError("Not handshake data.");
+
+ // Version
+ r.version = {
+ description: "Protocol Version",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Length
+ r.length = {
+ description: "Record Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+ if (s.length !== r.length.value + 5)
+ throw new OperationError("Incorrect handshake length.");
+
+ // Handshake
+ r.handshake = {
+ description: "Handshake",
+ length: r.length.value,
+ data: b.getBytes(r.length.value),
+ value: parseHandshake(s.getBytes(r.length.value))
+ };
+
+ return r;
+}
+
+/**
+ * Parse a TLS Handshake
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseHandshake(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const h = {};
+
+ // Handshake type
+ h.handshakeType = {
+ description: "Client Hello",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+ if (h.handshakeType.value !== 0x01)
+ throw new OperationError("Not a Client Hello.");
+
+ // Handshake length
+ h.handshakeLength = {
+ description: "Handshake Length",
+ length: 3,
+ data: b.getBytes(3),
+ value: s.readInt(3)
+ };
+ if (s.length !== h.handshakeLength.value + 4)
+ throw new OperationError("Not enough data in Client Hello.");
+
+ // Hello version
+ h.helloVersion = {
+ description: "Client Hello Version",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Random
+ h.random = {
+ description: "Client Random",
+ length: 32,
+ data: b.getBytes(32),
+ value: s.getBytes(32)
+ };
+
+ // Session ID Length
+ h.sessionIDLength = {
+ description: "Session ID Length",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Session ID
+ h.sessionID = {
+ description: "Session ID",
+ length: h.sessionIDLength.value,
+ data: b.getBytes(h.sessionIDLength.value),
+ value: s.getBytes(h.sessionIDLength.value)
+ };
+
+ // Cipher Suites Length
+ h.cipherSuitesLength = {
+ description: "Cipher Suites Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Cipher Suites
+ h.cipherSuites = {
+ description: "Cipher Suites",
+ length: h.cipherSuitesLength.value,
+ data: b.getBytes(h.cipherSuitesLength.value),
+ value: parseCipherSuites(s.getBytes(h.cipherSuitesLength.value))
+ };
+
+ // Compression Methods Length
+ h.compressionMethodsLength = {
+ description: "Compression Methods Length",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Compression Methods
+ h.compressionMethods = {
+ description: "Compression Methods",
+ length: h.compressionMethodsLength.value,
+ data: b.getBytes(h.compressionMethodsLength.value),
+ value: parseCompressionMethods(s.getBytes(h.compressionMethodsLength.value))
+ };
+
+ // Extensions Length
+ h.extensionsLength = {
+ description: "Extensions Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Extensions
+ h.extensions = {
+ description: "Extensions",
+ length: h.extensionsLength.value,
+ data: b.getBytes(h.extensionsLength.value),
+ value: parseExtensions(s.getBytes(h.extensionsLength.value))
+ };
+
+ return h;
+}
+
+/**
+ * Parse Cipher Suites
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseCipherSuites(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const cs = [];
+
+ while (s.hasMore()) {
+ cs.push({
+ description: "Cipher Suite",
+ length: 2,
+ data: b.getBytes(2),
+ value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
+ });
+ }
+ return cs;
+}
+
+/**
+ * Parse Compression Methods
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseCompressionMethods(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const cm = [];
+
+ while (s.hasMore()) {
+ cm.push({
+ description: "Compression Method",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1) // TODO: Compression method name here
+ });
+ }
+ return cm;
+}
+
+/**
+ * Parse Extensions
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseExtensions(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+
+ const exts = [];
+ while (s.hasMore()) {
+ const ext = {};
+
+ // Type
+ ext.type = {
+ description: "Extension Type",
+ length: 2,
+ data: b.getBytes(2),
+ value: EXTENSION_LOOKUP[s.readInt(2)] || "unknown"
+ };
+
+ // Length
+ ext.length = {
+ description: "Extension Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Value
+ ext.value = {
+ description: "Extension Value",
+ length: ext.length.value,
+ data: b.getBytes(ext.length.value),
+ value: s.getBytes(ext.length.value)
+ };
+
+ exts.push(ext);
+ }
+
+ return exts;
+}
+
+/**
+ * Extension type lookup table
+ */
+const EXTENSION_LOOKUP = {
+ 0: "server_name",
+ 1: "max_fragment_length",
+ 2: "client_certificate_url",
+ 3: "trusted_ca_keys",
+ 4: "truncated_hmac",
+ 5: "status_request",
+ 6: "user_mapping",
+ 7: "client_authz",
+ 8: "server_authz",
+ 9: "cert_type",
+ 10: "supported_groups",
+ 11: "ec_point_formats",
+ 12: "srp",
+ 13: "signature_algorithms",
+ 14: "use_srtp",
+ 15: "heartbeat",
+ 16: "application_layer_protocol_negotiation",
+ 17: "status_request_v2",
+ 18: "signed_certificate_timestamp",
+ 19: "client_certificate_type",
+ 20: "server_certificate_type",
+ 21: "padding",
+ 22: "encrypt_then_mac",
+ 23: "extended_master_secret",
+ 24: "token_binding",
+ 25: "cached_info",
+ 26: "tls_lts",
+ 27: "compress_certificate",
+ 28: "record_size_limit",
+ 29: "pwd_protect",
+ 30: "pwd_clear",
+ 31: "password_salt",
+ 32: "ticket_pinning",
+ 33: "tls_cert_with_extern_psk",
+ 34: "delegated_credential",
+ 35: "session_ticket",
+ 36: "TLMSP",
+ 37: "TLMSP_proxying",
+ 38: "TLMSP_delegate",
+ 39: "supported_ekt_ciphers",
+ 40: "Reserved",
+ 41: "pre_shared_key",
+ 42: "early_data",
+ 43: "supported_versions",
+ 44: "cookie",
+ 45: "psk_key_exchange_modes",
+ 46: "Reserved",
+ 47: "certificate_authorities",
+ 48: "oid_filters",
+ 49: "post_handshake_auth",
+ 50: "signature_algorithms_cert",
+ 51: "key_share",
+ 52: "transparency_info",
+ 53: "connection_id (deprecated)",
+ 54: "connection_id",
+ 55: "external_id_hash",
+ 56: "external_session_id",
+ 57: "quic_transport_parameters",
+ 58: "ticket_request",
+ 59: "dnssec_chain",
+ 60: "sequence_number_encryption_algorithms",
+ 61: "rrc",
+ 2570: "GREASE",
+ 6682: "GREASE",
+ 10794: "GREASE",
+ 14906: "GREASE",
+ 17513: "application_settings",
+ 19018: "GREASE",
+ 23130: "GREASE",
+ 27242: "GREASE",
+ 31354: "GREASE",
+ 35466: "GREASE",
+ 39578: "GREASE",
+ 43690: "GREASE",
+ 47802: "GREASE",
+ 51914: "GREASE",
+ 56026: "GREASE",
+ 60138: "GREASE",
+ 64250: "GREASE",
+ 64768: "ech_outer_extensions",
+ 65037: "encrypted_client_hello",
+ 65281: "renegotiation_info"
+};
+
+/**
+ * Cipher suites lookup table
+ */
+const CIPHER_SUITES_LOOKUP = {
+ 0x0000: "TLS_NULL_WITH_NULL_NULL",
+ 0x0001: "TLS_RSA_WITH_NULL_MD5",
+ 0x0002: "TLS_RSA_WITH_NULL_SHA",
+ 0x0003: "TLS_RSA_EXPORT_WITH_RC4_40_MD5",
+ 0x0004: "TLS_RSA_WITH_RC4_128_MD5",
+ 0x0005: "TLS_RSA_WITH_RC4_128_SHA",
+ 0x0006: "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5",
+ 0x0007: "TLS_RSA_WITH_IDEA_CBC_SHA",
+ 0x0008: "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0009: "TLS_RSA_WITH_DES_CBC_SHA",
+ 0x000A: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x000B: "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA",
+ 0x000C: "TLS_DH_DSS_WITH_DES_CBC_SHA",
+ 0x000D: "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0x000E: "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x000F: "TLS_DH_RSA_WITH_DES_CBC_SHA",
+ 0x0010: "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x0011: "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0012: "TLS_DHE_DSS_WITH_DES_CBC_SHA",
+ 0x0013: "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0x0014: "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0015: "TLS_DHE_RSA_WITH_DES_CBC_SHA",
+ 0x0016: "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x0017: "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5",
+ 0x0018: "TLS_DH_anon_WITH_RC4_128_MD5",
+ 0x0019: "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
+ 0x001A: "TLS_DH_anon_WITH_DES_CBC_SHA",
+ 0x001B: "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA",
+ 0x001E: "TLS_KRB5_WITH_DES_CBC_SHA",
+ 0x001F: "TLS_KRB5_WITH_3DES_EDE_CBC_SHA",
+ 0x0020: "TLS_KRB5_WITH_RC4_128_SHA",
+ 0x0021: "TLS_KRB5_WITH_IDEA_CBC_SHA",
+ 0x0022: "TLS_KRB5_WITH_DES_CBC_MD5",
+ 0x0023: "TLS_KRB5_WITH_3DES_EDE_CBC_MD5",
+ 0x0024: "TLS_KRB5_WITH_RC4_128_MD5",
+ 0x0025: "TLS_KRB5_WITH_IDEA_CBC_MD5",
+ 0x0026: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA",
+ 0x0027: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA",
+ 0x0028: "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
+ 0x0029: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5",
+ 0x002A: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5",
+ 0x002B: "TLS_KRB5_EXPORT_WITH_RC4_40_MD5",
+ 0x002C: "TLS_PSK_WITH_NULL_SHA",
+ 0x002D: "TLS_DHE_PSK_WITH_NULL_SHA",
+ 0x002E: "TLS_RSA_PSK_WITH_NULL_SHA",
+ 0x002F: "TLS_RSA_WITH_AES_128_CBC_SHA",
+ 0x0030: "TLS_DH_DSS_WITH_AES_128_CBC_SHA",
+ 0x0031: "TLS_DH_RSA_WITH_AES_128_CBC_SHA",
+ 0x0032: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
+ 0x0033: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
+ 0x0034: "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+ 0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA",
+ 0x0036: "TLS_DH_DSS_WITH_AES_256_CBC_SHA",
+ 0x0037: "TLS_DH_RSA_WITH_AES_256_CBC_SHA",
+ 0x0038: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
+ 0x0039: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
+ 0x003A: "TLS_DH_anon_WITH_AES_256_CBC_SHA",
+ 0x003B: "TLS_RSA_WITH_NULL_SHA256",
+ 0x003C: "TLS_RSA_WITH_AES_128_CBC_SHA256",
+ 0x003D: "TLS_RSA_WITH_AES_256_CBC_SHA256",
+ 0x003E: "TLS_DH_DSS_WITH_AES_128_CBC_SHA256",
+ 0x003F: "TLS_DH_RSA_WITH_AES_128_CBC_SHA256",
+ 0x0040: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
+ 0x0041: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0042: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0043: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0044: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0045: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0046: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0067: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
+ 0x0068: "TLS_DH_DSS_WITH_AES_256_CBC_SHA256",
+ 0x0069: "TLS_DH_RSA_WITH_AES_256_CBC_SHA256",
+ 0x006A: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
+ 0x006B: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
+ 0x006C: "TLS_DH_anon_WITH_AES_128_CBC_SHA256",
+ 0x006D: "TLS_DH_anon_WITH_AES_256_CBC_SHA256",
+ 0x0084: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0085: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0086: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0087: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0088: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0089: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA",
+ 0x008A: "TLS_PSK_WITH_RC4_128_SHA",
+ 0x008B: "TLS_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x008C: "TLS_PSK_WITH_AES_128_CBC_SHA",
+ 0x008D: "TLS_PSK_WITH_AES_256_CBC_SHA",
+ 0x008E: "TLS_DHE_PSK_WITH_RC4_128_SHA",
+ 0x008F: "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x0090: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
+ 0x0091: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
+ 0x0092: "TLS_RSA_PSK_WITH_RC4_128_SHA",
+ 0x0093: "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x0094: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA",
+ 0x0095: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA",
+ 0x0096: "TLS_RSA_WITH_SEED_CBC_SHA",
+ 0x0097: "TLS_DH_DSS_WITH_SEED_CBC_SHA",
+ 0x0098: "TLS_DH_RSA_WITH_SEED_CBC_SHA",
+ 0x0099: "TLS_DHE_DSS_WITH_SEED_CBC_SHA",
+ 0x009A: "TLS_DHE_RSA_WITH_SEED_CBC_SHA",
+ 0x009B: "TLS_DH_anon_WITH_SEED_CBC_SHA",
+ 0x009C: "TLS_RSA_WITH_AES_128_GCM_SHA256",
+ 0x009D: "TLS_RSA_WITH_AES_256_GCM_SHA384",
+ 0x009E: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
+ 0x009F: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
+ 0x00A0: "TLS_DH_RSA_WITH_AES_128_GCM_SHA256",
+ 0x00A1: "TLS_DH_RSA_WITH_AES_256_GCM_SHA384",
+ 0x00A2: "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
+ 0x00A3: "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
+ 0x00A4: "TLS_DH_DSS_WITH_AES_128_GCM_SHA256",
+ 0x00A5: "TLS_DH_DSS_WITH_AES_256_GCM_SHA384",
+ 0x00A6: "TLS_DH_anon_WITH_AES_128_GCM_SHA256",
+ 0x00A7: "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
+ 0x00A8: "TLS_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00A9: "TLS_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AA: "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00AB: "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AC: "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00AD: "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AE: "TLS_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00AF: "TLS_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B0: "TLS_PSK_WITH_NULL_SHA256",
+ 0x00B1: "TLS_PSK_WITH_NULL_SHA384",
+ 0x00B2: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00B3: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B4: "TLS_DHE_PSK_WITH_NULL_SHA256",
+ 0x00B5: "TLS_DHE_PSK_WITH_NULL_SHA384",
+ 0x00B6: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00B7: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B8: "TLS_RSA_PSK_WITH_NULL_SHA256",
+ 0x00B9: "TLS_RSA_PSK_WITH_NULL_SHA384",
+ 0x00BA: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BB: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BC: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BD: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BE: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BF: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00C0: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C1: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C2: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C3: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C4: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C5: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C6: "TLS_SM4_GCM_SM3",
+ 0x00C7: "TLS_SM4_CCM_SM3",
+ 0x00FF: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
+ 0x0A0A: "GREASE",
+ 0x1301: "TLS_AES_128_GCM_SHA256",
+ 0x1302: "TLS_AES_256_GCM_SHA384",
+ 0x1303: "TLS_CHACHA20_POLY1305_SHA256",
+ 0x1304: "TLS_AES_128_CCM_SHA256",
+ 0x1305: "TLS_AES_128_CCM_8_SHA256",
+ 0x1306: "TLS_AEGIS_256_SHA512",
+ 0x1307: "TLS_AEGIS_128L_SHA256",
+ 0x1A1A: "GREASE",
+ 0x2A2A: "GREASE",
+ 0x3A3A: "GREASE",
+ 0x4A4A: "GREASE",
+ 0x5600: "TLS_FALLBACK_SCSV",
+ 0x5A5A: "GREASE",
+ 0x6A6A: "GREASE",
+ 0x7A7A: "GREASE",
+ 0x8A8A: "GREASE",
+ 0x9A9A: "GREASE",
+ 0xAAAA: "GREASE",
+ 0xBABA: "GREASE",
+ 0xC001: "TLS_ECDH_ECDSA_WITH_NULL_SHA",
+ 0xC002: "TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
+ 0xC003: "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC004: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
+ 0xC005: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
+ 0xC006: "TLS_ECDHE_ECDSA_WITH_NULL_SHA",
+ 0xC007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
+ 0xC008: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+ 0xC00A: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+ 0xC00B: "TLS_ECDH_RSA_WITH_NULL_SHA",
+ 0xC00C: "TLS_ECDH_RSA_WITH_RC4_128_SHA",
+ 0xC00D: "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC00E: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
+ 0xC00F: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
+ 0xC010: "TLS_ECDHE_RSA_WITH_NULL_SHA",
+ 0xC011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
+ 0xC012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+ 0xC014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+ 0xC015: "TLS_ECDH_anon_WITH_NULL_SHA",
+ 0xC016: "TLS_ECDH_anon_WITH_RC4_128_SHA",
+ 0xC017: "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
+ 0xC018: "TLS_ECDH_anon_WITH_AES_128_CBC_SHA",
+ 0xC019: "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
+ 0xC01A: "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA",
+ 0xC01B: "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC01C: "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0xC01D: "TLS_SRP_SHA_WITH_AES_128_CBC_SHA",
+ 0xC01E: "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA",
+ 0xC01F: "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA",
+ 0xC020: "TLS_SRP_SHA_WITH_AES_256_CBC_SHA",
+ 0xC021: "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA",
+ 0xC022: "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA",
+ 0xC023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+ 0xC024: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+ 0xC025: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
+ 0xC026: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
+ 0xC027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+ 0xC028: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+ 0xC029: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256",
+ 0xC02A: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384",
+ 0xC02B: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ 0xC02C: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+ 0xC02D: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ 0xC02E: "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
+ 0xC02F: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ 0xC030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ 0xC031: "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256",
+ 0xC032: "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384",
+ 0xC033: "TLS_ECDHE_PSK_WITH_RC4_128_SHA",
+ 0xC034: "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0xC035: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
+ 0xC036: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
+ 0xC037: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256",
+ 0xC038: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384",
+ 0xC039: "TLS_ECDHE_PSK_WITH_NULL_SHA",
+ 0xC03A: "TLS_ECDHE_PSK_WITH_NULL_SHA256",
+ 0xC03B: "TLS_ECDHE_PSK_WITH_NULL_SHA384",
+ 0xC03C: "TLS_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC03D: "TLS_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC03E: "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256",
+ 0xC03F: "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384",
+ 0xC040: "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC041: "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC042: "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256",
+ 0xC043: "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384",
+ 0xC044: "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC045: "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC046: "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256",
+ 0xC047: "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384",
+ 0xC048: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC049: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04A: "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04B: "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04C: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04D: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04E: "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04F: "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC050: "TLS_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC051: "TLS_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC052: "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC053: "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC054: "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC055: "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC056: "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256",
+ 0xC057: "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384",
+ 0xC058: "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256",
+ 0xC059: "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384",
+ 0xC05A: "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256",
+ 0xC05B: "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384",
+ 0xC05C: "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC05D: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC05E: "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC05F: "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC060: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC061: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC062: "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC063: "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC064: "TLS_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC065: "TLS_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC066: "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC067: "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC068: "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC069: "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC06A: "TLS_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06B: "TLS_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC06C: "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06D: "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC06E: "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06F: "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC070: "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC071: "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC072: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC073: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC074: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC075: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC076: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC077: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC078: "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC079: "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC07A: "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07B: "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC07C: "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07D: "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC07E: "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07F: "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC080: "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC081: "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC082: "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC083: "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC084: "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC085: "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC086: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC087: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC088: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC089: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08A: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08B: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08C: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08D: "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08E: "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08F: "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC090: "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC091: "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC092: "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC093: "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC094: "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC095: "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC096: "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC097: "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC098: "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC099: "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC09A: "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC09B: "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC09C: "TLS_RSA_WITH_AES_128_CCM",
+ 0xC09D: "TLS_RSA_WITH_AES_256_CCM",
+ 0xC09E: "TLS_DHE_RSA_WITH_AES_128_CCM",
+ 0xC09F: "TLS_DHE_RSA_WITH_AES_256_CCM",
+ 0xC0A0: "TLS_RSA_WITH_AES_128_CCM_8",
+ 0xC0A1: "TLS_RSA_WITH_AES_256_CCM_8",
+ 0xC0A2: "TLS_DHE_RSA_WITH_AES_128_CCM_8",
+ 0xC0A3: "TLS_DHE_RSA_WITH_AES_256_CCM_8",
+ 0xC0A4: "TLS_PSK_WITH_AES_128_CCM",
+ 0xC0A5: "TLS_PSK_WITH_AES_256_CCM",
+ 0xC0A6: "TLS_DHE_PSK_WITH_AES_128_CCM",
+ 0xC0A7: "TLS_DHE_PSK_WITH_AES_256_CCM",
+ 0xC0A8: "TLS_PSK_WITH_AES_128_CCM_8",
+ 0xC0A9: "TLS_PSK_WITH_AES_256_CCM_8",
+ 0xC0AA: "TLS_PSK_DHE_WITH_AES_128_CCM_8",
+ 0xC0AB: "TLS_PSK_DHE_WITH_AES_256_CCM_8",
+ 0xC0AC: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
+ 0xC0AD: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
+ 0xC0AE: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
+ 0xC0AF: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
+ 0xC0B0: "TLS_ECCPWD_WITH_AES_128_GCM_SHA256",
+ 0xC0B1: "TLS_ECCPWD_WITH_AES_256_GCM_SHA384",
+ 0xC0B2: "TLS_ECCPWD_WITH_AES_128_CCM_SHA256",
+ 0xC0B3: "TLS_ECCPWD_WITH_AES_256_CCM_SHA384",
+ 0xC0B4: "TLS_SHA256_SHA256",
+ 0xC0B5: "TLS_SHA384_SHA384",
+ 0xC100: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC",
+ 0xC101: "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC",
+ 0xC102: "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT",
+ 0xC103: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L",
+ 0xC104: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L",
+ 0xC105: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S",
+ 0xC106: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S",
+ 0xCACA: "GREASE",
+ 0xCCA8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCA9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAA: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAB: "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAC: "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAD: "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAE: "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xD001: "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256",
+ 0xD002: "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384",
+ 0xD003: "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256",
+ 0xD005: "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256",
+ 0xDADA: "GREASE",
+ 0xEAEA: "GREASE",
+ 0xFAFA: "GREASE",
+};
+
+/**
+ * GREASE values
+ */
+export const GREASE_VALUES = [
+ 0x0a0a,
+ 0x1a1a,
+ 0x2a2a,
+ 0x3a3a,
+ 0x4a4a,
+ 0x5a5a,
+ 0x6a6a,
+ 0x7a7a,
+ 0x8a8a,
+ 0x9a9a,
+ 0xaaaa,
+ 0xbaba,
+ 0xcaca,
+ 0xdada,
+ 0xeaea,
+ 0xfafa
+];
+
+/**
+ * Parses the supported_versions extension and returns the highest supported version.
+ * @param {Uint8Array} bytes
+ * @returns {number}
+ */
+export function parseHighestSupportedVersion(bytes) {
+ const s = new Stream(bytes);
+
+ // Length
+ let i = s.readInt(1);
+
+ let highestVersion = 0;
+ while (s.hasMore() && i-- > 0) {
+ const v = s.readInt(2);
+ if (GREASE_VALUES.includes(v)) continue;
+ if (v > highestVersion) highestVersion = v;
+ }
+
+ return highestVersion;
+}
+
+/**
+ * Parses the application_layer_protocol_negotiation extension and returns the first value.
+ * @param {Uint8Array} bytes
+ * @returns {number}
+ */
+export function parseFirstALPNValue(bytes) {
+ const s = new Stream(bytes);
+ const alpnExtLen = s.readInt(2);
+ if (alpnExtLen < 3) return "00";
+ const strLen = s.readInt(1);
+ if (strLen < 2) return "00";
+ return s.readString(strLen);
+}
diff --git a/src/core/operations/JA4Fingerprint.mjs b/src/core/operations/JA4Fingerprint.mjs
new file mode 100644
index 00000000..31f57525
--- /dev/null
+++ b/src/core/operations/JA4Fingerprint.mjs
@@ -0,0 +1,73 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import {toJA4} from "../lib/JA4.mjs";
+
+/**
+ * JA4 Fingerprint operation
+ */
+class JA4Fingerprint extends Operation {
+
+ /**
+ * JA4Fingerprint constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "JA4 Fingerprint";
+ this.module = "Crypto";
+ this.description = "Generates a JA4 fingerprint to help identify TLS clients based on hashing together values from the Client Hello.
Input: A hex stream of the TLS or QUIC Client Hello packet application layer.";
+ this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Input format",
+ type: "option",
+ value: ["Hex", "Base64", "Raw"]
+ },
+ {
+ name: "Output format",
+ type: "option",
+ value: ["JA4", "JA4 Original Rendering", "JA4 Raw", "JA4 Raw Original Rendering", "All"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [inputFormat, outputFormat] = args;
+ input = Utils.convertToByteArray(input, inputFormat);
+ const ja4 = toJA4(new Uint8Array(input));
+
+ // Output
+ switch (outputFormat) {
+ case "JA4":
+ return ja4.JA4;
+ case "JA4 Original Rendering":
+ return ja4.JA4_o;
+ case "JA4 Raw":
+ return ja4.JA4_r;
+ case "JA4 Raw Original Rendering":
+ return ja4.JA4_ro;
+ case "All":
+ default:
+ return `JA4: ${ja4.JA4}
+JA4_o: ${ja4.JA4_o}
+JA4_r: ${ja4.JA4_r}
+JA4_ro: ${ja4.JA4_ro}`;
+ }
+ }
+
+}
+
+export default JA4Fingerprint;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index e85b6ad3..ef3ef41c 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -81,6 +81,7 @@ import "./tests/HKDF.mjs";
import "./tests/Image.mjs";
import "./tests/IndexOfCoincidence.mjs";
import "./tests/JA3Fingerprint.mjs";
+import "./tests/JA4Fingerprint.mjs";
import "./tests/JA3SFingerprint.mjs";
import "./tests/JSONBeautify.mjs";
import "./tests/JSONMinify.mjs";
diff --git a/tests/operations/tests/JA4Fingerprint.mjs b/tests/operations/tests/JA4Fingerprint.mjs
new file mode 100644
index 00000000..dba84a38
--- /dev/null
+++ b/tests/operations/tests/JA4Fingerprint.mjs
@@ -0,0 +1,55 @@
+/**
+ * JA4Fingerprint tests.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "JA4 Fingerprint: TLS 1.3",
+ input: "1603010200010001fc0303b2c03e7ba990ef540c316a665d4d925f8e9079ac4b15687e587dc99016e75a6c20d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f00205a5a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193fafa000000000024002200001f636f6e74656e742d6175746f66696c6c2e676f6f676c65617069732e636f6d0033002b00293a3a000100001d0020fb2cd8ef3d605b96ab03119ec4f30a6e2088cb1af86c41a81feace8706068c50000d001200100403080404010503080505010806060100230000000b00020100ff01000100000a000a00083a3a001d00170018001b000302000244690005000302683200120000002d000201010010000e000c02683208687474702f312e31000500050100000000002b0007060a0a03040303001700001a1a000100001500b800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d1516h2_8daaf6152771_e5627efa2ab1",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
+ {
+ name: "JA4 Fingerprint: TLS 1.3 Original Rendering",
+ input: "1603010200010001fc0303b2c03e7ba990ef540c316a665d4d925f8e9079ac4b15687e587dc99016e75a6c20d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f00205a5a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193fafa000000000024002200001f636f6e74656e742d6175746f66696c6c2e676f6f676c65617069732e636f6d0033002b00293a3a000100001d0020fb2cd8ef3d605b96ab03119ec4f30a6e2088cb1af86c41a81feace8706068c50000d001200100403080404010503080505010806060100230000000b00020100ff01000100000a000a00083a3a001d00170018001b000302000244690005000302683200120000002d000201010010000e000c02683208687474702f312e31000500050100000000002b0007060a0a03040303001700001a1a000100001500b800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d1516h2_acb858a92679_5276cb03a33b",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4 Original Rendering"]
+ }
+ ],
+ },
+ {
+ name: "JA4 Fingerprint: TLS 1.2",
+ input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d1715h2_5b57614c22b0_3d5424432f57",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
+ {
+ name: "JA4 Fingerprint: TLS 1.2 Original Rendering",
+ input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d1715h2_5b234860e130_014157ec0da2",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4 Original Rendering"]
+ }
+ ],
+ },
+]);