🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@quantakrypto/qprobe

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@quantakrypto/qprobe - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+2
-2
dist/clienthello.js
/**
* Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest
* parser — just enough to detect whether a server supports the post-quantum hybrid
* key-exchange group X25519MLKEM768 (codepoint 0x11EC, RFC 9370 / draft-kwiatkowski
* -tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot
* key-exchange group X25519MLKEM768 (codepoint 0x11EC, registered by
* draft-ietf-tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot
* be detected through `node:tls`; we advertise it in a raw ClientHello and read the

@@ -7,0 +7,0 @@ * group the server selects.

@@ -1,1 +0,1 @@

{"version":3,"file":"clienthello.js","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest\n * parser — just enough to detect whether a server supports the post-quantum hybrid\n * key-exchange group X25519MLKEM768 (codepoint 0x11EC, RFC 9370 / draft-kwiatkowski\n * -tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot\n * be detected through `node:tls`; we advertise it in a raw ClientHello and read the\n * group the server selects.\n *\n * We advertise `supported_groups = [X25519MLKEM768, x25519, …]` AND send a\n * WELL-FORMED X25519MLKEM768 key_share (`ML-KEM-768.ek (1184) || X25519.pk (32)`,\n * per draft-ietf-tls-ecdhe-mlkem) alongside an x25519 share. A supporting server\n * then selects 0x11EC DIRECTLY in its ServerHello (we read the selected group) —\n * this catches servers that support-but-don't-*prefer* the hybrid group, which the\n * older HelloRetryRequest-only inference missed. The ML-KEM ek is well-formed but\n * throwaway (see mlkem768.ts): qProbe never completes the handshake, so the ML-KEM\n * shared secret is neither computed nor needed. A server that does not support the\n * group falls back to our x25519 share (or answers with a HelloRetryRequest, which\n * the parser still reads).\n *\n * This module is byte manipulation over Buffers (the X25519 key and ML-KEM encoding\n * come from node:crypto / mlkem768.ts); the socket I/O lives in tls.ts. The pure\n * functions are unit-tested with crafted bytes.\n */\nimport { randomBytes, generateKeyPairSync } from \"node:crypto\";\nimport { wellFormedMlKem768Ek } from \"./mlkem768.js\";\n\n/** A raw 32-byte X25519 public key (from node:crypto — a real, valid key). */\nexport function x25519RawPublic(): Buffer {\n const { publicKey } = generateKeyPairSync(\"x25519\");\n const jwk = publicKey.export({ format: \"jwk\" }) as { x?: string };\n return Buffer.from(jwk.x ?? \"\", \"base64url\");\n}\n\n// TLS constants.\nexport const GROUP_X25519 = 0x001d;\nexport const GROUP_X25519MLKEM768 = 0x11ec;\nexport const GROUP_SECP256R1 = 0x0017;\nconst TLS13_VERSION = 0x0304;\n\nconst HS_CLIENT_HELLO = 0x01;\nconst HS_SERVER_HELLO = 0x02;\nconst REC_HANDSHAKE = 0x16;\n\nconst EXT_SERVER_NAME = 0x0000;\nconst EXT_SUPPORTED_GROUPS = 0x000a;\nconst EXT_SIGNATURE_ALGORITHMS = 0x000d;\nconst EXT_SUPPORTED_VERSIONS = 0x002b;\nconst EXT_KEY_SHARE = 0x0033;\n\n/** The special ServerHello.random that marks a HelloRetryRequest (RFC 8446 §4.1.3). */\nconst HRR_RANDOM = Buffer.from(\n \"cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c\",\n \"hex\",\n);\n\nfunction u16(n: number): Buffer {\n const b = Buffer.alloc(2);\n b.writeUInt16BE(n, 0);\n return b;\n}\n\n/** Prefix `body` with a length field of `bytes` width (1, 2, or 3 bytes). */\nfunction withLen(bytes: 1 | 2 | 3, body: Buffer): Buffer {\n const len = Buffer.alloc(bytes);\n if (bytes === 1) len.writeUInt8(body.length, 0);\n else if (bytes === 2) len.writeUInt16BE(body.length, 0);\n else len.writeUIntBE(body.length, 0, 3);\n return Buffer.concat([len, body]);\n}\n\n/** Build a single extension (type + 2-byte-length body). */\nfunction ext(type: number, body: Buffer): Buffer {\n return Buffer.concat([u16(type), withLen(2, body)]);\n}\n\n/**\n * Build a raw ClientHello TLS record advertising the hybrid group. `keyShareGroup`\n * is the group we actually send a key_share for (default x25519), while\n * `supportedGroups` is what we advertise (hybrid first).\n */\nexport function buildClientHello(opts: {\n serverName?: string;\n supportedGroups?: number[];\n keyShareGroup?: number;\n}): Buffer {\n const supportedGroups = opts.supportedGroups ?? [\n GROUP_X25519MLKEM768,\n GROUP_X25519,\n GROUP_SECP256R1,\n ];\n const keyShareGroup = opts.keyShareGroup ?? GROUP_X25519;\n\n const extensions: Buffer[] = [];\n\n if (\n opts.serverName &&\n opts.serverName.length > 0 &&\n !/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(opts.serverName)\n ) {\n // server_name: ServerNameList { name_type(0=host_name), HostName }\n const name = Buffer.from(opts.serverName, \"ascii\");\n const entry = Buffer.concat([Buffer.from([0x00]), withLen(2, name)]);\n extensions.push(ext(EXT_SERVER_NAME, withLen(2, entry)));\n }\n\n // supported_versions: TLS 1.3 only.\n extensions.push(ext(EXT_SUPPORTED_VERSIONS, withLen(1, u16(TLS13_VERSION))));\n\n // supported_groups (named_group list).\n extensions.push(ext(EXT_SUPPORTED_GROUPS, withLen(2, Buffer.concat(supportedGroups.map(u16)))));\n\n // signature_algorithms (a standard modern set).\n const sigAlgs = [0x0403, 0x0804, 0x0807, 0x0401, 0x0805, 0x0806];\n extensions.push(ext(EXT_SIGNATURE_ALGORITHMS, withLen(2, Buffer.concat(sigAlgs.map(u16)))));\n\n // key_share: send a REAL X25519 public plus a WELL-FORMED X25519MLKEM768 hybrid\n // share, so a server that supports the hybrid group can select it DIRECTLY (not\n // only via a HelloRetryRequest). For X25519MLKEM768 the client share is\n // `ML-KEM-768.ek (1184) || X25519.pk (32)` (draft-ietf-tls-ecdhe-mlkem). We never\n // complete the handshake, so the ML-KEM secret is neither computed nor needed.\n void keyShareGroup; // retained for API compat; the share set is now fixed\n const x25519Pub = x25519RawPublic();\n const hybridShareVal = Buffer.concat([wellFormedMlKem768Ek(), x25519Pub]);\n const shareEntries = Buffer.concat([\n u16(GROUP_X25519MLKEM768),\n withLen(2, hybridShareVal),\n u16(GROUP_X25519),\n withLen(2, x25519Pub),\n ]);\n extensions.push(ext(EXT_KEY_SHARE, withLen(2, shareEntries)));\n\n const cipherSuites = Buffer.concat([u16(0x1301), u16(0x1302), u16(0x1303)]);\n\n const helloBody = Buffer.concat([\n u16(0x0303), // legacy_version TLS 1.2\n randomBytes(32), // random\n withLen(1, randomBytes(32)), // legacy_session_id (non-empty → \"middlebox compat\")\n withLen(2, cipherSuites), // cipher_suites\n withLen(1, Buffer.from([0x00])), // compression_methods: null\n withLen(2, Buffer.concat(extensions)), // extensions\n ]);\n\n const handshake = Buffer.concat([Buffer.from([HS_CLIENT_HELLO]), withLen(3, helloBody)]);\n // TLS record: handshake, legacy record version 0x0301.\n return Buffer.concat([Buffer.from([REC_HANDSHAKE]), u16(0x0301), withLen(2, handshake)]);\n}\n\n/** A parsed TLS record. */\nexport interface TlsRecord {\n type: number;\n fragment: Buffer;\n}\n\n/** Split a buffer into TLS records. Stops at the first truncated record. */\nexport function parseRecords(buf: Buffer): TlsRecord[] {\n const out: TlsRecord[] = [];\n let off = 0;\n while (off + 5 <= buf.length) {\n const type = buf[off];\n const len = buf.readUInt16BE(off + 3);\n if (off + 5 + len > buf.length) break;\n out.push({ type, fragment: buf.subarray(off + 5, off + 5 + len) });\n off += 5 + len;\n }\n return out;\n}\n\n/** The result of reading a ServerHello / HelloRetryRequest. */\nexport interface ServerHelloInfo {\n isHelloRetryRequest: boolean;\n /** Group named in the key_share extension (selected group), if present. */\n selectedGroup?: number;\n /** Negotiated version from supported_versions (0x0304 for TLS 1.3), if present. */\n negotiatedVersion?: number;\n cipherSuite?: number;\n}\n\n/**\n * Parse a ServerHello handshake message body (the bytes AFTER the 4-byte handshake\n * header) and extract the selected group + negotiated version. Handles both a\n * normal ServerHello (key_share carries group + key) and a HelloRetryRequest\n * (key_share carries just the selected group) — in both the first 2 bytes of the\n * key_share body are the group.\n */\nexport function parseServerHelloBody(body: Buffer): ServerHelloInfo {\n let off = 0;\n const need = (n: number) => {\n if (off + n > body.length) throw new RangeError(\"truncated ServerHello\");\n };\n need(2);\n off += 2; // legacy_version\n need(32);\n const random = body.subarray(off, off + 32);\n off += 32;\n const isHRR = random.equals(HRR_RANDOM);\n need(1);\n const sidLen = body[off];\n off += 1;\n need(sidLen);\n off += sidLen; // legacy_session_id_echo\n need(2);\n const cipherSuite = body.readUInt16BE(off);\n off += 2;\n need(1);\n off += 1; // legacy_compression_method\n\n const info: ServerHelloInfo = { isHelloRetryRequest: isHRR, cipherSuite };\n if (off + 2 > body.length) return info; // no extensions\n const extTotal = body.readUInt16BE(off);\n off += 2;\n const extEnd = Math.min(off + extTotal, body.length);\n while (off + 4 <= extEnd) {\n const extType = body.readUInt16BE(off);\n const extLen = body.readUInt16BE(off + 2);\n const start = off + 4;\n if (start + extLen > body.length) break;\n const extBody = body.subarray(start, start + extLen);\n if (extType === EXT_KEY_SHARE && extBody.length >= 2) {\n info.selectedGroup = extBody.readUInt16BE(0);\n } else if (extType === EXT_SUPPORTED_VERSIONS && extBody.length >= 2) {\n info.negotiatedVersion = extBody.readUInt16BE(0);\n }\n off = start + extLen;\n }\n return info;\n}\n\n/** Read the first ServerHello/HRR out of a raw response buffer, if any. */\nexport function readServerHello(raw: Buffer): ServerHelloInfo | undefined {\n for (const rec of parseRecords(raw)) {\n if (rec.type !== REC_HANDSHAKE) continue;\n const f = rec.fragment;\n if (f.length < 4) continue;\n if (f[0] !== HS_SERVER_HELLO) continue;\n const len = f.readUIntBE(1, 3);\n if (4 + len > f.length) continue;\n return parseServerHelloBody(f.subarray(4, 4 + len));\n }\n return undefined;\n}\n"]}
{"version":3,"file":"clienthello.js","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest\n * parser — just enough to detect whether a server supports the post-quantum hybrid\n * key-exchange group X25519MLKEM768 (codepoint 0x11EC, registered by\n * draft-ietf-tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot\n * be detected through `node:tls`; we advertise it in a raw ClientHello and read the\n * group the server selects.\n *\n * We advertise `supported_groups = [X25519MLKEM768, x25519, …]` AND send a\n * WELL-FORMED X25519MLKEM768 key_share (`ML-KEM-768.ek (1184) || X25519.pk (32)`,\n * per draft-ietf-tls-ecdhe-mlkem) alongside an x25519 share. A supporting server\n * then selects 0x11EC DIRECTLY in its ServerHello (we read the selected group) —\n * this catches servers that support-but-don't-*prefer* the hybrid group, which the\n * older HelloRetryRequest-only inference missed. The ML-KEM ek is well-formed but\n * throwaway (see mlkem768.ts): qProbe never completes the handshake, so the ML-KEM\n * shared secret is neither computed nor needed. A server that does not support the\n * group falls back to our x25519 share (or answers with a HelloRetryRequest, which\n * the parser still reads).\n *\n * This module is byte manipulation over Buffers (the X25519 key and ML-KEM encoding\n * come from node:crypto / mlkem768.ts); the socket I/O lives in tls.ts. The pure\n * functions are unit-tested with crafted bytes.\n */\nimport { randomBytes, generateKeyPairSync } from \"node:crypto\";\nimport { wellFormedMlKem768Ek } from \"./mlkem768.js\";\n\n/** A raw 32-byte X25519 public key (from node:crypto — a real, valid key). */\nexport function x25519RawPublic(): Buffer {\n const { publicKey } = generateKeyPairSync(\"x25519\");\n const jwk = publicKey.export({ format: \"jwk\" }) as { x?: string };\n return Buffer.from(jwk.x ?? \"\", \"base64url\");\n}\n\n// TLS constants.\nexport const GROUP_X25519 = 0x001d;\nexport const GROUP_X25519MLKEM768 = 0x11ec;\nexport const GROUP_SECP256R1 = 0x0017;\nconst TLS13_VERSION = 0x0304;\n\nconst HS_CLIENT_HELLO = 0x01;\nconst HS_SERVER_HELLO = 0x02;\nconst REC_HANDSHAKE = 0x16;\n\nconst EXT_SERVER_NAME = 0x0000;\nconst EXT_SUPPORTED_GROUPS = 0x000a;\nconst EXT_SIGNATURE_ALGORITHMS = 0x000d;\nconst EXT_SUPPORTED_VERSIONS = 0x002b;\nconst EXT_KEY_SHARE = 0x0033;\n\n/** The special ServerHello.random that marks a HelloRetryRequest (RFC 8446 §4.1.3). */\nconst HRR_RANDOM = Buffer.from(\n \"cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c\",\n \"hex\",\n);\n\nfunction u16(n: number): Buffer {\n const b = Buffer.alloc(2);\n b.writeUInt16BE(n, 0);\n return b;\n}\n\n/** Prefix `body` with a length field of `bytes` width (1, 2, or 3 bytes). */\nfunction withLen(bytes: 1 | 2 | 3, body: Buffer): Buffer {\n const len = Buffer.alloc(bytes);\n if (bytes === 1) len.writeUInt8(body.length, 0);\n else if (bytes === 2) len.writeUInt16BE(body.length, 0);\n else len.writeUIntBE(body.length, 0, 3);\n return Buffer.concat([len, body]);\n}\n\n/** Build a single extension (type + 2-byte-length body). */\nfunction ext(type: number, body: Buffer): Buffer {\n return Buffer.concat([u16(type), withLen(2, body)]);\n}\n\n/**\n * Build a raw ClientHello TLS record advertising the hybrid group. `keyShareGroup`\n * is the group we actually send a key_share for (default x25519), while\n * `supportedGroups` is what we advertise (hybrid first).\n */\nexport function buildClientHello(opts: {\n serverName?: string;\n supportedGroups?: number[];\n keyShareGroup?: number;\n}): Buffer {\n const supportedGroups = opts.supportedGroups ?? [\n GROUP_X25519MLKEM768,\n GROUP_X25519,\n GROUP_SECP256R1,\n ];\n const keyShareGroup = opts.keyShareGroup ?? GROUP_X25519;\n\n const extensions: Buffer[] = [];\n\n if (\n opts.serverName &&\n opts.serverName.length > 0 &&\n !/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(opts.serverName)\n ) {\n // server_name: ServerNameList { name_type(0=host_name), HostName }\n const name = Buffer.from(opts.serverName, \"ascii\");\n const entry = Buffer.concat([Buffer.from([0x00]), withLen(2, name)]);\n extensions.push(ext(EXT_SERVER_NAME, withLen(2, entry)));\n }\n\n // supported_versions: TLS 1.3 only.\n extensions.push(ext(EXT_SUPPORTED_VERSIONS, withLen(1, u16(TLS13_VERSION))));\n\n // supported_groups (named_group list).\n extensions.push(ext(EXT_SUPPORTED_GROUPS, withLen(2, Buffer.concat(supportedGroups.map(u16)))));\n\n // signature_algorithms (a standard modern set).\n const sigAlgs = [0x0403, 0x0804, 0x0807, 0x0401, 0x0805, 0x0806];\n extensions.push(ext(EXT_SIGNATURE_ALGORITHMS, withLen(2, Buffer.concat(sigAlgs.map(u16)))));\n\n // key_share: send a REAL X25519 public plus a WELL-FORMED X25519MLKEM768 hybrid\n // share, so a server that supports the hybrid group can select it DIRECTLY (not\n // only via a HelloRetryRequest). For X25519MLKEM768 the client share is\n // `ML-KEM-768.ek (1184) || X25519.pk (32)` (draft-ietf-tls-ecdhe-mlkem). We never\n // complete the handshake, so the ML-KEM secret is neither computed nor needed.\n void keyShareGroup; // retained for API compat; the share set is now fixed\n const x25519Pub = x25519RawPublic();\n const hybridShareVal = Buffer.concat([wellFormedMlKem768Ek(), x25519Pub]);\n const shareEntries = Buffer.concat([\n u16(GROUP_X25519MLKEM768),\n withLen(2, hybridShareVal),\n u16(GROUP_X25519),\n withLen(2, x25519Pub),\n ]);\n extensions.push(ext(EXT_KEY_SHARE, withLen(2, shareEntries)));\n\n const cipherSuites = Buffer.concat([u16(0x1301), u16(0x1302), u16(0x1303)]);\n\n const helloBody = Buffer.concat([\n u16(0x0303), // legacy_version TLS 1.2\n randomBytes(32), // random\n withLen(1, randomBytes(32)), // legacy_session_id (non-empty → \"middlebox compat\")\n withLen(2, cipherSuites), // cipher_suites\n withLen(1, Buffer.from([0x00])), // compression_methods: null\n withLen(2, Buffer.concat(extensions)), // extensions\n ]);\n\n const handshake = Buffer.concat([Buffer.from([HS_CLIENT_HELLO]), withLen(3, helloBody)]);\n // TLS record: handshake, legacy record version 0x0301.\n return Buffer.concat([Buffer.from([REC_HANDSHAKE]), u16(0x0301), withLen(2, handshake)]);\n}\n\n/** A parsed TLS record. */\nexport interface TlsRecord {\n type: number;\n fragment: Buffer;\n}\n\n/** Split a buffer into TLS records. Stops at the first truncated record. */\nexport function parseRecords(buf: Buffer): TlsRecord[] {\n const out: TlsRecord[] = [];\n let off = 0;\n while (off + 5 <= buf.length) {\n const type = buf[off];\n const len = buf.readUInt16BE(off + 3);\n if (off + 5 + len > buf.length) break;\n out.push({ type, fragment: buf.subarray(off + 5, off + 5 + len) });\n off += 5 + len;\n }\n return out;\n}\n\n/** The result of reading a ServerHello / HelloRetryRequest. */\nexport interface ServerHelloInfo {\n isHelloRetryRequest: boolean;\n /** Group named in the key_share extension (selected group), if present. */\n selectedGroup?: number;\n /** Negotiated version from supported_versions (0x0304 for TLS 1.3), if present. */\n negotiatedVersion?: number;\n cipherSuite?: number;\n}\n\n/**\n * Parse a ServerHello handshake message body (the bytes AFTER the 4-byte handshake\n * header) and extract the selected group + negotiated version. Handles both a\n * normal ServerHello (key_share carries group + key) and a HelloRetryRequest\n * (key_share carries just the selected group) — in both the first 2 bytes of the\n * key_share body are the group.\n */\nexport function parseServerHelloBody(body: Buffer): ServerHelloInfo {\n let off = 0;\n const need = (n: number) => {\n if (off + n > body.length) throw new RangeError(\"truncated ServerHello\");\n };\n need(2);\n off += 2; // legacy_version\n need(32);\n const random = body.subarray(off, off + 32);\n off += 32;\n const isHRR = random.equals(HRR_RANDOM);\n need(1);\n const sidLen = body[off];\n off += 1;\n need(sidLen);\n off += sidLen; // legacy_session_id_echo\n need(2);\n const cipherSuite = body.readUInt16BE(off);\n off += 2;\n need(1);\n off += 1; // legacy_compression_method\n\n const info: ServerHelloInfo = { isHelloRetryRequest: isHRR, cipherSuite };\n if (off + 2 > body.length) return info; // no extensions\n const extTotal = body.readUInt16BE(off);\n off += 2;\n const extEnd = Math.min(off + extTotal, body.length);\n while (off + 4 <= extEnd) {\n const extType = body.readUInt16BE(off);\n const extLen = body.readUInt16BE(off + 2);\n const start = off + 4;\n if (start + extLen > body.length) break;\n const extBody = body.subarray(start, start + extLen);\n if (extType === EXT_KEY_SHARE && extBody.length >= 2) {\n info.selectedGroup = extBody.readUInt16BE(0);\n } else if (extType === EXT_SUPPORTED_VERSIONS && extBody.length >= 2) {\n info.negotiatedVersion = extBody.readUInt16BE(0);\n }\n off = start + extLen;\n }\n return info;\n}\n\n/** Read the first ServerHello/HRR out of a raw response buffer, if any. */\nexport function readServerHello(raw: Buffer): ServerHelloInfo | undefined {\n for (const rec of parseRecords(raw)) {\n if (rec.type !== REC_HANDSHAKE) continue;\n const f = rec.fragment;\n if (f.length < 4) continue;\n if (f[0] !== HS_SERVER_HELLO) continue;\n const len = f.readUIntBE(1, 3);\n if (4 + len > f.length) continue;\n return parseServerHelloBody(f.subarray(4, 4 + len));\n }\n return undefined;\n}\n"]}
{
"name": "@quantakrypto/qprobe",
"version": "0.8.0",
"version": "0.9.0",
"description": "qProbe — actively inspect live TLS/SSH endpoints you OWN for post-quantum readiness (hybrid KEX, classical certs). Gated behind an ownership attestation. Zero runtime dependencies.",

@@ -40,3 +40,3 @@ "license": "Apache-2.0",

"dependencies": {
"@quantakrypto/core": "0.8.0"
"@quantakrypto/core": "0.9.0"
},

@@ -43,0 +43,0 @@ "scripts": {