update structure
This commit is contained in:
3
ts/tls/alerts/index.ts
Normal file
3
ts/tls/alerts/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/**
|
||||
* TLS alerts
|
||||
*/
|
@ -1,52 +1,53 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { TlsAlertLevel, TlsAlertDescription, TlsVersion } from '../utils/tls-utils.js';
|
||||
|
||||
/**
|
||||
* TlsAlert class for managing TLS alert messages
|
||||
* TlsAlert class for creating and sending TLS alert messages
|
||||
*/
|
||||
export class TlsAlert {
|
||||
// TLS Alert Levels
|
||||
static readonly LEVEL_WARNING = 0x01;
|
||||
static readonly LEVEL_FATAL = 0x02;
|
||||
|
||||
// TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2)
|
||||
static readonly CLOSE_NOTIFY = 0x00;
|
||||
static readonly UNEXPECTED_MESSAGE = 0x0A;
|
||||
static readonly BAD_RECORD_MAC = 0x14;
|
||||
static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only
|
||||
static readonly RECORD_OVERFLOW = 0x16;
|
||||
static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below
|
||||
static readonly HANDSHAKE_FAILURE = 0x28;
|
||||
static readonly NO_CERTIFICATE = 0x29; // SSLv3 only
|
||||
static readonly BAD_CERTIFICATE = 0x2A;
|
||||
static readonly UNSUPPORTED_CERTIFICATE = 0x2B;
|
||||
static readonly CERTIFICATE_REVOKED = 0x2C;
|
||||
static readonly CERTIFICATE_EXPIRED = 0x2F;
|
||||
static readonly CERTIFICATE_UNKNOWN = 0x30;
|
||||
static readonly ILLEGAL_PARAMETER = 0x2F;
|
||||
static readonly UNKNOWN_CA = 0x30;
|
||||
static readonly ACCESS_DENIED = 0x31;
|
||||
static readonly DECODE_ERROR = 0x32;
|
||||
static readonly DECRYPT_ERROR = 0x33;
|
||||
static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only
|
||||
static readonly PROTOCOL_VERSION = 0x46;
|
||||
static readonly INSUFFICIENT_SECURITY = 0x47;
|
||||
static readonly INTERNAL_ERROR = 0x50;
|
||||
static readonly INAPPROPRIATE_FALLBACK = 0x56;
|
||||
static readonly USER_CANCELED = 0x5A;
|
||||
static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below
|
||||
static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3
|
||||
static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3
|
||||
static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3
|
||||
static readonly UNRECOGNIZED_NAME = 0x70;
|
||||
static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71;
|
||||
static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below
|
||||
static readonly UNKNOWN_PSK_IDENTITY = 0x73;
|
||||
static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3
|
||||
static readonly NO_APPLICATION_PROTOCOL = 0x78;
|
||||
// Use enum values from TlsAlertLevel
|
||||
static readonly LEVEL_WARNING = TlsAlertLevel.WARNING;
|
||||
static readonly LEVEL_FATAL = TlsAlertLevel.FATAL;
|
||||
|
||||
// Use enum values from TlsAlertDescription
|
||||
static readonly CLOSE_NOTIFY = TlsAlertDescription.CLOSE_NOTIFY;
|
||||
static readonly UNEXPECTED_MESSAGE = TlsAlertDescription.UNEXPECTED_MESSAGE;
|
||||
static readonly BAD_RECORD_MAC = TlsAlertDescription.BAD_RECORD_MAC;
|
||||
static readonly DECRYPTION_FAILED = TlsAlertDescription.DECRYPTION_FAILED;
|
||||
static readonly RECORD_OVERFLOW = TlsAlertDescription.RECORD_OVERFLOW;
|
||||
static readonly DECOMPRESSION_FAILURE = TlsAlertDescription.DECOMPRESSION_FAILURE;
|
||||
static readonly HANDSHAKE_FAILURE = TlsAlertDescription.HANDSHAKE_FAILURE;
|
||||
static readonly NO_CERTIFICATE = TlsAlertDescription.NO_CERTIFICATE;
|
||||
static readonly BAD_CERTIFICATE = TlsAlertDescription.BAD_CERTIFICATE;
|
||||
static readonly UNSUPPORTED_CERTIFICATE = TlsAlertDescription.UNSUPPORTED_CERTIFICATE;
|
||||
static readonly CERTIFICATE_REVOKED = TlsAlertDescription.CERTIFICATE_REVOKED;
|
||||
static readonly CERTIFICATE_EXPIRED = TlsAlertDescription.CERTIFICATE_EXPIRED;
|
||||
static readonly CERTIFICATE_UNKNOWN = TlsAlertDescription.CERTIFICATE_UNKNOWN;
|
||||
static readonly ILLEGAL_PARAMETER = TlsAlertDescription.ILLEGAL_PARAMETER;
|
||||
static readonly UNKNOWN_CA = TlsAlertDescription.UNKNOWN_CA;
|
||||
static readonly ACCESS_DENIED = TlsAlertDescription.ACCESS_DENIED;
|
||||
static readonly DECODE_ERROR = TlsAlertDescription.DECODE_ERROR;
|
||||
static readonly DECRYPT_ERROR = TlsAlertDescription.DECRYPT_ERROR;
|
||||
static readonly EXPORT_RESTRICTION = TlsAlertDescription.EXPORT_RESTRICTION;
|
||||
static readonly PROTOCOL_VERSION = TlsAlertDescription.PROTOCOL_VERSION;
|
||||
static readonly INSUFFICIENT_SECURITY = TlsAlertDescription.INSUFFICIENT_SECURITY;
|
||||
static readonly INTERNAL_ERROR = TlsAlertDescription.INTERNAL_ERROR;
|
||||
static readonly INAPPROPRIATE_FALLBACK = TlsAlertDescription.INAPPROPRIATE_FALLBACK;
|
||||
static readonly USER_CANCELED = TlsAlertDescription.USER_CANCELED;
|
||||
static readonly NO_RENEGOTIATION = TlsAlertDescription.NO_RENEGOTIATION;
|
||||
static readonly MISSING_EXTENSION = TlsAlertDescription.MISSING_EXTENSION;
|
||||
static readonly UNSUPPORTED_EXTENSION = TlsAlertDescription.UNSUPPORTED_EXTENSION;
|
||||
static readonly CERTIFICATE_REQUIRED = TlsAlertDescription.CERTIFICATE_REQUIRED;
|
||||
static readonly UNRECOGNIZED_NAME = TlsAlertDescription.UNRECOGNIZED_NAME;
|
||||
static readonly BAD_CERTIFICATE_STATUS_RESPONSE = TlsAlertDescription.BAD_CERTIFICATE_STATUS_RESPONSE;
|
||||
static readonly BAD_CERTIFICATE_HASH_VALUE = TlsAlertDescription.BAD_CERTIFICATE_HASH_VALUE;
|
||||
static readonly UNKNOWN_PSK_IDENTITY = TlsAlertDescription.UNKNOWN_PSK_IDENTITY;
|
||||
static readonly CERTIFICATE_REQUIRED_1_3 = TlsAlertDescription.CERTIFICATE_REQUIRED_1_3;
|
||||
static readonly NO_APPLICATION_PROTOCOL = TlsAlertDescription.NO_APPLICATION_PROTOCOL;
|
||||
|
||||
/**
|
||||
* Create a TLS alert buffer with the specified level and description code
|
||||
*
|
||||
*
|
||||
* @param level Alert level (warning or fatal)
|
||||
* @param description Alert description code
|
||||
* @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303)
|
||||
@ -55,7 +56,7 @@ export class TlsAlert {
|
||||
static create(
|
||||
level: number,
|
||||
description: number,
|
||||
tlsVersion: [number, number] = [0x03, 0x03]
|
||||
tlsVersion: [number, number] = [TlsVersion.TLS1_2[0], TlsVersion.TLS1_2[1]]
|
||||
): Buffer {
|
||||
return Buffer.from([
|
||||
0x15, // Alert record type
|
||||
|
33
ts/tls/index.ts
Normal file
33
ts/tls/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* TLS module providing SNI extraction, TLS alerts, and other TLS-related utilities
|
||||
*/
|
||||
|
||||
// Export TLS alert functionality
|
||||
export * from './alerts/tls-alert.js';
|
||||
|
||||
// Export SNI handling
|
||||
export * from './sni/sni-handler.js';
|
||||
export * from './sni/sni-extraction.js';
|
||||
export * from './sni/client-hello-parser.js';
|
||||
|
||||
// Export TLS utilities
|
||||
export * from './utils/tls-utils.js';
|
||||
|
||||
// Create a namespace for SNI utilities
|
||||
import { SniHandler } from './sni/sni-handler.js';
|
||||
import { SniExtraction } from './sni/sni-extraction.js';
|
||||
import { ClientHelloParser } from './sni/client-hello-parser.js';
|
||||
|
||||
// Export utility objects for convenience
|
||||
export const SNI = {
|
||||
// Main handler class (for backward compatibility)
|
||||
Handler: SniHandler,
|
||||
|
||||
// Utility classes
|
||||
Extraction: SniExtraction,
|
||||
Parser: ClientHelloParser,
|
||||
|
||||
// Convenience functions
|
||||
extractSNI: SniHandler.extractSNI,
|
||||
processTlsPacket: SniHandler.processTlsPacket,
|
||||
};
|
629
ts/tls/sni/client-hello-parser.ts
Normal file
629
ts/tls/sni/client-hello-parser.ts
Normal file
@ -0,0 +1,629 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import {
|
||||
TlsRecordType,
|
||||
TlsHandshakeType,
|
||||
TlsExtensionType,
|
||||
TlsUtils
|
||||
} from '../utils/tls-utils.js';
|
||||
|
||||
/**
|
||||
* Interface for logging functions used by the parser
|
||||
*/
|
||||
export type LoggerFunction = (message: string) => void;
|
||||
|
||||
/**
|
||||
* Result of a session resumption check
|
||||
*/
|
||||
export interface SessionResumptionResult {
|
||||
isResumption: boolean;
|
||||
hasSNI: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about parsed TLS extensions
|
||||
*/
|
||||
export interface ExtensionInfo {
|
||||
type: number;
|
||||
length: number;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a ClientHello parse operation
|
||||
*/
|
||||
export interface ClientHelloParseResult {
|
||||
isValid: boolean;
|
||||
version?: [number, number];
|
||||
random?: Buffer;
|
||||
sessionId?: Buffer;
|
||||
hasSessionId: boolean;
|
||||
cipherSuites?: Buffer;
|
||||
compressionMethods?: Buffer;
|
||||
extensions: ExtensionInfo[];
|
||||
serverNameList?: string[];
|
||||
hasSessionTicket: boolean;
|
||||
hasPsk: boolean;
|
||||
hasEarlyData: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragment tracking information
|
||||
*/
|
||||
export interface FragmentTrackingInfo {
|
||||
buffer: Buffer;
|
||||
timestamp: number;
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for parsing TLS ClientHello messages
|
||||
*/
|
||||
export class ClientHelloParser {
|
||||
// Buffer for handling fragmented ClientHello messages
|
||||
private static fragmentedBuffers: Map<string, FragmentTrackingInfo> = new Map();
|
||||
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
||||
|
||||
/**
|
||||
* Clean up expired fragments
|
||||
*/
|
||||
private static cleanupExpiredFragments(): void {
|
||||
const now = Date.now();
|
||||
for (const [connectionId, info] of this.fragmentedBuffers.entries()) {
|
||||
if (now - info.timestamp > this.fragmentTimeout) {
|
||||
this.fragmentedBuffers.delete(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
||||
* TLS record fragments that might span multiple TCP packets.
|
||||
*
|
||||
* @param buffer The current buffer fragment
|
||||
* @param connectionId Unique identifier for the connection
|
||||
* @param logger Optional logging function
|
||||
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
|
||||
*/
|
||||
public static handleFragmentedClientHello(
|
||||
buffer: Buffer,
|
||||
connectionId: string,
|
||||
logger?: LoggerFunction
|
||||
): Buffer | undefined {
|
||||
const log = logger || (() => {});
|
||||
|
||||
// Periodically clean up expired fragments
|
||||
this.cleanupExpiredFragments();
|
||||
|
||||
// Check if we've seen this connection before
|
||||
if (!this.fragmentedBuffers.has(connectionId)) {
|
||||
// New connection, start with this buffer
|
||||
this.fragmentedBuffers.set(connectionId, {
|
||||
buffer,
|
||||
timestamp: Date.now(),
|
||||
connectionId
|
||||
});
|
||||
|
||||
// Evaluate if this buffer already contains a complete ClientHello
|
||||
try {
|
||||
if (buffer.length >= 5) {
|
||||
// Get the record length from TLS header
|
||||
const recordLength = (buffer[3] << 8) + buffer[4] + 5; // +5 for the TLS record header itself
|
||||
log(`Initial buffer size: ${buffer.length}, expected record length: ${recordLength}`);
|
||||
|
||||
// Check if this buffer already contains a complete TLS record
|
||||
if (buffer.length >= recordLength) {
|
||||
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
|
||||
return buffer;
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
`Initial buffer too small (${buffer.length} bytes), needs at least 5 bytes for TLS header`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error checking initial buffer completeness: ${e}`);
|
||||
}
|
||||
|
||||
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
|
||||
return undefined; // Need more fragments
|
||||
} else {
|
||||
// Existing connection, append this buffer
|
||||
const existingInfo = this.fragmentedBuffers.get(connectionId)!;
|
||||
const newBuffer = Buffer.concat([existingInfo.buffer, buffer]);
|
||||
|
||||
// Update the buffer and timestamp
|
||||
this.fragmentedBuffers.set(connectionId, {
|
||||
...existingInfo,
|
||||
buffer: newBuffer,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
|
||||
|
||||
// Check if we now have a complete ClientHello
|
||||
try {
|
||||
if (newBuffer.length >= 5) {
|
||||
// Get the record length from TLS header
|
||||
const recordLength = (newBuffer[3] << 8) + newBuffer[4] + 5; // +5 for the TLS record header itself
|
||||
log(
|
||||
`Reassembled buffer size: ${newBuffer.length}, expected record length: ${recordLength}`
|
||||
);
|
||||
|
||||
// Check if we have a complete TLS record now
|
||||
if (newBuffer.length >= recordLength) {
|
||||
log(
|
||||
`Assembled complete ClientHello, length: ${newBuffer.length}, needed: ${recordLength}`
|
||||
);
|
||||
|
||||
// Extract the complete TLS record (might be followed by more data)
|
||||
const completeRecord = newBuffer.slice(0, recordLength);
|
||||
|
||||
// Check if this record is indeed a ClientHello (type 1) at position 5
|
||||
if (
|
||||
completeRecord.length > 5 &&
|
||||
completeRecord[5] === TlsHandshakeType.CLIENT_HELLO
|
||||
) {
|
||||
log(`Verified record is a ClientHello handshake message`);
|
||||
|
||||
// Complete message received, remove from tracking
|
||||
this.fragmentedBuffers.delete(connectionId);
|
||||
return completeRecord;
|
||||
} else {
|
||||
log(`Record is complete but not a ClientHello handshake, continuing to buffer`);
|
||||
// This might be another TLS record type preceding the ClientHello
|
||||
|
||||
// Try checking for a ClientHello starting at the end of this record
|
||||
if (newBuffer.length > recordLength + 5) {
|
||||
const nextRecordType = newBuffer[recordLength];
|
||||
log(
|
||||
`Next record type: ${nextRecordType} (looking for ${TlsRecordType.HANDSHAKE})`
|
||||
);
|
||||
|
||||
if (nextRecordType === TlsRecordType.HANDSHAKE) {
|
||||
const handshakeType = newBuffer[recordLength + 5];
|
||||
log(
|
||||
`Next handshake type: ${handshakeType} (looking for ${TlsHandshakeType.CLIENT_HELLO})`
|
||||
);
|
||||
|
||||
if (handshakeType === TlsHandshakeType.CLIENT_HELLO) {
|
||||
// Found a ClientHello in the next record, return the entire buffer
|
||||
log(`Found ClientHello in subsequent record, returning full buffer`);
|
||||
this.fragmentedBuffers.delete(connectionId);
|
||||
return newBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error checking reassembled buffer completeness: ${e}`);
|
||||
}
|
||||
|
||||
return undefined; // Still need more fragments
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a TLS ClientHello message and extracts all components
|
||||
*
|
||||
* @param buffer The buffer containing the ClientHello message
|
||||
* @param logger Optional logging function
|
||||
* @returns Parsed ClientHello or undefined if parsing failed
|
||||
*/
|
||||
public static parseClientHello(
|
||||
buffer: Buffer,
|
||||
logger?: LoggerFunction
|
||||
): ClientHelloParseResult {
|
||||
const log = logger || (() => {});
|
||||
const result: ClientHelloParseResult = {
|
||||
isValid: false,
|
||||
hasSessionId: false,
|
||||
extensions: [],
|
||||
hasSessionTicket: false,
|
||||
hasPsk: false,
|
||||
hasEarlyData: false
|
||||
};
|
||||
|
||||
try {
|
||||
// Check basic validity
|
||||
if (buffer.length < 5) {
|
||||
result.error = 'Buffer too small for TLS record header';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check record type (must be HANDSHAKE)
|
||||
if (buffer[0] !== TlsRecordType.HANDSHAKE) {
|
||||
result.error = `Not a TLS handshake record: ${buffer[0]}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get TLS version from record header
|
||||
const majorVersion = buffer[1];
|
||||
const minorVersion = buffer[2];
|
||||
result.version = [majorVersion, minorVersion];
|
||||
log(`TLS record version: ${majorVersion}.${minorVersion}`);
|
||||
|
||||
// Parse record length (bytes 3-4, big-endian)
|
||||
const recordLength = (buffer[3] << 8) + buffer[4];
|
||||
log(`Record length: ${recordLength}`);
|
||||
|
||||
// Validate record length against buffer size
|
||||
if (buffer.length < recordLength + 5) {
|
||||
result.error = 'Buffer smaller than expected record length';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Start of handshake message in the buffer
|
||||
let pos = 5;
|
||||
|
||||
// Check handshake type (must be CLIENT_HELLO)
|
||||
if (buffer[pos] !== TlsHandshakeType.CLIENT_HELLO) {
|
||||
result.error = `Not a ClientHello message: ${buffer[pos]}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Skip handshake type (1 byte)
|
||||
pos += 1;
|
||||
|
||||
// Parse handshake length (3 bytes, big-endian)
|
||||
const handshakeLength = (buffer[pos] << 16) + (buffer[pos + 1] << 8) + buffer[pos + 2];
|
||||
log(`Handshake length: ${handshakeLength}`);
|
||||
|
||||
// Skip handshake length (3 bytes)
|
||||
pos += 3;
|
||||
|
||||
// Check client version (2 bytes)
|
||||
const clientMajorVersion = buffer[pos];
|
||||
const clientMinorVersion = buffer[pos + 1];
|
||||
log(`Client version: ${clientMajorVersion}.${clientMinorVersion}`);
|
||||
|
||||
// Skip client version (2 bytes)
|
||||
pos += 2;
|
||||
|
||||
// Extract client random (32 bytes)
|
||||
if (pos + 32 > buffer.length) {
|
||||
result.error = 'Buffer too small for client random';
|
||||
return result;
|
||||
}
|
||||
|
||||
result.random = buffer.slice(pos, pos + 32);
|
||||
log(`Client random: ${result.random.toString('hex')}`);
|
||||
|
||||
// Skip client random (32 bytes)
|
||||
pos += 32;
|
||||
|
||||
// Parse session ID
|
||||
if (pos + 1 > buffer.length) {
|
||||
result.error = 'Buffer too small for session ID length';
|
||||
return result;
|
||||
}
|
||||
|
||||
const sessionIdLength = buffer[pos];
|
||||
log(`Session ID length: ${sessionIdLength}`);
|
||||
pos += 1;
|
||||
|
||||
result.hasSessionId = sessionIdLength > 0;
|
||||
|
||||
if (sessionIdLength > 0) {
|
||||
if (pos + sessionIdLength > buffer.length) {
|
||||
result.error = 'Buffer too small for session ID';
|
||||
return result;
|
||||
}
|
||||
|
||||
result.sessionId = buffer.slice(pos, pos + sessionIdLength);
|
||||
log(`Session ID: ${result.sessionId.toString('hex')}`);
|
||||
}
|
||||
|
||||
// Skip session ID
|
||||
pos += sessionIdLength;
|
||||
|
||||
// Check if we have enough bytes left for cipher suites
|
||||
if (pos + 2 > buffer.length) {
|
||||
result.error = 'Buffer too small for cipher suites length';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse cipher suites length (2 bytes, big-endian)
|
||||
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||
log(`Cipher suites length: ${cipherSuitesLength}`);
|
||||
pos += 2;
|
||||
|
||||
// Extract cipher suites
|
||||
if (pos + cipherSuitesLength > buffer.length) {
|
||||
result.error = 'Buffer too small for cipher suites';
|
||||
return result;
|
||||
}
|
||||
|
||||
result.cipherSuites = buffer.slice(pos, pos + cipherSuitesLength);
|
||||
|
||||
// Skip cipher suites
|
||||
pos += cipherSuitesLength;
|
||||
|
||||
// Check if we have enough bytes left for compression methods
|
||||
if (pos + 1 > buffer.length) {
|
||||
result.error = 'Buffer too small for compression methods length';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse compression methods length (1 byte)
|
||||
const compressionMethodsLength = buffer[pos];
|
||||
log(`Compression methods length: ${compressionMethodsLength}`);
|
||||
pos += 1;
|
||||
|
||||
// Extract compression methods
|
||||
if (pos + compressionMethodsLength > buffer.length) {
|
||||
result.error = 'Buffer too small for compression methods';
|
||||
return result;
|
||||
}
|
||||
|
||||
result.compressionMethods = buffer.slice(pos, pos + compressionMethodsLength);
|
||||
|
||||
// Skip compression methods
|
||||
pos += compressionMethodsLength;
|
||||
|
||||
// Check if we have enough bytes for extensions length
|
||||
if (pos + 2 > buffer.length) {
|
||||
// No extensions present - this is valid for older TLS versions
|
||||
result.isValid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse extensions length (2 bytes, big-endian)
|
||||
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||
log(`Extensions length: ${extensionsLength}`);
|
||||
pos += 2;
|
||||
|
||||
// Extensions end position
|
||||
const extensionsEnd = pos + extensionsLength;
|
||||
|
||||
// Check if extensions length is valid
|
||||
if (extensionsEnd > buffer.length) {
|
||||
result.error = 'Extensions length exceeds buffer size';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Iterate through extensions
|
||||
const serverNames: string[] = [];
|
||||
|
||||
while (pos + 4 <= extensionsEnd) {
|
||||
// Parse extension type (2 bytes, big-endian)
|
||||
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
||||
log(`Extension type: 0x${extensionType.toString(16).padStart(4, '0')}`);
|
||||
pos += 2;
|
||||
|
||||
// Parse extension length (2 bytes, big-endian)
|
||||
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||
log(`Extension length: ${extensionLength}`);
|
||||
pos += 2;
|
||||
|
||||
// Extract extension data
|
||||
if (pos + extensionLength > extensionsEnd) {
|
||||
result.error = `Extension ${extensionType} data exceeds bounds`;
|
||||
return result;
|
||||
}
|
||||
|
||||
const extensionData = buffer.slice(pos, pos + extensionLength);
|
||||
|
||||
// Record all extensions
|
||||
result.extensions.push({
|
||||
type: extensionType,
|
||||
length: extensionLength,
|
||||
data: extensionData
|
||||
});
|
||||
|
||||
// Track specific extension types
|
||||
if (extensionType === TlsExtensionType.SERVER_NAME) {
|
||||
// Server Name Indication (SNI)
|
||||
this.parseServerNameExtension(extensionData, serverNames, logger);
|
||||
} else if (extensionType === TlsExtensionType.SESSION_TICKET) {
|
||||
// Session ticket
|
||||
result.hasSessionTicket = true;
|
||||
} else if (extensionType === TlsExtensionType.PRE_SHARED_KEY) {
|
||||
// TLS 1.3 PSK
|
||||
result.hasPsk = true;
|
||||
} else if (extensionType === TlsExtensionType.EARLY_DATA) {
|
||||
// TLS 1.3 Early Data (0-RTT)
|
||||
result.hasEarlyData = true;
|
||||
}
|
||||
|
||||
// Move to next extension
|
||||
pos += extensionLength;
|
||||
}
|
||||
|
||||
// Store any server names found
|
||||
if (serverNames.length > 0) {
|
||||
result.serverNameList = serverNames;
|
||||
}
|
||||
|
||||
// Mark as valid if we get here
|
||||
result.isValid = true;
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log(`Error parsing ClientHello: ${errorMessage}`);
|
||||
result.error = errorMessage;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the server name extension data and extracts hostnames
|
||||
*
|
||||
* @param data Extension data buffer
|
||||
* @param serverNames Array to populate with found server names
|
||||
* @param logger Optional logging function
|
||||
* @returns true if parsing succeeded
|
||||
*/
|
||||
private static parseServerNameExtension(
|
||||
data: Buffer,
|
||||
serverNames: string[],
|
||||
logger?: LoggerFunction
|
||||
): boolean {
|
||||
const log = logger || (() => {});
|
||||
|
||||
try {
|
||||
// Need at least 2 bytes for server name list length
|
||||
if (data.length < 2) {
|
||||
log('SNI extension too small for server name list length');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse server name list length (2 bytes)
|
||||
const listLength = (data[0] << 8) + data[1];
|
||||
|
||||
// Skip to first name entry
|
||||
let pos = 2;
|
||||
|
||||
// End of list
|
||||
const listEnd = pos + listLength;
|
||||
|
||||
// Validate length
|
||||
if (listEnd > data.length) {
|
||||
log('SNI server name list exceeds extension data');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process all name entries
|
||||
while (pos + 3 <= listEnd) {
|
||||
// Name type (1 byte)
|
||||
const nameType = data[pos];
|
||||
pos += 1;
|
||||
|
||||
// For hostname, type must be 0
|
||||
if (nameType !== 0) {
|
||||
// Skip this entry
|
||||
if (pos + 2 <= listEnd) {
|
||||
const nameLength = (data[pos] << 8) + data[pos + 1];
|
||||
pos += 2 + nameLength;
|
||||
continue;
|
||||
} else {
|
||||
log('Malformed SNI entry');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse hostname length (2 bytes)
|
||||
if (pos + 2 > listEnd) {
|
||||
log('SNI extension truncated');
|
||||
return false;
|
||||
}
|
||||
|
||||
const nameLength = (data[pos] << 8) + data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
// Extract hostname
|
||||
if (pos + nameLength > listEnd) {
|
||||
log('SNI hostname truncated');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the hostname as UTF-8
|
||||
try {
|
||||
const hostname = data.slice(pos, pos + nameLength).toString('utf8');
|
||||
log(`Found SNI hostname: ${hostname}`);
|
||||
serverNames.push(hostname);
|
||||
} catch (err) {
|
||||
log(`Error extracting hostname: ${err}`);
|
||||
}
|
||||
|
||||
// Move to next entry
|
||||
pos += nameLength;
|
||||
}
|
||||
|
||||
return serverNames.length > 0;
|
||||
} catch (error) {
|
||||
log(`Error parsing SNI extension: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a ClientHello contains session resumption indicators
|
||||
*
|
||||
* @param buffer The ClientHello buffer
|
||||
* @param logger Optional logging function
|
||||
* @returns Session resumption result
|
||||
*/
|
||||
public static hasSessionResumption(
|
||||
buffer: Buffer,
|
||||
logger?: LoggerFunction
|
||||
): SessionResumptionResult {
|
||||
const log = logger || (() => {});
|
||||
|
||||
if (!TlsUtils.isClientHello(buffer)) {
|
||||
return { isResumption: false, hasSNI: false };
|
||||
}
|
||||
|
||||
const parseResult = this.parseClientHello(buffer, logger);
|
||||
if (!parseResult.isValid) {
|
||||
log(`ClientHello parse failed: ${parseResult.error}`);
|
||||
return { isResumption: false, hasSNI: false };
|
||||
}
|
||||
|
||||
// Check resumption indicators
|
||||
const hasSessionId = parseResult.hasSessionId;
|
||||
const hasSessionTicket = parseResult.hasSessionTicket;
|
||||
const hasPsk = parseResult.hasPsk;
|
||||
const hasEarlyData = parseResult.hasEarlyData;
|
||||
|
||||
// Check for SNI
|
||||
const hasSNI = !!parseResult.serverNameList && parseResult.serverNameList.length > 0;
|
||||
|
||||
// Consider it a resumption if any resumption mechanism is present
|
||||
const isResumption = hasSessionTicket || hasPsk || hasEarlyData ||
|
||||
(hasSessionId && !hasPsk); // Legacy resumption
|
||||
|
||||
// Log details
|
||||
if (isResumption) {
|
||||
log(
|
||||
'Session resumption detected: ' +
|
||||
(hasSessionTicket ? 'session ticket, ' : '') +
|
||||
(hasPsk ? 'PSK, ' : '') +
|
||||
(hasEarlyData ? 'early data, ' : '') +
|
||||
(hasSessionId ? 'session ID' : '') +
|
||||
(hasSNI ? ', with SNI' : ', without SNI')
|
||||
);
|
||||
}
|
||||
|
||||
return { isResumption, hasSNI };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a ClientHello appears to be from a tab reactivation
|
||||
*
|
||||
* @param buffer The ClientHello buffer
|
||||
* @param logger Optional logging function
|
||||
* @returns true if it appears to be a tab reactivation
|
||||
*/
|
||||
public static isTabReactivationHandshake(
|
||||
buffer: Buffer,
|
||||
logger?: LoggerFunction
|
||||
): boolean {
|
||||
const log = logger || (() => {});
|
||||
|
||||
if (!TlsUtils.isClientHello(buffer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the ClientHello
|
||||
const parseResult = this.parseClientHello(buffer, logger);
|
||||
if (!parseResult.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tab reactivation pattern: session identifier + (ticket or PSK) but no SNI
|
||||
const hasSessionId = parseResult.hasSessionId;
|
||||
const hasSessionTicket = parseResult.hasSessionTicket;
|
||||
const hasPsk = parseResult.hasPsk;
|
||||
const hasSNI = !!parseResult.serverNameList && parseResult.serverNameList.length > 0;
|
||||
|
||||
if ((hasSessionId && (hasSessionTicket || hasPsk)) && !hasSNI) {
|
||||
log('Detected tab reactivation pattern: session resumption without SNI');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
3
ts/tls/sni/index.ts
Normal file
3
ts/tls/sni/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/**
|
||||
* SNI handling
|
||||
*/
|
353
ts/tls/sni/sni-extraction.ts
Normal file
353
ts/tls/sni/sni-extraction.ts
Normal file
@ -0,0 +1,353 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js';
|
||||
import {
|
||||
ClientHelloParser,
|
||||
type LoggerFunction
|
||||
} from './client-hello-parser.js';
|
||||
|
||||
/**
|
||||
* Connection tracking information
|
||||
*/
|
||||
export interface ConnectionInfo {
|
||||
sourceIp: string;
|
||||
sourcePort: number;
|
||||
destIp: string;
|
||||
destPort: number;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for extracting SNI information from TLS handshakes
|
||||
*/
|
||||
export class SniExtraction {
|
||||
/**
|
||||
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
|
||||
*
|
||||
* @param buffer The buffer containing the TLS ClientHello message
|
||||
* @param logger Optional logging function
|
||||
* @returns The extracted server name or undefined if not found
|
||||
*/
|
||||
public static extractSNI(buffer: Buffer, logger?: LoggerFunction): string | undefined {
|
||||
const log = logger || (() => {});
|
||||
|
||||
try {
|
||||
// Parse the ClientHello
|
||||
const parseResult = ClientHelloParser.parseClientHello(buffer, logger);
|
||||
if (!parseResult.isValid) {
|
||||
log(`Failed to parse ClientHello: ${parseResult.error}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if ServerName extension was found
|
||||
if (parseResult.serverNameList && parseResult.serverNameList.length > 0) {
|
||||
// Use the first hostname (most common case)
|
||||
const serverName = parseResult.serverNameList[0];
|
||||
log(`Found SNI: ${serverName}`);
|
||||
return serverName;
|
||||
}
|
||||
|
||||
log('No SNI extension found in ClientHello');
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
log(`Error extracting SNI: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
|
||||
*
|
||||
* In TLS 1.3, when a client attempts to resume a session, it may include
|
||||
* the server name in the PSK identity hint rather than in the SNI extension.
|
||||
*
|
||||
* @param buffer The buffer containing the TLS ClientHello message
|
||||
* @param logger Optional logging function
|
||||
* @returns The extracted server name or undefined if not found
|
||||
*/
|
||||
public static extractSNIFromPSKExtension(
|
||||
buffer: Buffer,
|
||||
logger?: LoggerFunction
|
||||
): string | undefined {
|
||||
const log = logger || (() => {});
|
||||
|
||||
try {
|
||||
// Ensure this is a ClientHello
|
||||
if (!TlsUtils.isClientHello(buffer)) {
|
||||
log('Not a ClientHello message');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Parse the ClientHello to find PSK extension
|
||||
const parseResult = ClientHelloParser.parseClientHello(buffer, logger);
|
||||
if (!parseResult.isValid || !parseResult.extensions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the PSK extension
|
||||
const pskExtension = parseResult.extensions.find(ext =>
|
||||
ext.type === TlsExtensionType.PRE_SHARED_KEY);
|
||||
|
||||
if (!pskExtension) {
|
||||
log('No PSK extension found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Parse the PSK extension data
|
||||
const data = pskExtension.data;
|
||||
|
||||
// PSK extension structure:
|
||||
// 2 bytes: identities list length
|
||||
if (data.length < 2) return undefined;
|
||||
|
||||
const identitiesLength = (data[0] << 8) + data[1];
|
||||
let pos = 2;
|
||||
|
||||
// End of identities list
|
||||
const identitiesEnd = pos + identitiesLength;
|
||||
if (identitiesEnd > data.length) return undefined;
|
||||
|
||||
// Process each PSK identity
|
||||
while (pos + 2 <= identitiesEnd) {
|
||||
// Identity length (2 bytes)
|
||||
if (pos + 2 > identitiesEnd) break;
|
||||
|
||||
const identityLength = (data[pos] << 8) + data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
if (pos + identityLength > identitiesEnd) break;
|
||||
|
||||
// Try to extract hostname from identity
|
||||
// Chrome often embeds the hostname in the PSK identity
|
||||
// This is a heuristic as there's no standard format
|
||||
if (identityLength > 0) {
|
||||
const identity = data.slice(pos, pos + identityLength);
|
||||
|
||||
// Skip identity bytes
|
||||
pos += identityLength;
|
||||
|
||||
// Skip obfuscated ticket age (4 bytes)
|
||||
if (pos + 4 <= identitiesEnd) {
|
||||
pos += 4;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to parse the identity as UTF-8
|
||||
try {
|
||||
const identityStr = identity.toString('utf8');
|
||||
log(`PSK identity: ${identityStr}`);
|
||||
|
||||
// Check if the identity contains hostname hints
|
||||
// Chrome often embeds the hostname in a known format
|
||||
// Try to extract using common patterns
|
||||
|
||||
// Pattern 1: Look for domain name pattern
|
||||
const domainPattern =
|
||||
/([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
|
||||
const domainMatch = identityStr.match(domainPattern);
|
||||
if (domainMatch && domainMatch[0]) {
|
||||
log(`Found domain in PSK identity: ${domainMatch[0]}`);
|
||||
return domainMatch[0];
|
||||
}
|
||||
|
||||
// Pattern 2: Chrome sometimes uses a specific format with delimiters
|
||||
// This is a heuristic approach since the format isn't standardized
|
||||
const parts = identityStr.split('|');
|
||||
if (parts.length > 1) {
|
||||
for (const part of parts) {
|
||||
if (part.includes('.') && !part.includes('/')) {
|
||||
const possibleDomain = part.trim();
|
||||
if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
|
||||
log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
|
||||
return possibleDomain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('Failed to parse PSK identity as UTF-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('No hostname found in PSK extension');
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for SNI extraction with support for fragmented messages
|
||||
* and session resumption edge cases.
|
||||
*
|
||||
* @param buffer The buffer containing TLS data
|
||||
* @param connectionInfo Connection tracking information
|
||||
* @param logger Optional logging function
|
||||
* @param cachedSni Optional previously cached SNI value
|
||||
* @returns The extracted server name or undefined
|
||||
*/
|
||||
public static extractSNIWithResumptionSupport(
|
||||
buffer: Buffer,
|
||||
connectionInfo?: ConnectionInfo,
|
||||
logger?: LoggerFunction,
|
||||
cachedSni?: string
|
||||
): string | undefined {
|
||||
const log = logger || (() => {});
|
||||
|
||||
// Log buffer details for debugging
|
||||
if (logger) {
|
||||
log(`Buffer size: ${buffer.length} bytes`);
|
||||
log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`);
|
||||
|
||||
if (buffer.length >= 5) {
|
||||
const recordType = buffer[0];
|
||||
const majorVersion = buffer[1];
|
||||
const minorVersion = buffer[2];
|
||||
const recordLength = (buffer[3] << 8) + buffer[4];
|
||||
|
||||
log(
|
||||
`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to handle fragmented packets
|
||||
let processBuffer = buffer;
|
||||
if (connectionInfo) {
|
||||
const connectionId = TlsUtils.createConnectionId(connectionInfo);
|
||||
const reassembledBuffer = ClientHelloParser.handleFragmentedClientHello(
|
||||
buffer,
|
||||
connectionId,
|
||||
logger
|
||||
);
|
||||
|
||||
if (!reassembledBuffer) {
|
||||
log(`Waiting for more fragments on connection ${connectionId}`);
|
||||
return undefined; // Need more fragments to complete ClientHello
|
||||
}
|
||||
|
||||
processBuffer = reassembledBuffer;
|
||||
log(`Using reassembled buffer of length ${processBuffer.length}`);
|
||||
}
|
||||
|
||||
// First try the standard SNI extraction
|
||||
const standardSni = this.extractSNI(processBuffer, logger);
|
||||
if (standardSni) {
|
||||
log(`Found standard SNI: ${standardSni}`);
|
||||
return standardSni;
|
||||
}
|
||||
|
||||
// Check for session resumption when standard SNI extraction fails
|
||||
if (TlsUtils.isClientHello(processBuffer)) {
|
||||
const resumptionInfo = ClientHelloParser.hasSessionResumption(processBuffer, logger);
|
||||
|
||||
if (resumptionInfo.isResumption) {
|
||||
log(`Detected session resumption in ClientHello without standard SNI`);
|
||||
|
||||
// Try to extract SNI from PSK extension
|
||||
const pskSni = this.extractSNIFromPSKExtension(processBuffer, logger);
|
||||
if (pskSni) {
|
||||
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
||||
return pskSni;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If cached SNI was provided, use it for application data packets
|
||||
if (cachedSni && TlsUtils.isTlsApplicationData(buffer)) {
|
||||
log(`Using provided cached SNI for application data: ${cachedSni}`);
|
||||
return cachedSni;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified method for processing a TLS packet and extracting SNI.
|
||||
* Main entry point for SNI extraction that handles all edge cases.
|
||||
*
|
||||
* @param buffer The buffer containing TLS data
|
||||
* @param connectionInfo Connection tracking information
|
||||
* @param logger Optional logging function
|
||||
* @param cachedSni Optional previously cached SNI value
|
||||
* @returns The extracted server name or undefined
|
||||
*/
|
||||
public static processTlsPacket(
|
||||
buffer: Buffer,
|
||||
connectionInfo: ConnectionInfo,
|
||||
logger?: LoggerFunction,
|
||||
cachedSni?: string
|
||||
): string | undefined {
|
||||
const log = logger || (() => {});
|
||||
|
||||
// Add timestamp if not provided
|
||||
if (!connectionInfo.timestamp) {
|
||||
connectionInfo.timestamp = Date.now();
|
||||
}
|
||||
|
||||
// Check if this is a TLS handshake or application data
|
||||
if (!TlsUtils.isTlsHandshake(buffer) && !TlsUtils.isTlsApplicationData(buffer)) {
|
||||
log('Not a TLS handshake or application data packet');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create connection ID for tracking
|
||||
const connectionId = TlsUtils.createConnectionId(connectionInfo);
|
||||
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
|
||||
|
||||
// Handle application data with cached SNI (for connection racing)
|
||||
if (TlsUtils.isTlsApplicationData(buffer)) {
|
||||
// If explicit cachedSni was provided, use it
|
||||
if (cachedSni) {
|
||||
log(`Using provided cached SNI for application data: ${cachedSni}`);
|
||||
return cachedSni;
|
||||
}
|
||||
|
||||
log('Application data packet without cached SNI, cannot determine hostname');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Enhanced session resumption detection
|
||||
if (TlsUtils.isClientHello(buffer)) {
|
||||
const resumptionInfo = ClientHelloParser.hasSessionResumption(buffer, logger);
|
||||
|
||||
if (resumptionInfo.isResumption) {
|
||||
log(`Session resumption detected in TLS packet`);
|
||||
|
||||
// Always try standard SNI extraction first
|
||||
const standardSni = this.extractSNI(buffer, logger);
|
||||
if (standardSni) {
|
||||
log(`Found standard SNI in session resumption: ${standardSni}`);
|
||||
return standardSni;
|
||||
}
|
||||
|
||||
// Enhanced session resumption SNI extraction
|
||||
// Try extracting from PSK identity
|
||||
const pskSni = this.extractSNIFromPSKExtension(buffer, logger);
|
||||
if (pskSni) {
|
||||
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
||||
return pskSni;
|
||||
}
|
||||
|
||||
log(`Session resumption without extractable SNI`);
|
||||
}
|
||||
}
|
||||
|
||||
// For handshake messages, try the full extraction process
|
||||
const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, logger);
|
||||
|
||||
if (sni) {
|
||||
log(`Successfully extracted SNI: ${sni}`);
|
||||
return sni;
|
||||
}
|
||||
|
||||
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
||||
if (TlsUtils.isClientHello(buffer)) {
|
||||
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
3
ts/tls/utils/index.ts
Normal file
3
ts/tls/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/**
|
||||
* TLS utilities
|
||||
*/
|
201
ts/tls/utils/tls-utils.ts
Normal file
201
ts/tls/utils/tls-utils.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
|
||||
/**
|
||||
* TLS record types as defined in various RFCs
|
||||
*/
|
||||
export enum TlsRecordType {
|
||||
CHANGE_CIPHER_SPEC = 20,
|
||||
ALERT = 21,
|
||||
HANDSHAKE = 22,
|
||||
APPLICATION_DATA = 23,
|
||||
HEARTBEAT = 24, // RFC 6520
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS handshake message types
|
||||
*/
|
||||
export enum TlsHandshakeType {
|
||||
HELLO_REQUEST = 0,
|
||||
CLIENT_HELLO = 1,
|
||||
SERVER_HELLO = 2,
|
||||
NEW_SESSION_TICKET = 4,
|
||||
ENCRYPTED_EXTENSIONS = 8, // TLS 1.3
|
||||
CERTIFICATE = 11,
|
||||
SERVER_KEY_EXCHANGE = 12,
|
||||
CERTIFICATE_REQUEST = 13,
|
||||
SERVER_HELLO_DONE = 14,
|
||||
CERTIFICATE_VERIFY = 15,
|
||||
CLIENT_KEY_EXCHANGE = 16,
|
||||
FINISHED = 20,
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS extension types
|
||||
*/
|
||||
export enum TlsExtensionType {
|
||||
SERVER_NAME = 0, // SNI
|
||||
MAX_FRAGMENT_LENGTH = 1,
|
||||
CLIENT_CERTIFICATE_URL = 2,
|
||||
TRUSTED_CA_KEYS = 3,
|
||||
TRUNCATED_HMAC = 4,
|
||||
STATUS_REQUEST = 5, // OCSP
|
||||
SUPPORTED_GROUPS = 10, // Previously named "elliptic_curves"
|
||||
EC_POINT_FORMATS = 11,
|
||||
SIGNATURE_ALGORITHMS = 13,
|
||||
APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16, // ALPN
|
||||
SIGNED_CERTIFICATE_TIMESTAMP = 18, // Certificate Transparency
|
||||
PADDING = 21,
|
||||
SESSION_TICKET = 35,
|
||||
PRE_SHARED_KEY = 41, // TLS 1.3
|
||||
EARLY_DATA = 42, // TLS 1.3 0-RTT
|
||||
SUPPORTED_VERSIONS = 43, // TLS 1.3
|
||||
COOKIE = 44, // TLS 1.3
|
||||
PSK_KEY_EXCHANGE_MODES = 45, // TLS 1.3
|
||||
CERTIFICATE_AUTHORITIES = 47, // TLS 1.3
|
||||
POST_HANDSHAKE_AUTH = 49, // TLS 1.3
|
||||
SIGNATURE_ALGORITHMS_CERT = 50, // TLS 1.3
|
||||
KEY_SHARE = 51, // TLS 1.3
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS alert levels
|
||||
*/
|
||||
export enum TlsAlertLevel {
|
||||
WARNING = 1,
|
||||
FATAL = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS alert description codes
|
||||
*/
|
||||
export enum TlsAlertDescription {
|
||||
CLOSE_NOTIFY = 0,
|
||||
UNEXPECTED_MESSAGE = 10,
|
||||
BAD_RECORD_MAC = 20,
|
||||
DECRYPTION_FAILED = 21, // TLS 1.0 only
|
||||
RECORD_OVERFLOW = 22,
|
||||
DECOMPRESSION_FAILURE = 30, // TLS 1.2 and below
|
||||
HANDSHAKE_FAILURE = 40,
|
||||
NO_CERTIFICATE = 41, // SSLv3 only
|
||||
BAD_CERTIFICATE = 42,
|
||||
UNSUPPORTED_CERTIFICATE = 43,
|
||||
CERTIFICATE_REVOKED = 44,
|
||||
CERTIFICATE_EXPIRED = 45,
|
||||
CERTIFICATE_UNKNOWN = 46,
|
||||
ILLEGAL_PARAMETER = 47,
|
||||
UNKNOWN_CA = 48,
|
||||
ACCESS_DENIED = 49,
|
||||
DECODE_ERROR = 50,
|
||||
DECRYPT_ERROR = 51,
|
||||
EXPORT_RESTRICTION = 60, // TLS 1.0 only
|
||||
PROTOCOL_VERSION = 70,
|
||||
INSUFFICIENT_SECURITY = 71,
|
||||
INTERNAL_ERROR = 80,
|
||||
INAPPROPRIATE_FALLBACK = 86,
|
||||
USER_CANCELED = 90,
|
||||
NO_RENEGOTIATION = 100, // TLS 1.2 and below
|
||||
MISSING_EXTENSION = 109, // TLS 1.3
|
||||
UNSUPPORTED_EXTENSION = 110, // TLS 1.3
|
||||
CERTIFICATE_REQUIRED = 111, // TLS 1.3
|
||||
UNRECOGNIZED_NAME = 112,
|
||||
BAD_CERTIFICATE_STATUS_RESPONSE = 113,
|
||||
BAD_CERTIFICATE_HASH_VALUE = 114, // TLS 1.2 and below
|
||||
UNKNOWN_PSK_IDENTITY = 115,
|
||||
CERTIFICATE_REQUIRED_1_3 = 116, // TLS 1.3
|
||||
NO_APPLICATION_PROTOCOL = 120,
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS version codes (major.minor)
|
||||
*/
|
||||
export const TlsVersion = {
|
||||
SSL3: [0x03, 0x00],
|
||||
TLS1_0: [0x03, 0x01],
|
||||
TLS1_1: [0x03, 0x02],
|
||||
TLS1_2: [0x03, 0x03],
|
||||
TLS1_3: [0x03, 0x04],
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility functions for TLS protocol operations
|
||||
*/
|
||||
export class TlsUtils {
|
||||
/**
|
||||
* Checks if a buffer contains a TLS handshake record
|
||||
* @param buffer The buffer to check
|
||||
* @returns true if the buffer starts with a TLS handshake record
|
||||
*/
|
||||
public static isTlsHandshake(buffer: Buffer): boolean {
|
||||
return buffer.length > 0 && buffer[0] === TlsRecordType.HANDSHAKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a buffer contains TLS application data
|
||||
* @param buffer The buffer to check
|
||||
* @returns true if the buffer starts with a TLS application data record
|
||||
*/
|
||||
public static isTlsApplicationData(buffer: Buffer): boolean {
|
||||
return buffer.length > 0 && buffer[0] === TlsRecordType.APPLICATION_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a buffer contains a TLS alert record
|
||||
* @param buffer The buffer to check
|
||||
* @returns true if the buffer starts with a TLS alert record
|
||||
*/
|
||||
public static isTlsAlert(buffer: Buffer): boolean {
|
||||
return buffer.length > 0 && buffer[0] === TlsRecordType.ALERT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a buffer contains a TLS ClientHello message
|
||||
* @param buffer The buffer to check
|
||||
* @returns true if the buffer appears to be a ClientHello message
|
||||
*/
|
||||
public static isClientHello(buffer: Buffer): boolean {
|
||||
// Minimum ClientHello size (TLS record header + handshake header)
|
||||
if (buffer.length < 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check record type (must be TLS_HANDSHAKE_RECORD_TYPE)
|
||||
if (buffer[0] !== TlsRecordType.HANDSHAKE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip version and length in TLS record header (5 bytes total)
|
||||
// Check handshake type at byte 5 (must be CLIENT_HELLO)
|
||||
return buffer[5] === TlsHandshakeType.CLIENT_HELLO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the record length from a TLS record header
|
||||
* @param buffer Buffer containing a TLS record
|
||||
* @returns The record length if the buffer is valid, -1 otherwise
|
||||
*/
|
||||
public static getTlsRecordLength(buffer: Buffer): number {
|
||||
if (buffer.length < 5) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Bytes 3-4 contain the record length (big-endian)
|
||||
return (buffer[3] << 8) + buffer[4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection ID based on source/destination information
|
||||
* Used to track fragmented ClientHello messages across multiple packets
|
||||
*
|
||||
* @param connectionInfo Object containing connection identifiers
|
||||
* @returns A string ID for the connection
|
||||
*/
|
||||
public static createConnectionId(connectionInfo: {
|
||||
sourceIp?: string;
|
||||
sourcePort?: number;
|
||||
destIp?: string;
|
||||
destPort?: number;
|
||||
}): string {
|
||||
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
||||
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user