/** * Profile validation against real captures. * * Real Wireshark / JA4 captures are produced later by the testing package; this * module is the reusable utility that consumes them. It projects a profile's TLS * fields onto the wire values a ClientHello would carry, then compares those * expectations to a captured ClientHello, reporting diffs. * * A key honesty: GREASE (RFC 8701) values are randomized per-connection, so a * profile cannot predict the exact GREASE bytes. We handle this where the data * lets us — cipher suites mark GREASE slots with a named placeholder, so a * GREASE slot in the profile matches any GREASE-pattern byte pair in the capture. * Extension GREASE is not handled here because the profile stores literal wire * codes for extensions (not placeholders), so it cannot robustly flag a GREASE * extension slot. See the per-field comments below. */ import type { BrowserProfile } from "./types.js"; import type { ProfileDiff } from "./diff.js"; import { ProfileError } from "./errors.js"; /** A captured ClientHello, as parsed out of a packet capture by the testing package. */ export interface TlsCapture { /** Offered cipher suites as IANA 2-byte codes, in wire order. */ readonly cipherSuites: readonly number[]; /** Extension type codes present, in wire order. */ readonly extensionTypes: readonly number[]; /** Supported versions advertised, as 2-byte codes, highest first. */ readonly supportedVersions: readonly number[]; /** Key-share named-group ids offered. */ readonly keyShareGroups: readonly number[]; /** Signature algorithm codes offered. */ readonly signatureAlgorithms: readonly number[]; /** EC point formats observed (wire codes). Absent when the capture tool doesn't record them. */ readonly ecPointFormats?: readonly number[]; /** compress_certificate algorithms observed (wire codes). Absent when not recorded. */ readonly compressCertificateAlgorithms?: readonly number[]; /** Whether the ClientHello used GREASE randomization. */ readonly grease: boolean; /** Optional JA3 / JA4 strings, when the capture tool computed them. */ readonly ja3?: string; readonly ja4?: string; } /** The wire values a profile's ClientHello is expected to carry. */ export interface ClientHelloExpected { /** Cipher suites as IANA 2-byte codes, in wire order. */ readonly cipherSuites: readonly number[]; /** Extension type codes, in wire order. */ readonly extensionTypes: readonly number[]; /** Supported versions as 2-byte codes, highest first. */ readonly supportedVersions: readonly number[]; /** Key-share named-group ids. */ readonly keyShareGroups: readonly number[]; /** Signature algorithm codes. */ readonly signatureAlgorithms: readonly number[]; /** EC point formats offered (wire codes). Undefined when the profile doesn't specify them. */ readonly ecPointFormats?: readonly number[]; /** compress_certificate algorithms offered (wire codes). Undefined when unspecified. */ readonly compressCertificateAlgorithms?: readonly number[]; /** Whether GREASE randomization is expected. */ readonly grease: boolean; /** SNI hostname the client would send, derived from the connection target. */ readonly sni: string; } /** Validation / projection failure (unknown profile value, bad capture, etc.). */ export declare class ValidationError extends ProfileError { readonly kind: "ValidationError"; /** * @param message - Description of what failed to validate or project. * @param options - `cause` wraps the underlying error. */ constructor(message: string, options?: { cause?: Error; }); } /** * Outcome of validating a profile against a captured ClientHello. * * Produced by {@link validateProfileAgainstCapture}. */ export interface ValidationResult { /** True when every field matches (respecting GREASE randomization for cipher suites). */ readonly ok: boolean; /** Empty when `ok` is true; otherwise one {@link ProfileDiff} per mismatched field. */ readonly diffs: ProfileDiff[]; } /** * Project a profile's TLS fields onto the wire values its ClientHello should * carry for a connection to `serverName`. * * Extension order is taken verbatim from the profile (already stored as wire * codes), since extension order is a fingerprint signal. Cipher suites, named * groups, signature schemes, and TLS versions are projected through the IANA * code tables in {@link codes.ts}. * * @param profile - The {@link BrowserProfile} to project. * @param serverName - The SNI server name the connection targets. * @returns The {@link ClientHelloExpected} wire values. * @throws {ValidationError} If the profile references an unknown cipher suite, * named group, signature scheme, or TLS version. * * @example * ```ts * const expected = buildExpectedClientHello(chrome140, "example.com"); * // expected.cipherSuites = [0x1301, 0x1302, ...] * ``` * * @since 0.1.0 */ export declare function buildExpectedClientHello(profile: BrowserProfile, serverName: string): ClientHelloExpected; /** * Validate a profile against a captured ClientHello. * * Projects the profile to expected wire values (via {@link buildExpectedClientHello}) * and compares them field-by-field against the capture. Returns `ok: true` when * every field matches (respecting GREASE randomization for cipher suites), and * the list of diffs otherwise. * * SNI is not compared because captures do not record the destination hostname. * * @param profile - The {@link BrowserProfile} to validate. * @param capture - A {@link TlsCapture} parsed from a packet capture. * @returns A {@link ValidationResult} indicating match/mismatch. * @throws {ValidationError} If the profile references an unknown cipher suite, * named group, signature scheme, or TLS version. * * @example * ```ts * const result = validateProfileAgainstCapture(chrome140, capture); * if (!result.ok) { * for (const d of result.diffs) { * console.log(`${d.path}: expected ${d.a}, got ${d.b}`); * } * } * ``` * * @see buildExpectedClientHello for the projection step. * @since 0.1.0 */ export declare function validateProfileAgainstCapture(profile: BrowserProfile, capture: TlsCapture): ValidationResult; //# sourceMappingURL=validate.d.ts.map