@quantakrypto/qprobe 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/args.d.ts +5 -4
  3. package/dist/args.d.ts.map +1 -1
  4. package/dist/args.js +14 -2
  5. package/dist/args.js.map +1 -1
  6. package/dist/attest.d.ts.map +1 -1
  7. package/dist/attest.js +21 -2
  8. package/dist/attest.js.map +1 -1
  9. package/dist/cli.js +9 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/clienthello.d.ts +2 -0
  12. package/dist/clienthello.d.ts.map +1 -1
  13. package/dist/clienthello.js +36 -12
  14. package/dist/clienthello.js.map +1 -1
  15. package/dist/index.d.ts +9 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +31 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/mlkem768.d.ts +15 -0
  20. package/dist/mlkem768.d.ts.map +1 -0
  21. package/dist/mlkem768.js +71 -0
  22. package/dist/mlkem768.js.map +1 -0
  23. package/dist/postgres.d.ts +9 -0
  24. package/dist/postgres.d.ts.map +1 -0
  25. package/dist/postgres.js +79 -0
  26. package/dist/postgres.js.map +1 -0
  27. package/dist/smtp.d.ts.map +1 -1
  28. package/dist/smtp.js +5 -1
  29. package/dist/smtp.js.map +1 -1
  30. package/dist/ssh.d.ts.map +1 -1
  31. package/dist/ssh.js +4 -0
  32. package/dist/ssh.js.map +1 -1
  33. package/dist/starttls.d.ts +24 -0
  34. package/dist/starttls.d.ts.map +1 -0
  35. package/dist/starttls.js +98 -0
  36. package/dist/starttls.js.map +1 -0
  37. package/dist/tls.d.ts.map +1 -1
  38. package/dist/tls.js +17 -1
  39. package/dist/tls.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/dist/x509.d.ts +2 -1
  44. package/dist/x509.d.ts.map +1 -1
  45. package/dist/x509.js.map +1 -1
  46. package/package.json +2 -2
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Minimal ML-KEM-768 (FIPS 203) encapsulation-key ENCODING — just enough to put a
3
+ * WELL-FORMED encapsulation key into a TLS X25519MLKEM768 key_share so that a
4
+ * server which supports the hybrid group selects it directly (rather than only via
5
+ * a HelloRetryRequest, which misses servers that support-but-don't-prefer it).
6
+ *
7
+ * qProbe never completes the handshake, so it does NOT need real ML-KEM key
8
+ * generation (no NTT, matrix A, or CBD sampling). It only needs bytes the server's
9
+ * ML-KEM.Encaps input check accepts — a `ByteEncode₁₂` of coefficients in `[0, q)`
10
+ * plus a 32-byte ρ. FIPS 203 §7.2's "modulus check" (ByteEncode(ByteDecode(ek))==ek)
11
+ * passes for any in-range coefficients, so a random valid-range vector is accepted.
12
+ * The value is throwaway; we discard the (uncomputable-by-us) shared secret.
13
+ *
14
+ * Pure and unit-tested (encode/decode round-trip, length, in-range).
15
+ */
16
+ import { randomBytes } from "node:crypto";
17
+ export const ML_KEM_Q = 3329;
18
+ const N = 256; // coefficients per polynomial
19
+ const K = 3; // ML-KEM-768 rank
20
+ /** Encoded encapsulation-key length: 384·k bytes of ByteEncode₁₂ + 32-byte ρ. */
21
+ export const ML_KEM_768_EK_BYTES = 384 * K + 32; // 1184
22
+ /** ByteEncode₁₂: pack 256 12-bit coefficients (each in [0, q)) into 384 bytes. */
23
+ export function byteEncode12(coeffs) {
24
+ if (coeffs.length !== N)
25
+ throw new RangeError(`byteEncode12 expects ${N} coefficients`);
26
+ const out = Buffer.alloc((N * 12) / 8); // 384
27
+ for (let i = 0, o = 0; i < N; i += 2, o += 3) {
28
+ const a = coeffs[i] & 0xfff;
29
+ const b = coeffs[i + 1] & 0xfff;
30
+ out[o] = a & 0xff;
31
+ out[o + 1] = (a >> 8) | ((b & 0x0f) << 4);
32
+ out[o + 2] = b >> 4;
33
+ }
34
+ return out;
35
+ }
36
+ /** ByteDecode₁₂: inverse of {@link byteEncode12} (for tests). */
37
+ export function byteDecode12(bytes) {
38
+ if (bytes.length !== 384)
39
+ throw new RangeError("byteDecode12 expects 384 bytes");
40
+ const out = new Array(N);
41
+ for (let i = 0, o = 0; i < N; i += 2, o += 3) {
42
+ out[i] = bytes[o] | ((bytes[o + 1] & 0x0f) << 8);
43
+ out[i + 1] = (bytes[o + 1] >> 4) | (bytes[o + 2] << 4);
44
+ }
45
+ return out;
46
+ }
47
+ /** A random polynomial with all coefficients in [0, q) (throwaway, not uniform). */
48
+ function randomInRangePoly() {
49
+ const raw = randomBytes(N * 2);
50
+ const poly = new Array(N);
51
+ for (let i = 0; i < N; i++) {
52
+ // 16-bit sample reduced mod q — biased but always in [0, q), which is all the
53
+ // server's modulus check requires. This is a throwaway key.
54
+ poly[i] = raw.readUInt16BE(i * 2) % ML_KEM_Q;
55
+ }
56
+ return poly;
57
+ }
58
+ /**
59
+ * Build a WELL-FORMED ML-KEM-768 encapsulation key (1184 bytes): `ByteEncode₁₂` of
60
+ * a random in-range `t̂` (k=3 polynomials) followed by a 32-byte ρ. Accepted by a
61
+ * FIPS 203 encaps input check; the corresponding secret is not recoverable by us
62
+ * (and is not needed — we only observe which group the server selects).
63
+ */
64
+ export function wellFormedMlKem768Ek() {
65
+ const parts = [];
66
+ for (let i = 0; i < K; i++)
67
+ parts.push(byteEncode12(randomInRangePoly()));
68
+ parts.push(randomBytes(32)); // ρ
69
+ return Buffer.concat(parts);
70
+ }
71
+ //# sourceMappingURL=mlkem768.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mlkem768.js","sourceRoot":"","sources":["../src/mlkem768.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal ML-KEM-768 (FIPS 203) encapsulation-key ENCODING — just enough to put a\n * WELL-FORMED encapsulation key into a TLS X25519MLKEM768 key_share so that a\n * server which supports the hybrid group selects it directly (rather than only via\n * a HelloRetryRequest, which misses servers that support-but-don't-prefer it).\n *\n * qProbe never completes the handshake, so it does NOT need real ML-KEM key\n * generation (no NTT, matrix A, or CBD sampling). It only needs bytes the server's\n * ML-KEM.Encaps input check accepts — a `ByteEncode₁₂` of coefficients in `[0, q)`\n * plus a 32-byte ρ. FIPS 203 §7.2's \"modulus check\" (ByteEncode(ByteDecode(ek))==ek)\n * passes for any in-range coefficients, so a random valid-range vector is accepted.\n * The value is throwaway; we discard the (uncomputable-by-us) shared secret.\n *\n * Pure and unit-tested (encode/decode round-trip, length, in-range).\n */\nimport { randomBytes } from \"node:crypto\";\n\nexport const ML_KEM_Q = 3329;\nconst N = 256; // coefficients per polynomial\nconst K = 3; // ML-KEM-768 rank\n/** Encoded encapsulation-key length: 384·k bytes of ByteEncode₁₂ + 32-byte ρ. */\nexport const ML_KEM_768_EK_BYTES = 384 * K + 32; // 1184\n\n/** ByteEncode₁₂: pack 256 12-bit coefficients (each in [0, q)) into 384 bytes. */\nexport function byteEncode12(coeffs: readonly number[]): Buffer {\n if (coeffs.length !== N) throw new RangeError(`byteEncode12 expects ${N} coefficients`);\n const out = Buffer.alloc((N * 12) / 8); // 384\n for (let i = 0, o = 0; i < N; i += 2, o += 3) {\n const a = coeffs[i] & 0xfff;\n const b = coeffs[i + 1] & 0xfff;\n out[o] = a & 0xff;\n out[o + 1] = (a >> 8) | ((b & 0x0f) << 4);\n out[o + 2] = b >> 4;\n }\n return out;\n}\n\n/** ByteDecode₁₂: inverse of {@link byteEncode12} (for tests). */\nexport function byteDecode12(bytes: Buffer): number[] {\n if (bytes.length !== 384) throw new RangeError(\"byteDecode12 expects 384 bytes\");\n const out: number[] = new Array(N);\n for (let i = 0, o = 0; i < N; i += 2, o += 3) {\n out[i] = bytes[o] | ((bytes[o + 1] & 0x0f) << 8);\n out[i + 1] = (bytes[o + 1] >> 4) | (bytes[o + 2] << 4);\n }\n return out;\n}\n\n/** A random polynomial with all coefficients in [0, q) (throwaway, not uniform). */\nfunction randomInRangePoly(): number[] {\n const raw = randomBytes(N * 2);\n const poly: number[] = new Array(N);\n for (let i = 0; i < N; i++) {\n // 16-bit sample reduced mod q — biased but always in [0, q), which is all the\n // server's modulus check requires. This is a throwaway key.\n poly[i] = raw.readUInt16BE(i * 2) % ML_KEM_Q;\n }\n return poly;\n}\n\n/**\n * Build a WELL-FORMED ML-KEM-768 encapsulation key (1184 bytes): `ByteEncode₁₂` of\n * a random in-range `t̂` (k=3 polynomials) followed by a 32-byte ρ. Accepted by a\n * FIPS 203 encaps input check; the corresponding secret is not recoverable by us\n * (and is not needed — we only observe which group the server selects).\n */\nexport function wellFormedMlKem768Ek(): Buffer {\n const parts: Buffer[] = [];\n for (let i = 0; i < K; i++) parts.push(byteEncode12(randomInRangePoly()));\n parts.push(randomBytes(32)); // ρ\n return Buffer.concat(parts);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { type TlsNegotiated } from "./tls.js";
2
+ /** The 8-byte libpq SSLRequest message: length(0x00000008) + code(80877103). */
3
+ export declare function sslRequestFrame(): Buffer;
4
+ /** Probe a PostgreSQL endpoint: send SSLRequest, and on `S` upgrade to TLS. */
5
+ export declare function probePostgresSsl(host: string, port: number, opts?: {
6
+ servername?: string;
7
+ timeoutMs?: number;
8
+ }): Promise<TlsNegotiated>;
9
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAYA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAEjE,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CAiDxB"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * PostgreSQL SSLRequest probing. A Postgres server does not speak raw TLS on
3
+ * connect — the client first sends an 8-byte SSLRequest and the server answers a
4
+ * single byte: `S` (SSL available) or `N` (not). On `S` the same socket is upgraded
5
+ * to TLS, whose classical (EC)DHE key exchange protects the database session in
6
+ * transit and is harvest-now-decrypt-later exposed. We send the SSLRequest, and on
7
+ * `S` upgrade and reuse the direct TLS probe's negotiated-parameter inspection.
8
+ *
9
+ * The request-frame builder is pure and unit-tested; the socket dialog adds I/O.
10
+ */
11
+ import { connect as netConnect } from "node:net";
12
+ import { connect as tlsConnect } from "node:tls";
13
+ import { extractNegotiated } from "./tls.js";
14
+ /** The 8-byte libpq SSLRequest message: length(0x00000008) + code(80877103). */
15
+ export function sslRequestFrame() {
16
+ const b = Buffer.alloc(8);
17
+ b.writeInt32BE(8, 0); // message length
18
+ b.writeInt32BE(80877103, 4); // SSLRequest magic (0x04D2162F)
19
+ return b;
20
+ }
21
+ /** Probe a PostgreSQL endpoint: send SSLRequest, and on `S` upgrade to TLS. */
22
+ export function probePostgresSsl(host, port, opts = {}) {
23
+ const timeoutMs = opts.timeoutMs ?? 8000;
24
+ return new Promise((resolve) => {
25
+ let done = false;
26
+ const finish = (r) => {
27
+ if (done)
28
+ return;
29
+ done = true;
30
+ try {
31
+ socket.destroy();
32
+ }
33
+ catch {
34
+ /* already closed */
35
+ }
36
+ resolve(r);
37
+ };
38
+ const socket = netConnect({ host, port });
39
+ socket.setTimeout(timeoutMs);
40
+ socket.on("connect", () => socket.write(sslRequestFrame()));
41
+ socket.on("timeout", () => finish({ error: "timeout" }));
42
+ socket.on("error", (e) => finish({ error: e.message }));
43
+ socket.on("close", () => finish({ error: "connection closed" }));
44
+ socket.on("data", (chunk) => {
45
+ if (chunk.length < 1)
46
+ return;
47
+ const reply = String.fromCharCode(chunk[0]);
48
+ if (reply === "N")
49
+ return finish({ error: "server does not offer TLS (SSLRequest → N)" });
50
+ if (reply !== "S")
51
+ return finish({ error: `unexpected SSLRequest reply "${reply}"` });
52
+ socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners
53
+ const isIp = /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":");
54
+ const tls = tlsConnect({
55
+ socket,
56
+ servername: opts.servername ?? (isIp ? undefined : host),
57
+ rejectUnauthorized: false,
58
+ minVersion: "TLSv1.2",
59
+ });
60
+ tls.setTimeout(timeoutMs);
61
+ tls.on("timeout", () => finish({ error: "timeout" }));
62
+ tls.on("error", (e) => finish({ error: e.message }));
63
+ tls.on("secureConnect", () => {
64
+ const negotiated = extractNegotiated(tls);
65
+ if (done)
66
+ return;
67
+ done = true;
68
+ try {
69
+ tls.destroy();
70
+ }
71
+ catch {
72
+ /* already closed */
73
+ }
74
+ resolve(negotiated);
75
+ });
76
+ });
77
+ });
78
+ }
79
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PostgreSQL SSLRequest probing. A Postgres server does not speak raw TLS on\n * connect — the client first sends an 8-byte SSLRequest and the server answers a\n * single byte: `S` (SSL available) or `N` (not). On `S` the same socket is upgraded\n * to TLS, whose classical (EC)DHE key exchange protects the database session in\n * transit and is harvest-now-decrypt-later exposed. We send the SSLRequest, and on\n * `S` upgrade and reuse the direct TLS probe's negotiated-parameter inspection.\n *\n * The request-frame builder is pure and unit-tested; the socket dialog adds I/O.\n */\nimport { connect as netConnect } from \"node:net\";\nimport { connect as tlsConnect } from \"node:tls\";\nimport { extractNegotiated, type TlsNegotiated } from \"./tls.js\";\n\n/** The 8-byte libpq SSLRequest message: length(0x00000008) + code(80877103). */\nexport function sslRequestFrame(): Buffer {\n const b = Buffer.alloc(8);\n b.writeInt32BE(8, 0); // message length\n b.writeInt32BE(80877103, 4); // SSLRequest magic (0x04D2162F)\n return b;\n}\n\n/** Probe a PostgreSQL endpoint: send SSLRequest, and on `S` upgrade to TLS. */\nexport function probePostgresSsl(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n try {\n socket.destroy();\n } catch {\n /* already closed */\n }\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"connect\", () => socket.write(sslRequestFrame()));\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n socket.on(\"close\", () => finish({ error: \"connection closed\" }));\n socket.on(\"data\", (chunk: Buffer) => {\n if (chunk.length < 1) return;\n const reply = String.fromCharCode(chunk[0]);\n if (reply === \"N\") return finish({ error: \"server does not offer TLS (SSLRequest → N)\" });\n if (reply !== \"S\") return finish({ error: `unexpected SSLRequest reply \"${reply}\"` });\n socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const tls = tlsConnect({\n socket,\n servername: opts.servername ?? (isIp ? undefined : host),\n rejectUnauthorized: false,\n minVersion: \"TLSv1.2\",\n });\n tls.setTimeout(timeoutMs);\n tls.on(\"timeout\", () => finish({ error: \"timeout\" }));\n tls.on(\"error\", (e) => finish({ error: e.message }));\n tls.on(\"secureConnect\", () => {\n const negotiated = extractNegotiated(tls);\n if (done) return;\n done = true;\n try {\n tls.destroy();\n } catch {\n /* already closed */\n }\n resolve(negotiated);\n });\n });\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"smtp.d.ts","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"AAYA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAEjE,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKtD;AAED,gEAAgE;AAChE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CA8DxB"}
1
+ {"version":3,"file":"smtp.d.ts","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"AAYA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAEjE,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKtD;AAED,gEAAgE;AAChE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CAkExB"}
package/dist/smtp.js CHANGED
@@ -50,6 +50,10 @@ export function probeSmtpStartTls(host, port, opts = {}) {
50
50
  socket.setTimeout(timeoutMs);
51
51
  socket.on("timeout", () => finish({ error: "timeout" }));
52
52
  socket.on("error", (e) => finish({ error: e.message }));
53
+ // Clean close during the plaintext SMTP phase would otherwise hang the Promise
54
+ // (the timeout does not fire post-close). Guarded by `done`; once STARTTLS has
55
+ // upgraded, a successful probe has already resolved so this is a no-op.
56
+ socket.on("close", () => finish({ error: "connection closed" }));
53
57
  socket.on("data", (chunk) => {
54
58
  buf = Buffer.concat([buf, chunk]);
55
59
  if (buf.length > 128 * 1024)
@@ -72,7 +76,7 @@ export function probeSmtpStartTls(host, port, opts = {}) {
72
76
  else {
73
77
  if (!/^220 /m.test(text))
74
78
  return finish({ error: "STARTTLS refused" });
75
- socket.removeAllListeners("data");
79
+ socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners
76
80
  const isIp = /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":");
77
81
  const tls = tlsConnect({
78
82
  socket,
package/dist/smtp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"smtp.js","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SMTP STARTTLS probing. Mail transport is \"communication between things\": an MTA\n * that upgrades to TLS via STARTTLS negotiates the same classical (EC)DHE key\n * exchange as any TLS endpoint, so the session protecting mail in transit is\n * harvest-now-decrypt-later exposed. We speak the minimal SMTP dialog\n * (banner → EHLO → STARTTLS), upgrade the socket to TLS, and reuse the same\n * negotiated-parameter inspection as the direct TLS probe.\n *\n * The reply-framing helpers are pure and unit-tested; the socket dialog adds I/O.\n */\nimport { connect as netConnect } from \"node:net\";\nimport { connect as tlsConnect } from \"node:tls\";\nimport { extractNegotiated, type TlsNegotiated } from \"./tls.js\";\n\n/** True once `buf` contains a COMPLETE SMTP reply (last line is `NNN␠…`). */\nexport function smtpReplyComplete(buf: string): boolean {\n if (!/\\n$/.test(buf)) return false;\n const lines = buf.replace(/\\r?\\n$/, \"\").split(/\\r?\\n/);\n const last = lines[lines.length - 1] ?? \"\";\n return /^\\d{3} /.test(last); // a space (not '-') after the code = final line\n}\n\n/** True if an EHLO reply advertises the STARTTLS capability. */\nexport function smtpAdvertisesStartTls(ehlo: string): boolean {\n return /^\\d{3}[- ]STARTTLS\\b/im.test(ehlo);\n}\n\n/**\n * Probe an SMTP endpoint: dial, EHLO, STARTTLS, upgrade to TLS, and read the\n * negotiated parameters + certificate posture. Returns `{ error }` when the server\n * does not offer STARTTLS or the upgrade fails.\n */\nexport function probeSmtpStartTls(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n let buf = Buffer.alloc(0);\n let stage: \"banner\" | \"ehlo\" | \"starttls\" = \"banner\";\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n try {\n socket.destroy();\n } catch {\n /* already closed */\n }\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > 128 * 1024) return finish({ error: \"SMTP response too large\" });\n const text = buf.toString(\"ascii\");\n if (!smtpReplyComplete(text)) return;\n\n if (stage === \"banner\") {\n buf = Buffer.alloc(0);\n stage = \"ehlo\";\n socket.write(\"EHLO qprobe.local\\r\\n\");\n } else if (stage === \"ehlo\") {\n if (!smtpAdvertisesStartTls(text)) return finish({ error: \"STARTTLS not advertised\" });\n buf = Buffer.alloc(0);\n stage = \"starttls\";\n socket.write(\"STARTTLS\\r\\n\");\n } else {\n if (!/^220 /m.test(text)) return finish({ error: \"STARTTLS refused\" });\n socket.removeAllListeners(\"data\");\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const tls = tlsConnect({\n socket,\n servername: opts.servername ?? (isIp ? undefined : host),\n rejectUnauthorized: false,\n minVersion: \"TLSv1.2\",\n });\n tls.setTimeout(timeoutMs);\n tls.on(\"timeout\", () => finish({ error: \"timeout\" }));\n tls.on(\"error\", (e) => finish({ error: e.message }));\n tls.on(\"secureConnect\", () => {\n const negotiated = extractNegotiated(tls);\n if (done) return;\n done = true;\n try {\n tls.destroy();\n } catch {\n /* already closed */\n }\n resolve(negotiated);\n });\n }\n });\n });\n}\n"]}
1
+ {"version":3,"file":"smtp.js","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SMTP STARTTLS probing. Mail transport is \"communication between things\": an MTA\n * that upgrades to TLS via STARTTLS negotiates the same classical (EC)DHE key\n * exchange as any TLS endpoint, so the session protecting mail in transit is\n * harvest-now-decrypt-later exposed. We speak the minimal SMTP dialog\n * (banner → EHLO → STARTTLS), upgrade the socket to TLS, and reuse the same\n * negotiated-parameter inspection as the direct TLS probe.\n *\n * The reply-framing helpers are pure and unit-tested; the socket dialog adds I/O.\n */\nimport { connect as netConnect } from \"node:net\";\nimport { connect as tlsConnect } from \"node:tls\";\nimport { extractNegotiated, type TlsNegotiated } from \"./tls.js\";\n\n/** True once `buf` contains a COMPLETE SMTP reply (last line is `NNN␠…`). */\nexport function smtpReplyComplete(buf: string): boolean {\n if (!/\\n$/.test(buf)) return false;\n const lines = buf.replace(/\\r?\\n$/, \"\").split(/\\r?\\n/);\n const last = lines[lines.length - 1] ?? \"\";\n return /^\\d{3} /.test(last); // a space (not '-') after the code = final line\n}\n\n/** True if an EHLO reply advertises the STARTTLS capability. */\nexport function smtpAdvertisesStartTls(ehlo: string): boolean {\n return /^\\d{3}[- ]STARTTLS\\b/im.test(ehlo);\n}\n\n/**\n * Probe an SMTP endpoint: dial, EHLO, STARTTLS, upgrade to TLS, and read the\n * negotiated parameters + certificate posture. Returns `{ error }` when the server\n * does not offer STARTTLS or the upgrade fails.\n */\nexport function probeSmtpStartTls(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n let buf = Buffer.alloc(0);\n let stage: \"banner\" | \"ehlo\" | \"starttls\" = \"banner\";\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n try {\n socket.destroy();\n } catch {\n /* already closed */\n }\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n // Clean close during the plaintext SMTP phase would otherwise hang the Promise\n // (the timeout does not fire post-close). Guarded by `done`; once STARTTLS has\n // upgraded, a successful probe has already resolved so this is a no-op.\n socket.on(\"close\", () => finish({ error: \"connection closed\" }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > 128 * 1024) return finish({ error: \"SMTP response too large\" });\n const text = buf.toString(\"ascii\");\n if (!smtpReplyComplete(text)) return;\n\n if (stage === \"banner\") {\n buf = Buffer.alloc(0);\n stage = \"ehlo\";\n socket.write(\"EHLO qprobe.local\\r\\n\");\n } else if (stage === \"ehlo\") {\n if (!smtpAdvertisesStartTls(text)) return finish({ error: \"STARTTLS not advertised\" });\n buf = Buffer.alloc(0);\n stage = \"starttls\";\n socket.write(\"STARTTLS\\r\\n\");\n } else {\n if (!/^220 /m.test(text)) return finish({ error: \"STARTTLS refused\" });\n socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const tls = tlsConnect({\n socket,\n servername: opts.servername ?? (isIp ? undefined : host),\n rejectUnauthorized: false,\n minVersion: \"TLSv1.2\",\n });\n tls.setTimeout(timeoutMs);\n tls.on(\"timeout\", () => finish({ error: \"timeout\" }));\n tls.on(\"error\", (e) => finish({ error: e.message }));\n tls.on(\"secureConnect\", () => {\n const negotiated = extractNegotiated(tls);\n if (done) return;\n done = true;\n try {\n tls.destroy();\n } catch {\n /* already closed */\n }\n resolve(negotiated);\n });\n }\n });\n });\n}\n"]}
package/dist/ssh.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"AAeA,wEAAwE;AACxE,eAAO,MAAM,UAAU,UAMtB,CAAC;AAEF,MAAM,WAAW,OAAO;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAYD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CASrD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU5E;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAmC9F"}
1
+ {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"AAeA,wEAAwE;AACxE,eAAO,MAAM,UAAU,UAMtB,CAAC;AAEF,MAAM,WAAW,OAAO;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAYD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CASrD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU5E;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAuC9F"}
package/dist/ssh.js CHANGED
@@ -91,6 +91,10 @@ export function probeSsh(host, port, timeoutMs = 8000) {
91
91
  socket.on("connect", () => socket.write("SSH-2.0-qprobe_0.1\r\n"));
92
92
  socket.on("timeout", () => finish({ pqKexOffered: false, error: "timeout" }));
93
93
  socket.on("error", (e) => finish({ pqKexOffered: false, error: e.message }));
94
+ // A peer that accepts then cleanly closes (FIN) before a complete KEXINIT would
95
+ // otherwise leave the Promise unsettled (the timeout timer does not fire after a
96
+ // clean close). The `done` guard makes this a no-op once we've already resolved.
97
+ socket.on("close", () => finish({ pqKexOffered: false, error: "connection closed" }));
94
98
  socket.on("data", (chunk) => {
95
99
  buf = Buffer.concat([buf, chunk]);
96
100
  if (buf.length > 512 * 1024) {
package/dist/ssh.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSH endpoint inspection. An SSH server sends its identification banner and then\n * a cleartext SSH_MSG_KEXINIT (message 20) BEFORE any encryption or authentication\n * — so reading the offered key-exchange and host-key algorithm name-lists needs no\n * credentials and performs no crypto. We look for post-quantum KEX\n * (`sntrup761x25519-sha512@openssh.com`, `mlkem768x25519-sha256`) as a positive\n * readiness signal, and flag classical-only servers.\n *\n * The wire parsing (`parseKexinit`) is pure and unit-tested; `probeSsh` adds the\n * socket I/O.\n */\nimport { connect } from \"node:net\";\n\nconst SSH_MSG_KEXINIT = 20;\n\n/** PQC / hybrid SSH key-exchange algorithm names (OpenSSH + drafts). */\nexport const PQ_SSH_KEX = [\n \"sntrup761x25519-sha512@openssh.com\",\n \"sntrup761x25519-sha512\",\n \"mlkem768x25519-sha256\",\n \"mlkem768nistp256-sha256\",\n \"mlkem1024nistp384-sha384\",\n];\n\nexport interface KexInit {\n kexAlgorithms: string[];\n hostKeyAlgorithms: string[];\n}\n\n/** Read a `uint32`-length-prefixed comma-separated name-list at `off`. */\nfunction readNameList(payload: Buffer, off: number): { list: string[]; next: number } {\n if (off + 4 > payload.length) throw new RangeError(\"truncated name-list length\");\n const len = payload.readUInt32BE(off);\n const start = off + 4;\n if (start + len > payload.length) throw new RangeError(\"truncated name-list\");\n const s = payload.subarray(start, start + len).toString(\"ascii\");\n return { list: s === \"\" ? [] : s.split(\",\"), next: start + len };\n}\n\n/**\n * Parse an SSH_MSG_KEXINIT payload (starting at the message-code byte) into its\n * kex and host-key algorithm name-lists.\n */\nexport function parseKexinit(payload: Buffer): KexInit {\n if (payload.length < 1 + 16 || payload[0] !== SSH_MSG_KEXINIT) {\n throw new RangeError(\"not an SSH_MSG_KEXINIT payload\");\n }\n let off = 1 + 16; // message code + 16-byte cookie\n const kex = readNameList(payload, off);\n off = kex.next;\n const hostKey = readNameList(payload, off);\n return { kexAlgorithms: kex.list, hostKeyAlgorithms: hostKey.list };\n}\n\n/**\n * Extract the first binary SSH packet's payload from a raw server buffer that\n * begins after the identification banner line. Returns undefined if the packet is\n * not yet complete.\n */\nexport function extractPacketPayload(afterBanner: Buffer): Buffer | undefined {\n if (afterBanner.length < 5) return undefined;\n const packetLength = afterBanner.readUInt32BE(0);\n if (packetLength < 2 || packetLength > 1_000_000)\n throw new RangeError(\"implausible SSH packet length\");\n if (afterBanner.length < 4 + packetLength) return undefined;\n const paddingLength = afterBanner[4];\n const payloadLen = packetLength - paddingLength - 1;\n if (payloadLen < 1) throw new RangeError(\"bad SSH padding\");\n return afterBanner.subarray(5, 5 + payloadLen);\n}\n\n/** Locate the end of the SSH identification banner line (`SSH-...\\n`). */\nexport function bannerEnd(buf: Buffer): number | undefined {\n // Servers may send pre-banner lines; find the SSH- line and its terminating LF.\n const idx = buf.indexOf(\"SSH-\");\n if (idx < 0) return undefined;\n const lf = buf.indexOf(0x0a, idx);\n if (lf < 0) return undefined;\n return lf + 1;\n}\n\nexport interface SshProbeResult {\n banner?: string;\n kex?: KexInit;\n pqKexOffered: boolean;\n error?: string;\n}\n\n/** Connect to an SSH endpoint and read its KEXINIT (no auth, no crypto). */\nexport function probeSsh(host: string, port: number, timeoutMs = 8000): Promise<SshProbeResult> {\n return new Promise((resolve) => {\n let buf = Buffer.alloc(0);\n let done = false;\n const finish = (r: SshProbeResult) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n const socket = connect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"connect\", () => socket.write(\"SSH-2.0-qprobe_0.1\\r\\n\"));\n socket.on(\"timeout\", () => finish({ pqKexOffered: false, error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ pqKexOffered: false, error: e.message }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > 512 * 1024) {\n finish({ pqKexOffered: false, error: \"response too large\" });\n return;\n }\n const end = bannerEnd(buf);\n if (end === undefined) return;\n const banner = buf.subarray(0, end).toString(\"ascii\").trim();\n try {\n const payload = extractPacketPayload(buf.subarray(end));\n if (!payload) return; // wait for more data\n const kex = parseKexinit(payload);\n const pqKexOffered = kex.kexAlgorithms.some((a) => PQ_SSH_KEX.includes(a));\n finish({ banner, kex, pqKexOffered });\n } catch (e) {\n finish({ banner, pqKexOffered: false, error: (e as Error).message });\n }\n });\n });\n}\n"]}
1
+ {"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSH endpoint inspection. An SSH server sends its identification banner and then\n * a cleartext SSH_MSG_KEXINIT (message 20) BEFORE any encryption or authentication\n * — so reading the offered key-exchange and host-key algorithm name-lists needs no\n * credentials and performs no crypto. We look for post-quantum KEX\n * (`sntrup761x25519-sha512@openssh.com`, `mlkem768x25519-sha256`) as a positive\n * readiness signal, and flag classical-only servers.\n *\n * The wire parsing (`parseKexinit`) is pure and unit-tested; `probeSsh` adds the\n * socket I/O.\n */\nimport { connect } from \"node:net\";\n\nconst SSH_MSG_KEXINIT = 20;\n\n/** PQC / hybrid SSH key-exchange algorithm names (OpenSSH + drafts). */\nexport const PQ_SSH_KEX = [\n \"sntrup761x25519-sha512@openssh.com\",\n \"sntrup761x25519-sha512\",\n \"mlkem768x25519-sha256\",\n \"mlkem768nistp256-sha256\",\n \"mlkem1024nistp384-sha384\",\n];\n\nexport interface KexInit {\n kexAlgorithms: string[];\n hostKeyAlgorithms: string[];\n}\n\n/** Read a `uint32`-length-prefixed comma-separated name-list at `off`. */\nfunction readNameList(payload: Buffer, off: number): { list: string[]; next: number } {\n if (off + 4 > payload.length) throw new RangeError(\"truncated name-list length\");\n const len = payload.readUInt32BE(off);\n const start = off + 4;\n if (start + len > payload.length) throw new RangeError(\"truncated name-list\");\n const s = payload.subarray(start, start + len).toString(\"ascii\");\n return { list: s === \"\" ? [] : s.split(\",\"), next: start + len };\n}\n\n/**\n * Parse an SSH_MSG_KEXINIT payload (starting at the message-code byte) into its\n * kex and host-key algorithm name-lists.\n */\nexport function parseKexinit(payload: Buffer): KexInit {\n if (payload.length < 1 + 16 || payload[0] !== SSH_MSG_KEXINIT) {\n throw new RangeError(\"not an SSH_MSG_KEXINIT payload\");\n }\n let off = 1 + 16; // message code + 16-byte cookie\n const kex = readNameList(payload, off);\n off = kex.next;\n const hostKey = readNameList(payload, off);\n return { kexAlgorithms: kex.list, hostKeyAlgorithms: hostKey.list };\n}\n\n/**\n * Extract the first binary SSH packet's payload from a raw server buffer that\n * begins after the identification banner line. Returns undefined if the packet is\n * not yet complete.\n */\nexport function extractPacketPayload(afterBanner: Buffer): Buffer | undefined {\n if (afterBanner.length < 5) return undefined;\n const packetLength = afterBanner.readUInt32BE(0);\n if (packetLength < 2 || packetLength > 1_000_000)\n throw new RangeError(\"implausible SSH packet length\");\n if (afterBanner.length < 4 + packetLength) return undefined;\n const paddingLength = afterBanner[4];\n const payloadLen = packetLength - paddingLength - 1;\n if (payloadLen < 1) throw new RangeError(\"bad SSH padding\");\n return afterBanner.subarray(5, 5 + payloadLen);\n}\n\n/** Locate the end of the SSH identification banner line (`SSH-...\\n`). */\nexport function bannerEnd(buf: Buffer): number | undefined {\n // Servers may send pre-banner lines; find the SSH- line and its terminating LF.\n const idx = buf.indexOf(\"SSH-\");\n if (idx < 0) return undefined;\n const lf = buf.indexOf(0x0a, idx);\n if (lf < 0) return undefined;\n return lf + 1;\n}\n\nexport interface SshProbeResult {\n banner?: string;\n kex?: KexInit;\n pqKexOffered: boolean;\n error?: string;\n}\n\n/** Connect to an SSH endpoint and read its KEXINIT (no auth, no crypto). */\nexport function probeSsh(host: string, port: number, timeoutMs = 8000): Promise<SshProbeResult> {\n return new Promise((resolve) => {\n let buf = Buffer.alloc(0);\n let done = false;\n const finish = (r: SshProbeResult) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n const socket = connect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"connect\", () => socket.write(\"SSH-2.0-qprobe_0.1\\r\\n\"));\n socket.on(\"timeout\", () => finish({ pqKexOffered: false, error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ pqKexOffered: false, error: e.message }));\n // A peer that accepts then cleanly closes (FIN) before a complete KEXINIT would\n // otherwise leave the Promise unsettled (the timeout timer does not fire after a\n // clean close). The `done` guard makes this a no-op once we've already resolved.\n socket.on(\"close\", () => finish({ pqKexOffered: false, error: \"connection closed\" }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > 512 * 1024) {\n finish({ pqKexOffered: false, error: \"response too large\" });\n return;\n }\n const end = bannerEnd(buf);\n if (end === undefined) return;\n const banner = buf.subarray(0, end).toString(\"ascii\").trim();\n try {\n const payload = extractPacketPayload(buf.subarray(end));\n if (!payload) return; // wait for more data\n const kex = parseKexinit(payload);\n const pqKexOffered = kex.kexAlgorithms.some((a) => PQ_SSH_KEX.includes(a));\n finish({ banner, kex, pqKexOffered });\n } catch (e) {\n finish({ banner, pqKexOffered: false, error: (e as Error).message });\n }\n });\n });\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { type TlsNegotiated } from "./tls.js";
2
+ /** A one-shot line-based STARTTLS dialog: send `command`, expect `success`, upgrade. */
3
+ interface StartTlsDialog {
4
+ /** The upgrade command to send after the server greeting. */
5
+ command: string;
6
+ /** Matches the server reply that grants the TLS upgrade. */
7
+ success: RegExp;
8
+ }
9
+ /** IMAP (RFC 3501): `* OK` greeting → `a1 STARTTLS` → `a1 OK`. */
10
+ export declare const IMAP_DIALOG: StartTlsDialog;
11
+ /** POP3 (RFC 2595): `+OK` greeting → `STLS` → `+OK`. */
12
+ export declare const POP3_DIALOG: StartTlsDialog;
13
+ /**
14
+ * Probe a line-based STARTTLS endpoint (IMAP/POP3): read the greeting line, send
15
+ * the upgrade command, and on a success reply upgrade to TLS and read the
16
+ * negotiated parameters + certificate posture. Returns `{ error }` when the server
17
+ * does not offer the upgrade or it fails.
18
+ */
19
+ export declare function probeLineStartTls(host: string, port: number, dialog: StartTlsDialog, opts?: {
20
+ servername?: string;
21
+ timeoutMs?: number;
22
+ }): Promise<TlsNegotiated>;
23
+ export {};
24
+ //# sourceMappingURL=starttls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starttls.d.ts","sourceRoot":"","sources":["../src/starttls.ts"],"names":[],"mappings":"AAaA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAEjE,wFAAwF;AACxF,UAAU,cAAc;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kEAAkE;AAClE,eAAO,MAAM,WAAW,EAAE,cAGzB,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,WAAW,EAAE,cAGzB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EACtB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CA4DxB"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Generic line-based STARTTLS probing for IMAP and POP3 — the mail-retrieval
3
+ * cousins of SMTP. Both greet with a single status line, take a one-shot upgrade
4
+ * command (`a1 STARTTLS` / `STLS`), and then speak TLS on the same socket, so the
5
+ * post-upgrade key exchange is the same classical (EC)DHE surface as any TLS
6
+ * endpoint (harvest-now-decrypt-later exposed). We speak the minimal dialog and
7
+ * reuse the direct TLS probe's negotiated-parameter inspection.
8
+ *
9
+ * (SMTP has a capability-negotiation step (EHLO) and lives in smtp.ts. LDAP
10
+ * StartTLS is a binary ASN.1 extended operation and is intentionally out of scope.)
11
+ */
12
+ import { connect as netConnect } from "node:net";
13
+ import { connect as tlsConnect } from "node:tls";
14
+ import { extractNegotiated } from "./tls.js";
15
+ /** IMAP (RFC 3501): `* OK` greeting → `a1 STARTTLS` → `a1 OK`. */
16
+ export const IMAP_DIALOG = {
17
+ command: "a1 STARTTLS\r\n",
18
+ success: /^a1 OK/im,
19
+ };
20
+ /** POP3 (RFC 2595): `+OK` greeting → `STLS` → `+OK`. */
21
+ export const POP3_DIALOG = {
22
+ command: "STLS\r\n",
23
+ success: /^\+OK/im,
24
+ };
25
+ /**
26
+ * Probe a line-based STARTTLS endpoint (IMAP/POP3): read the greeting line, send
27
+ * the upgrade command, and on a success reply upgrade to TLS and read the
28
+ * negotiated parameters + certificate posture. Returns `{ error }` when the server
29
+ * does not offer the upgrade or it fails.
30
+ */
31
+ export function probeLineStartTls(host, port, dialog, opts = {}) {
32
+ const timeoutMs = opts.timeoutMs ?? 8000;
33
+ return new Promise((resolve) => {
34
+ let done = false;
35
+ let buf = Buffer.alloc(0);
36
+ let stage = "greeting";
37
+ const finish = (r) => {
38
+ if (done)
39
+ return;
40
+ done = true;
41
+ try {
42
+ socket.destroy();
43
+ }
44
+ catch {
45
+ /* already closed */
46
+ }
47
+ resolve(r);
48
+ };
49
+ const socket = netConnect({ host, port });
50
+ socket.setTimeout(timeoutMs);
51
+ socket.on("timeout", () => finish({ error: "timeout" }));
52
+ socket.on("error", (e) => finish({ error: e.message }));
53
+ // Clean close during the plaintext phase would otherwise hang the Promise.
54
+ socket.on("close", () => finish({ error: "connection closed" }));
55
+ socket.on("data", (chunk) => {
56
+ buf = Buffer.concat([buf, chunk]);
57
+ if (buf.length > 64 * 1024)
58
+ return finish({ error: "response too large" });
59
+ const text = buf.toString("ascii");
60
+ if (!/\n/.test(text))
61
+ return; // wait for a complete line
62
+ if (stage === "greeting") {
63
+ buf = Buffer.alloc(0);
64
+ stage = "response";
65
+ socket.write(dialog.command);
66
+ return;
67
+ }
68
+ // stage === "response"
69
+ if (!dialog.success.test(text))
70
+ return finish({ error: "STARTTLS not offered" });
71
+ socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners
72
+ const isIp = /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":");
73
+ const tls = tlsConnect({
74
+ socket,
75
+ servername: opts.servername ?? (isIp ? undefined : host),
76
+ rejectUnauthorized: false,
77
+ minVersion: "TLSv1.2",
78
+ });
79
+ tls.setTimeout(timeoutMs);
80
+ tls.on("timeout", () => finish({ error: "timeout" }));
81
+ tls.on("error", (e) => finish({ error: e.message }));
82
+ tls.on("secureConnect", () => {
83
+ const negotiated = extractNegotiated(tls);
84
+ if (done)
85
+ return;
86
+ done = true;
87
+ try {
88
+ tls.destroy();
89
+ }
90
+ catch {
91
+ /* already closed */
92
+ }
93
+ resolve(negotiated);
94
+ });
95
+ });
96
+ });
97
+ }
98
+ //# sourceMappingURL=starttls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starttls.js","sourceRoot":"","sources":["../src/starttls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generic line-based STARTTLS probing for IMAP and POP3 — the mail-retrieval\n * cousins of SMTP. Both greet with a single status line, take a one-shot upgrade\n * command (`a1 STARTTLS` / `STLS`), and then speak TLS on the same socket, so the\n * post-upgrade key exchange is the same classical (EC)DHE surface as any TLS\n * endpoint (harvest-now-decrypt-later exposed). We speak the minimal dialog and\n * reuse the direct TLS probe's negotiated-parameter inspection.\n *\n * (SMTP has a capability-negotiation step (EHLO) and lives in smtp.ts. LDAP\n * StartTLS is a binary ASN.1 extended operation and is intentionally out of scope.)\n */\nimport { connect as netConnect } from \"node:net\";\nimport { connect as tlsConnect } from \"node:tls\";\nimport { extractNegotiated, type TlsNegotiated } from \"./tls.js\";\n\n/** A one-shot line-based STARTTLS dialog: send `command`, expect `success`, upgrade. */\ninterface StartTlsDialog {\n /** The upgrade command to send after the server greeting. */\n command: string;\n /** Matches the server reply that grants the TLS upgrade. */\n success: RegExp;\n}\n\n/** IMAP (RFC 3501): `* OK` greeting → `a1 STARTTLS` → `a1 OK`. */\nexport const IMAP_DIALOG: StartTlsDialog = {\n command: \"a1 STARTTLS\\r\\n\",\n success: /^a1 OK/im,\n};\n\n/** POP3 (RFC 2595): `+OK` greeting → `STLS` → `+OK`. */\nexport const POP3_DIALOG: StartTlsDialog = {\n command: \"STLS\\r\\n\",\n success: /^\\+OK/im,\n};\n\n/**\n * Probe a line-based STARTTLS endpoint (IMAP/POP3): read the greeting line, send\n * the upgrade command, and on a success reply upgrade to TLS and read the\n * negotiated parameters + certificate posture. Returns `{ error }` when the server\n * does not offer the upgrade or it fails.\n */\nexport function probeLineStartTls(\n host: string,\n port: number,\n dialog: StartTlsDialog,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n let buf = Buffer.alloc(0);\n let stage: \"greeting\" | \"response\" = \"greeting\";\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n try {\n socket.destroy();\n } catch {\n /* already closed */\n }\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n // Clean close during the plaintext phase would otherwise hang the Promise.\n socket.on(\"close\", () => finish({ error: \"connection closed\" }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > 64 * 1024) return finish({ error: \"response too large\" });\n const text = buf.toString(\"ascii\");\n if (!/\\n/.test(text)) return; // wait for a complete line\n\n if (stage === \"greeting\") {\n buf = Buffer.alloc(0);\n stage = \"response\";\n socket.write(dialog.command);\n return;\n }\n // stage === \"response\"\n if (!dialog.success.test(text)) return finish({ error: \"STARTTLS not offered\" });\n socket.removeAllListeners(); // the TLS socket now owns the connection; drop stale net listeners\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const tls = tlsConnect({\n socket,\n servername: opts.servername ?? (isIp ? undefined : host),\n rejectUnauthorized: false,\n minVersion: \"TLSv1.2\",\n });\n tls.setTimeout(timeoutMs);\n tls.on(\"timeout\", () => finish({ error: \"timeout\" }));\n tls.on(\"error\", (e) => finish({ error: e.message }));\n tls.on(\"secureConnect\", () => {\n const negotiated = extractNegotiated(tls);\n if (done) return;\n done = true;\n try {\n tls.destroy();\n } catch {\n /* already closed */\n }\n resolve(negotiated);\n });\n });\n });\n}\n"]}
package/dist/tls.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAUjE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,CAmBlE;AAED,yEAAyE;AACzE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CAiCxB"}
1
+ {"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAUjE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,CAmBlE;AAED,yEAAyE;AACzE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC,aAAa,CAAC,CAgDxB"}
package/dist/tls.js CHANGED
@@ -89,9 +89,25 @@ export function probeHybridSupport(host, port, opts = {}) {
89
89
  });
90
90
  socket.on("timeout", () => finish({ hybridSelected: false, error: "timeout" }));
91
91
  socket.on("error", (e) => finish({ hybridSelected: false, error: e.message }));
92
+ // Clean close before a ServerHello would otherwise hang the Promise (the timeout
93
+ // does not fire post-close). Guarded by `done` so a normal finish wins.
94
+ socket.on("close", () => finish({ hybridSelected: false, error: "connection closed" }));
92
95
  socket.on("data", (chunk) => {
93
96
  buf = Buffer.concat([buf, chunk]);
94
- const sh = readServerHello(buf);
97
+ let sh;
98
+ try {
99
+ sh = readServerHello(buf);
100
+ }
101
+ catch {
102
+ // A hostile or broken endpoint can send a length-consistent but internally
103
+ // truncated ServerHello, which trips the parser's bounds check. readServerHello
104
+ // only parses a handshake body that is already fully present, so a throw means
105
+ // the message is malformed, not merely incomplete — finish gracefully instead
106
+ // of letting the RangeError escape this data handler as an uncaught exception
107
+ // (which would crash the process). Mirrors the guarded parse in ssh.ts.
108
+ finish({ hybridSelected: false, error: "malformed ServerHello" });
109
+ return;
110
+ }
95
111
  if (!sh) {
96
112
  if (buf.length > 64 * 1024)
97
113
  finish({ hybridSelected: false, error: "no ServerHello" });
package/dist/tls.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tls.js","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TLS endpoint inspection. Two engines:\n * 1. `probeTlsNegotiated` — a normal `node:tls` handshake to read what the server\n * actually negotiates: TLS version, cipher suite, ephemeral key-exchange group\n * (the harvestable part), and the leaf certificate's public-key type/size.\n * 2. `probeHybridSupport` — a raw ClientHello (clienthello.ts) advertising\n * X25519MLKEM768, to detect PQC-HYBRID support that Node's bundled OpenSSL\n * cannot itself negotiate.\n *\n * `engine disposes`: we read the negotiated reality and disconnect; we never\n * modify the endpoint.\n */\nimport { connect as tlsConnect, type TLSSocket } from \"node:tls\";\nimport { connect as netConnect } from \"node:net\";\nimport {\n buildClientHello,\n readServerHello,\n GROUP_X25519MLKEM768,\n type ServerHelloInfo,\n} from \"./clienthello.js\";\nimport { certSignatureAlgorithm } from \"./x509.js\";\n\nexport interface TlsNegotiated {\n protocol?: string;\n cipher?: string;\n /** Ephemeral key-exchange group, e.g. \"X25519\", \"P-256\", \"DH\". */\n kexGroup?: string;\n kexType?: string;\n /** Leaf certificate public-key summary. */\n certKeyType?: string;\n certKeyBits?: number;\n certSubject?: string;\n /** How the leaf certificate is SIGNED (the CA's algorithm) — e.g. \"RSA\", \"ECDSA\". */\n certSigFamily?: string;\n certSigOid?: string;\n error?: string;\n}\n\n/** Read the negotiated parameters + leaf-cert posture from a connected TLS socket. */\nexport function extractNegotiated(socket: TLSSocket): TlsNegotiated {\n const cipher = socket.getCipher();\n const kex = socket.getEphemeralKeyInfo();\n const cert = socket.getPeerCertificate(false);\n const cn = cert?.subject?.CN;\n // node:tls exposes the public-key type but not how the leaf was SIGNED; read the\n // signature algorithm (the forgeable-at-Q-day part) from the raw DER.\n const sig = cert?.raw ? certSignatureAlgorithm(cert.raw) : undefined;\n return {\n protocol: socket.getProtocol() ?? undefined,\n cipher: cipher?.standardName ?? cipher?.name,\n kexGroup: kex && \"name\" in kex ? kex.name : undefined,\n kexType: kex && \"type\" in kex ? kex.type : undefined,\n certKeyType: cert?.asn1Curve ? `EC(${cert.asn1Curve})` : cert?.pubkey ? \"RSA\" : undefined,\n certKeyBits: typeof cert?.bits === \"number\" ? cert.bits : undefined,\n certSubject: Array.isArray(cn) ? cn[0] : cn,\n certSigFamily: sig?.family,\n certSigOid: sig?.oid,\n };\n}\n\n/** Perform a normal TLS handshake and read the negotiated parameters. */\nexport function probeTlsNegotiated(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n // SNI must not be an IP literal (RFC 6066); omit it for bare IPs.\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const servername = opts.servername ?? (isIp ? undefined : host);\n const socket = tlsConnect({\n host,\n port,\n servername,\n rejectUnauthorized: false, // we inspect posture; we do not assert trust\n minVersion: \"TLSv1.2\",\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n socket.on(\"secureConnect\", () => {\n finish(extractNegotiated(socket));\n });\n });\n}\n\nexport interface HybridSupport {\n /** True only when the server SELECTED the hybrid group (proof of support). */\n hybridSelected: boolean;\n selectedGroup?: number;\n isHelloRetryRequest?: boolean;\n negotiatedVersion?: number;\n error?: string;\n}\n\n/**\n * Send a raw ClientHello advertising X25519MLKEM768 and read the group the server\n * selects. A server that supports+prefers the hybrid group answers with a\n * HelloRetryRequest selecting 0x11EC — positive proof of PQC-hybrid support.\n */\nexport function probeHybridSupport(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<HybridSupport> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let buf = Buffer.alloc(0);\n let done = false;\n const finish = (r: HybridSupport) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"connect\", () => {\n socket.write(buildClientHello({ serverName: opts.servername ?? host }));\n });\n socket.on(\"timeout\", () => finish({ hybridSelected: false, error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ hybridSelected: false, error: e.message }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n const sh: ServerHelloInfo | undefined = readServerHello(buf);\n if (!sh) {\n if (buf.length > 64 * 1024) finish({ hybridSelected: false, error: \"no ServerHello\" });\n return;\n }\n finish({\n hybridSelected: sh.selectedGroup === GROUP_X25519MLKEM768,\n selectedGroup: sh.selectedGroup,\n isHelloRetryRequest: sh.isHelloRetryRequest,\n negotiatedVersion: sh.negotiatedVersion,\n });\n });\n });\n}\n"]}
1
+ {"version":3,"file":"tls.js","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TLS endpoint inspection. Two engines:\n * 1. `probeTlsNegotiated` — a normal `node:tls` handshake to read what the server\n * actually negotiates: TLS version, cipher suite, ephemeral key-exchange group\n * (the harvestable part), and the leaf certificate's public-key type/size.\n * 2. `probeHybridSupport` — a raw ClientHello (clienthello.ts) advertising\n * X25519MLKEM768, to detect PQC-HYBRID support that Node's bundled OpenSSL\n * cannot itself negotiate.\n *\n * `engine disposes`: we read the negotiated reality and disconnect; we never\n * modify the endpoint.\n */\nimport { connect as tlsConnect, type TLSSocket } from \"node:tls\";\nimport { connect as netConnect } from \"node:net\";\nimport {\n buildClientHello,\n readServerHello,\n GROUP_X25519MLKEM768,\n type ServerHelloInfo,\n} from \"./clienthello.js\";\nimport { certSignatureAlgorithm } from \"./x509.js\";\n\nexport interface TlsNegotiated {\n protocol?: string;\n cipher?: string;\n /** Ephemeral key-exchange group, e.g. \"X25519\", \"P-256\", \"DH\". */\n kexGroup?: string;\n kexType?: string;\n /** Leaf certificate public-key summary. */\n certKeyType?: string;\n certKeyBits?: number;\n certSubject?: string;\n /** How the leaf certificate is SIGNED (the CA's algorithm) — e.g. \"RSA\", \"ECDSA\". */\n certSigFamily?: string;\n certSigOid?: string;\n error?: string;\n}\n\n/** Read the negotiated parameters + leaf-cert posture from a connected TLS socket. */\nexport function extractNegotiated(socket: TLSSocket): TlsNegotiated {\n const cipher = socket.getCipher();\n const kex = socket.getEphemeralKeyInfo();\n const cert = socket.getPeerCertificate(false);\n const cn = cert?.subject?.CN;\n // node:tls exposes the public-key type but not how the leaf was SIGNED; read the\n // signature algorithm (the forgeable-at-Q-day part) from the raw DER.\n const sig = cert?.raw ? certSignatureAlgorithm(cert.raw) : undefined;\n return {\n protocol: socket.getProtocol() ?? undefined,\n cipher: cipher?.standardName ?? cipher?.name,\n kexGroup: kex && \"name\" in kex ? kex.name : undefined,\n kexType: kex && \"type\" in kex ? kex.type : undefined,\n certKeyType: cert?.asn1Curve ? `EC(${cert.asn1Curve})` : cert?.pubkey ? \"RSA\" : undefined,\n certKeyBits: typeof cert?.bits === \"number\" ? cert.bits : undefined,\n certSubject: Array.isArray(cn) ? cn[0] : cn,\n certSigFamily: sig?.family,\n certSigOid: sig?.oid,\n };\n}\n\n/** Perform a normal TLS handshake and read the negotiated parameters. */\nexport function probeTlsNegotiated(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<TlsNegotiated> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let done = false;\n const finish = (r: TlsNegotiated) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n // SNI must not be an IP literal (RFC 6066); omit it for bare IPs.\n const isIp = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) || host.includes(\":\");\n const servername = opts.servername ?? (isIp ? undefined : host);\n const socket = tlsConnect({\n host,\n port,\n servername,\n rejectUnauthorized: false, // we inspect posture; we do not assert trust\n minVersion: \"TLSv1.2\",\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => finish({ error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ error: e.message }));\n socket.on(\"secureConnect\", () => {\n finish(extractNegotiated(socket));\n });\n });\n}\n\nexport interface HybridSupport {\n /** True only when the server SELECTED the hybrid group (proof of support). */\n hybridSelected: boolean;\n selectedGroup?: number;\n isHelloRetryRequest?: boolean;\n negotiatedVersion?: number;\n error?: string;\n}\n\n/**\n * Send a raw ClientHello advertising X25519MLKEM768 and read the group the server\n * selects. A server that supports+prefers the hybrid group answers with a\n * HelloRetryRequest selecting 0x11EC — positive proof of PQC-hybrid support.\n */\nexport function probeHybridSupport(\n host: string,\n port: number,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<HybridSupport> {\n const timeoutMs = opts.timeoutMs ?? 8000;\n return new Promise((resolve) => {\n let buf = Buffer.alloc(0);\n let done = false;\n const finish = (r: HybridSupport) => {\n if (done) return;\n done = true;\n socket.destroy();\n resolve(r);\n };\n const socket = netConnect({ host, port });\n socket.setTimeout(timeoutMs);\n socket.on(\"connect\", () => {\n socket.write(buildClientHello({ serverName: opts.servername ?? host }));\n });\n socket.on(\"timeout\", () => finish({ hybridSelected: false, error: \"timeout\" }));\n socket.on(\"error\", (e) => finish({ hybridSelected: false, error: e.message }));\n // Clean close before a ServerHello would otherwise hang the Promise (the timeout\n // does not fire post-close). Guarded by `done` so a normal finish wins.\n socket.on(\"close\", () => finish({ hybridSelected: false, error: \"connection closed\" }));\n socket.on(\"data\", (chunk: Buffer) => {\n buf = Buffer.concat([buf, chunk]);\n let sh: ServerHelloInfo | undefined;\n try {\n sh = readServerHello(buf);\n } catch {\n // A hostile or broken endpoint can send a length-consistent but internally\n // truncated ServerHello, which trips the parser's bounds check. readServerHello\n // only parses a handshake body that is already fully present, so a throw means\n // the message is malformed, not merely incomplete — finish gracefully instead\n // of letting the RangeError escape this data handler as an uncaught exception\n // (which would crash the process). Mirrors the guarded parse in ssh.ts.\n finish({ hybridSelected: false, error: \"malformed ServerHello\" });\n return;\n }\n if (!sh) {\n if (buf.length > 64 * 1024) finish({ hybridSelected: false, error: \"no ServerHello\" });\n return;\n }\n finish({\n hybridSelected: sh.selectedGroup === GROUP_X25519MLKEM768,\n selectedGroup: sh.selectedGroup,\n isHelloRetryRequest: sh.isHelloRetryRequest,\n negotiatedVersion: sh.negotiatedVersion,\n });\n });\n });\n}\n"]}
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** qProbe version, surfaced in JSON output. Keep in sync with package.json. */
2
- export declare const VERSION = "0.1.0";
2
+ export declare const VERSION = "0.5.0";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** qProbe version, surfaced in JSON output. Keep in sync with package.json. */
2
- export const VERSION = "0.1.0";
2
+ export const VERSION = "0.5.0";
3
3
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC","sourcesContent":["/** qProbe version, surfaced in JSON output. Keep in sync with package.json. */\nexport const VERSION = \"0.1.0\";\n"]}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC","sourcesContent":["/** qProbe version, surfaced in JSON output. Keep in sync with package.json. */\nexport const VERSION = \"0.5.0\";\n"]}
package/dist/x509.d.ts CHANGED
@@ -16,7 +16,7 @@ import type { AlgorithmFamily } from "@quantakrypto/core";
16
16
  export declare function decodeOid(bytes: Buffer): string;
17
17
  /** Map a signature-algorithm OID to a classical family (or undefined). */
18
18
  export declare function oidToSignatureFamily(oid: string): AlgorithmFamily | undefined;
19
- export interface CertSignature {
19
+ interface CertSignature {
20
20
  oid: string;
21
21
  family?: AlgorithmFamily;
22
22
  }
@@ -25,4 +25,5 @@ export interface CertSignature {
25
25
  * Returns undefined if the DER can't be parsed.
26
26
  */
27
27
  export declare function certSignatureAlgorithm(der: Buffer): CertSignature | undefined;
28
+ export {};
28
29
  //# sourceMappingURL=x509.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"x509.d.ts","sourceRoot":"","sources":["../src/x509.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA4B1D,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAa/C;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAM7E;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAc7E"}
1
+ {"version":3,"file":"x509.d.ts","sourceRoot":"","sources":["../src/x509.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA4B1D,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAa/C;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAM7E;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAc7E"}