mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-21 23:36:16 -04:00
264 lines
10 KiB
JavaScript
264 lines
10 KiB
JavaScript
/**
|
||
* 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);
|
||
if (tlsr.handshake.value.handshakeType.value !== 0x01) {
|
||
throw new Error();
|
||
}
|
||
} 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.handshake.value.helloVersion.value;
|
||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||
if (ext.type.value === "supported_versions") {
|
||
version = parseHighestSupportedVersion(ext.value.data);
|
||
break;
|
||
}
|
||
}
|
||
version = tlsVersionMapper(version);
|
||
|
||
/* 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);
|
||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||
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}`,
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* Calculate the JA4Server from a given TLS Server Hello Stream
|
||
* @param {Uint8Array} bytes
|
||
* @returns {string}
|
||
*/
|
||
export function toJA4S(bytes) {
|
||
let tlsr = {};
|
||
try {
|
||
tlsr = parseTLSRecord(bytes);
|
||
if (tlsr.handshake.value.handshakeType.value !== 0x02) {
|
||
throw new Error();
|
||
}
|
||
} catch (err) {
|
||
throw new OperationError("Data is not a valid TLS Server 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.handshake.value.helloVersion.value;
|
||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||
if (ext.type.value === "supported_versions") {
|
||
version = parseHighestSupportedVersion(ext.value.data);
|
||
break;
|
||
}
|
||
}
|
||
version = tlsVersionMapper(version);
|
||
|
||
/* Number of Extensions
|
||
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”.
|
||
*/
|
||
let extLen = tlsr.handshake.value.extensions.value.length;
|
||
extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
|
||
|
||
/* ALPN Extension Chosen 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);
|
||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* Chosen Cipher
|
||
The hex value of the chosen cipher suite
|
||
*/
|
||
const cipher = toHexFast(tlsr.handshake.value.cipherSuite.data);
|
||
|
||
/* Extension hash
|
||
A 12 character truncated sha256 hash of the list of extensions.
|
||
The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited.
|
||
*/
|
||
const extensionsList = [];
|
||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||
extensionsList.push(toHexFast(ext.type.data));
|
||
}
|
||
const extensionsRaw = extensionsList.join(",");
|
||
const extensionsHash = runHash(
|
||
"sha256",
|
||
Utils.strToArrayBuffer(extensionsRaw)
|
||
).substring(0, 12);
|
||
|
||
return {
|
||
"JA4S": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsHash}`,
|
||
"JA4S_r": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsRaw}`,
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* Takes a TLS version value and returns a JA4 TLS version string
|
||
* @param {Uint8Array} version - Two byte array of version number
|
||
* @returns {string}
|
||
*/
|
||
function tlsVersionMapper(version) {
|
||
switch (version) {
|
||
case 0x0304: return "13"; // TLS 1.3
|
||
case 0x0303: return "12"; // TLS 1.2
|
||
case 0x0302: return "11"; // TLS 1.1
|
||
case 0x0301: return "10"; // TLS 1.0
|
||
case 0x0300: return "s3"; // SSL 3.0
|
||
case 0x0200: return "s2"; // SSL 2.0
|
||
case 0x0100: return "s1"; // SSL 1.0
|
||
default: return "00"; // Unknown
|
||
}
|
||
}
|