Initial curated CertScope source from fe2bbe3ca5c98bf64be26a09c6c58ee805a7f87e
This commit is contained in:
commit
7348faf6c0
65 files changed
+12509
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import tls from "node:tls";
|
||||
import { createDeadline, remainingMs, timeoutError } from "./deadline";
|
||||
import { validateResolvedAddresses } from "./policy";
|
||||
|
||||
export const TLS_TIMEOUT_MS = 5_000;
|
||||
export const TLS_TOTAL_TIMEOUT_MS = 8_000;
|
||||
export const TLS_MAX_ADDRESS_ATTEMPTS = 6;
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
type TlsError = { probe: "TLS"; message: string };
|
||||
type TlsSocket = ReturnType<typeof tls.connect>;
|
||||
export type TlsConnectOptions = tls.ConnectionOptions;
|
||||
export type TlsConnector = (options: TlsConnectOptions) => TlsSocket;
|
||||
|
||||
export interface TlsCollectionOptions {
|
||||
connect?: TlsConnector;
|
||||
now?: number;
|
||||
deadline?: number;
|
||||
}
|
||||
|
||||
function failure(message: string, attempts: Array<Record<string, unknown>> = []): Record<string, unknown> {
|
||||
const errors: TlsError[] = [{ probe: "TLS", message }];
|
||||
return { error: message, attempts, _meta: { status: "failed", errors } };
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
const code = typeof error === "object" && error !== null && "code" in error
|
||||
? String((error as { code?: unknown }).code ?? "")
|
||||
: "";
|
||||
return code ? "TLS probe failed (" + code + ")." : "TLS probe failed.";
|
||||
}
|
||||
|
||||
export function certificateDaysRemaining(validTo: string | null | undefined, now = Date.now()) {
|
||||
if (!validTo) return null;
|
||||
const timestamp = Date.parse(validTo);
|
||||
if (!Number.isFinite(timestamp)) return null;
|
||||
return Math.floor((timestamp - now) / DAY_MS);
|
||||
}
|
||||
|
||||
function certificateNotYetValid(validFrom: string | null | undefined, now: number) {
|
||||
if (!validFrom) return null;
|
||||
const timestamp = Date.parse(validFrom);
|
||||
return Number.isFinite(timestamp) ? timestamp > now : null;
|
||||
}
|
||||
|
||||
function certificateValidityDatesValid(validFrom: string | null | undefined, validTo: string | null | undefined) {
|
||||
if (!validFrom || !validTo) return false;
|
||||
const from = Date.parse(validFrom);
|
||||
const to = Date.parse(validTo);
|
||||
return Number.isFinite(from) && Number.isFinite(to) && from <= to;
|
||||
}
|
||||
|
||||
function collectTlsCandidate(host: string, address: string, deadline: number, connect: TlsConnector, now: number): Promise<Record<string, unknown>> {
|
||||
const budget = Math.min(TLS_TIMEOUT_MS, remainingMs(deadline));
|
||||
if (!budget) return Promise.reject(timeoutError("TLS handshake timed out."));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let socket: TlsSocket | undefined;
|
||||
let settled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const done = (callback: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
socket?.destroy();
|
||||
callback();
|
||||
};
|
||||
|
||||
const fail = (error: unknown) => done(() => reject(error));
|
||||
try {
|
||||
socket = connect({
|
||||
host: address,
|
||||
port: 443,
|
||||
servername: host,
|
||||
rejectUnauthorized: false,
|
||||
timeout: budget,
|
||||
});
|
||||
timer = setTimeout(() => fail(timeoutError("TLS handshake timed out.")), budget);
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.once("secureConnect", () => {
|
||||
try {
|
||||
const cert = socket!.getPeerCertificate(true);
|
||||
if (!cert || !Object.keys(cert).length) {
|
||||
fail(new Error("TLS peer certificate was unavailable."));
|
||||
return;
|
||||
}
|
||||
const hostnameError = tls.checkServerIdentity(host, cert);
|
||||
const validTo = cert.valid_to ?? null;
|
||||
const validFrom = cert.valid_from ?? null;
|
||||
const result = {
|
||||
address,
|
||||
protocol: socket!.getProtocol(),
|
||||
authorized: socket!.authorized,
|
||||
authorizationError: socket!.authorizationError ? String(socket!.authorizationError) : null,
|
||||
hostnameValid: !hostnameError,
|
||||
hostnameError: hostnameError?.message ?? null,
|
||||
subject: cert.subject ?? null,
|
||||
issuer: cert.issuer ?? null,
|
||||
subjectaltname: cert.subjectaltname ?? null,
|
||||
validFrom,
|
||||
validTo,
|
||||
validityDatesValid: certificateValidityDatesValid(validFrom, validTo),
|
||||
daysRemaining: certificateDaysRemaining(validTo, now),
|
||||
notYetValid: certificateNotYetValid(validFrom, now),
|
||||
fingerprint256: cert.fingerprint256 ?? null,
|
||||
_meta: { status: "ok", errors: [] },
|
||||
};
|
||||
done(() => resolve(result));
|
||||
} catch (error) {
|
||||
fail(new Error(error instanceof Error ? "TLS certificate could not be parsed." : "TLS probe failed."));
|
||||
}
|
||||
});
|
||||
socket.once("timeout", () => fail(timeoutError("TLS handshake timed out.")));
|
||||
socket.once("error", (error) => fail(error));
|
||||
});
|
||||
}
|
||||
|
||||
export async function collectTls(host: string, rawAddresses: string[] | string, options: TlsCollectionOptions = {}): Promise<Record<string, unknown>> {
|
||||
const addresses = validateResolvedAddresses(Array.isArray(rawAddresses) ? rawAddresses : [rawAddresses]);
|
||||
const deadline = options.deadline ?? createDeadline(TLS_TOTAL_TIMEOUT_MS);
|
||||
const connect = options.connect ?? ((connectionOptions) => tls.connect(connectionOptions));
|
||||
const now = options.now ?? Date.now();
|
||||
const attempts: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const address of addresses.slice(0, TLS_MAX_ADDRESS_ATTEMPTS)) {
|
||||
if (!remainingMs(deadline)) break;
|
||||
try {
|
||||
return { ...(await collectTlsCandidate(host, address, deadline, connect, now)), attempts };
|
||||
} catch (error) {
|
||||
attempts.push({ address, error: errorMessage(error) });
|
||||
}
|
||||
}
|
||||
|
||||
const message = remainingMs(deadline) ? "TLS probe failed for all resolved addresses." : "TLS probe deadline exhausted.";
|
||||
return failure(message, attempts);
|
||||
}
|
||||
Reference in new issue
Block a user