258 lines
9.6 KiB
TypeScript
258 lines
9.6 KiB
TypeScript
import * as net from 'net';
|
|
|
|
/**
|
|
* TlsAlert class for managing 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;
|
|
|
|
/**
|
|
* 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)
|
|
* @returns Buffer containing the TLS alert message
|
|
*/
|
|
static create(
|
|
level: number,
|
|
description: number,
|
|
tlsVersion: [number, number] = [0x03, 0x03]
|
|
): Buffer {
|
|
return Buffer.from([
|
|
0x15, // Alert record type
|
|
tlsVersion[0],
|
|
tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303)
|
|
0x00,
|
|
0x02, // Length
|
|
level, // Alert level
|
|
description, // Alert description
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create a warning-level TLS alert
|
|
*
|
|
* @param description Alert description code
|
|
* @returns Buffer containing the warning-level TLS alert message
|
|
*/
|
|
static createWarning(description: number): Buffer {
|
|
return this.create(this.LEVEL_WARNING, description);
|
|
}
|
|
|
|
/**
|
|
* Create a fatal-level TLS alert
|
|
*
|
|
* @param description Alert description code
|
|
* @returns Buffer containing the fatal-level TLS alert message
|
|
*/
|
|
static createFatal(description: number): Buffer {
|
|
return this.create(this.LEVEL_FATAL, description);
|
|
}
|
|
|
|
/**
|
|
* Send a TLS alert to a socket and optionally close the connection
|
|
*
|
|
* @param socket The socket to send the alert to
|
|
* @param level Alert level (warning or fatal)
|
|
* @param description Alert description code
|
|
* @param closeAfterSend Whether to close the connection after sending the alert
|
|
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
|
* @returns Promise that resolves when the alert has been sent
|
|
*/
|
|
static async send(
|
|
socket: net.Socket,
|
|
level: number,
|
|
description: number,
|
|
closeAfterSend: boolean = false,
|
|
closeDelay: number = 200
|
|
): Promise<void> {
|
|
const alert = this.create(level, description);
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
try {
|
|
// Ensure the alert is written as a single packet
|
|
socket.cork();
|
|
const writeSuccessful = socket.write(alert, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (closeAfterSend) {
|
|
setTimeout(() => {
|
|
socket.end();
|
|
resolve();
|
|
}, closeDelay);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
socket.uncork();
|
|
|
|
// If write wasn't successful immediately, wait for drain
|
|
if (!writeSuccessful && !closeAfterSend) {
|
|
socket.once('drain', () => {
|
|
resolve();
|
|
});
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pre-defined TLS alert messages
|
|
*/
|
|
static readonly alerts = {
|
|
// Warning level alerts
|
|
closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY),
|
|
unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION),
|
|
certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED),
|
|
unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME),
|
|
noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION),
|
|
userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED),
|
|
|
|
// Warning level alerts for session resumption
|
|
certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED),
|
|
handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE),
|
|
insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY),
|
|
|
|
// Fatal level alerts
|
|
unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE),
|
|
badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC),
|
|
recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW),
|
|
handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE),
|
|
badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE),
|
|
certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED),
|
|
certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN),
|
|
illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER),
|
|
unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA),
|
|
accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED),
|
|
decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR),
|
|
decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR),
|
|
protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
|
|
insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
|
|
internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
|
|
unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
|
|
};
|
|
|
|
/**
|
|
* Utility method to send a warning-level unrecognized_name alert
|
|
* Specifically designed for SNI issues to encourage the client to retry with SNI
|
|
*
|
|
* @param socket The socket to send the alert to
|
|
* @returns Promise that resolves when the alert has been sent
|
|
*/
|
|
static async sendSniRequired(socket: net.Socket): Promise<void> {
|
|
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
|
|
}
|
|
|
|
/**
|
|
* Utility method to send a close_notify alert and close the connection
|
|
*
|
|
* @param socket The socket to send the alert to
|
|
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
|
* @returns Promise that resolves when the alert has been sent and the connection closed
|
|
*/
|
|
static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> {
|
|
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
|
|
}
|
|
|
|
/**
|
|
* Utility method to send a certificate_expired alert to force new TLS session
|
|
*
|
|
* @param socket The socket to send the alert to
|
|
* @param fatal Whether to send as a fatal alert (default: false)
|
|
* @param closeAfterSend Whether to close the connection after sending the alert (default: true)
|
|
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
|
* @returns Promise that resolves when the alert has been sent
|
|
*/
|
|
static async sendCertificateExpired(
|
|
socket: net.Socket,
|
|
fatal: boolean = false,
|
|
closeAfterSend: boolean = true,
|
|
closeDelay: number = 200
|
|
): Promise<void> {
|
|
const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING;
|
|
return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay);
|
|
}
|
|
|
|
/**
|
|
* Send a sequence of alerts to force SNI from clients
|
|
* This combines multiple alerts to ensure maximum browser compatibility
|
|
*
|
|
* @param socket The socket to send the alerts to
|
|
* @returns Promise that resolves when all alerts have been sent
|
|
*/
|
|
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
|
|
try {
|
|
// Send unrecognized_name (warning)
|
|
socket.cork();
|
|
socket.write(this.alerts.unrecognizedName);
|
|
socket.uncork();
|
|
|
|
// Give the socket time to send the alert
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, 50);
|
|
});
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a fatal level alert that immediately terminates the connection
|
|
*
|
|
* @param socket The socket to send the alert to
|
|
* @param description Alert description code
|
|
* @param closeDelay Milliseconds to wait before closing the connection (default: 100ms)
|
|
* @returns Promise that resolves when the alert has been sent and the connection closed
|
|
*/
|
|
static async sendFatalAndClose(
|
|
socket: net.Socket,
|
|
description: number,
|
|
closeDelay: number = 100
|
|
): Promise<void> {
|
|
return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
|
|
}
|
|
} |