@quantakrypto/qprobe 0.4.4
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.
- package/README.md +109 -0
- package/THREAT-MODEL.md +83 -0
- package/dist/args.d.ts +19 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +94 -0
- package/dist/args.js.map +1 -0
- package/dist/attest.d.ts +31 -0
- package/dist/attest.d.ts.map +1 -0
- package/dist/attest.js +39 -0
- package/dist/attest.js.map +1 -0
- package/dist/classify.d.ts +19 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +105 -0
- package/dist/classify.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +118 -0
- package/dist/cli.js.map +1 -0
- package/dist/clienthello.d.ts +40 -0
- package/dist/clienthello.d.ts.map +1 -0
- package/dist/clienthello.js +182 -0
- package/dist/clienthello.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +51 -0
- package/dist/report.js.map +1 -0
- package/dist/smtp.d.ts +15 -0
- package/dist/smtp.d.ts.map +1 -0
- package/dist/smtp.js +103 -0
- package/dist/smtp.js.map +1 -0
- package/dist/ssh.d.ts +28 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +118 -0
- package/dist/ssh.js.map +1 -0
- package/dist/target.d.ts +25 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +81 -0
- package/dist/target.js.map +1 -0
- package/dist/tls.d.ts +53 -0
- package/dist/tls.d.ts.map +1 -0
- package/dist/tls.js +109 -0
- package/dist/tls.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/x509.d.ts +28 -0
- package/dist/x509.d.ts.map +1 -0
- package/dist/x509.js +73 -0
- package/dist/x509.js.map +1 -0
- package/package.json +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* qprobe CLI — a thin shell over the programmatic API: parse argv, authorize,
|
|
4
|
+
* probe, print, and set an exit code. It never throws to the top level.
|
|
5
|
+
* Exit codes mirror qScan: 0 OK, 1 findings, 2 error/usage.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { parseArgs, HELP } from "./args.js";
|
|
9
|
+
import { parseTarget, TargetError } from "./target.js";
|
|
10
|
+
import { parseOwnedHosts, AttestationError } from "./attest.js";
|
|
11
|
+
import { runProbe } from "./index.js";
|
|
12
|
+
import { toJsonReport, toSarifReport, toCbomReport } from "./report.js";
|
|
13
|
+
import { VERSION } from "./version.js";
|
|
14
|
+
const EXIT = { OK: 0, FINDINGS: 1, ERROR: 2 };
|
|
15
|
+
function endpointLine(r) {
|
|
16
|
+
const t = `${r.target.host}:${r.target.port}`;
|
|
17
|
+
const lines = [`\n${t} [${r.mode}]`];
|
|
18
|
+
if ((r.mode === "tls" || r.mode === "smtp") && r.tls) {
|
|
19
|
+
if (r.tls.error)
|
|
20
|
+
lines.push(` ${r.mode}: ${r.tls.error}`);
|
|
21
|
+
else
|
|
22
|
+
lines.push(` ${r.tls.protocol ?? "?"} · ${r.tls.cipher ?? "?"} · KEX ${r.tls.kexGroup ?? "?"} · cert ${r.tls.certKeyType ?? "?"}${r.tls.certKeyBits ? `-${r.tls.certKeyBits}` : ""}${r.tls.certSigFamily ? ` (sig ${r.tls.certSigFamily})` : ""}`);
|
|
23
|
+
if (r.hybrid && !r.hybrid.error)
|
|
24
|
+
lines.push(` PQC-hybrid (X25519MLKEM768): ${r.hybrid.hybridSelected ? "SUPPORTED ✓" : "not negotiated"}`);
|
|
25
|
+
}
|
|
26
|
+
if (r.mode === "ssh" && r.ssh) {
|
|
27
|
+
if (r.ssh.error)
|
|
28
|
+
lines.push(` ssh: ${r.ssh.error}`);
|
|
29
|
+
else
|
|
30
|
+
lines.push(` ${r.ssh.banner ?? ""} · PQC KEX: ${r.ssh.pqKexOffered ? "offered ✓" : "none"}`);
|
|
31
|
+
}
|
|
32
|
+
for (const p of r.positives)
|
|
33
|
+
lines.push(` ✓ ${p}`);
|
|
34
|
+
for (const f of r.findings)
|
|
35
|
+
lines.push(` [${f.severity}] ${f.title} — ${f.message}`);
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function formatHuman(result) {
|
|
39
|
+
const out = [`qProbe — live post-quantum readiness · qprobe v${VERSION}`];
|
|
40
|
+
for (const r of result.reports)
|
|
41
|
+
out.push(endpointLine(r));
|
|
42
|
+
const f = result.findings;
|
|
43
|
+
out.push(`\n${f.length} finding${f.length === 1 ? "" : "s"} · ${result.inventory.hndlCount} HNDL-exposed · readiness ${result.inventory.readinessScore}/100`);
|
|
44
|
+
return out.join("\n") + "\n";
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const args = parseArgs(process.argv.slice(2));
|
|
48
|
+
if (args.help) {
|
|
49
|
+
console.log(HELP);
|
|
50
|
+
return EXIT.OK;
|
|
51
|
+
}
|
|
52
|
+
if (args.targets.length === 0) {
|
|
53
|
+
console.error(HELP);
|
|
54
|
+
return EXIT.ERROR;
|
|
55
|
+
}
|
|
56
|
+
const defaultPort = args.mode === "ssh" ? 22 : args.mode === "smtp" ? 587 : 443;
|
|
57
|
+
let targets;
|
|
58
|
+
try {
|
|
59
|
+
targets = args.targets.map((t) => parseTarget(t, defaultPort));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
if (e instanceof TargetError) {
|
|
63
|
+
console.error(`qprobe: ${e.message}`);
|
|
64
|
+
return EXIT.ERROR;
|
|
65
|
+
}
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
let ownedHosts;
|
|
69
|
+
if (args.ownedHostsFile) {
|
|
70
|
+
try {
|
|
71
|
+
ownedHosts = parseOwnedHosts(readFileSync(args.ownedHostsFile, "utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
console.error(`qprobe: cannot read ownership manifest ${args.ownedHostsFile}`);
|
|
75
|
+
return EXIT.ERROR;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const startedAt = new Date().toISOString();
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await runProbe({
|
|
82
|
+
targets,
|
|
83
|
+
mode: args.mode,
|
|
84
|
+
attest: { iOwnThis: args.iOwnThis, ownedHosts },
|
|
85
|
+
servername: args.servername,
|
|
86
|
+
timeoutMs: args.timeoutMs,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (e instanceof AttestationError) {
|
|
91
|
+
console.error(`qprobe: ${e.message}`);
|
|
92
|
+
return EXIT.ERROR;
|
|
93
|
+
}
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
const finishedAt = new Date().toISOString();
|
|
97
|
+
switch (args.format) {
|
|
98
|
+
case "json":
|
|
99
|
+
console.log(JSON.stringify(toJsonReport(result, startedAt, finishedAt), null, 2));
|
|
100
|
+
break;
|
|
101
|
+
case "sarif":
|
|
102
|
+
console.log(JSON.stringify(toSarifReport(result, startedAt, finishedAt), null, 2));
|
|
103
|
+
break;
|
|
104
|
+
case "cbom":
|
|
105
|
+
console.log(JSON.stringify(toCbomReport(result, startedAt, finishedAt), null, 2));
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
process.stdout.write(formatHuman(result));
|
|
109
|
+
}
|
|
110
|
+
return result.findings.length > 0 ? EXIT.FINDINGS : EXIT.OK;
|
|
111
|
+
}
|
|
112
|
+
main()
|
|
113
|
+
.then((code) => process.exit(code))
|
|
114
|
+
.catch((e) => {
|
|
115
|
+
console.error(`qprobe: ${e instanceof Error ? e.message : String(e)}`);
|
|
116
|
+
process.exit(EXIT.ERROR);
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * qprobe CLI — a thin shell over the programmatic API: parse argv, authorize,\n * probe, print, and set an exit code. It never throws to the top level.\n * Exit codes mirror qScan: 0 OK, 1 findings, 2 error/usage.\n */\nimport { readFileSync } from \"node:fs\";\nimport { parseArgs, HELP } from \"./args.js\";\nimport { parseTarget, TargetError } from \"./target.js\";\nimport { parseOwnedHosts, AttestationError } from \"./attest.js\";\nimport { runProbe, type RunResult, type EndpointReport } from \"./index.js\";\nimport { toJsonReport, toSarifReport, toCbomReport } from \"./report.js\";\nimport { VERSION } from \"./version.js\";\n\nconst EXIT = { OK: 0, FINDINGS: 1, ERROR: 2 } as const;\n\nfunction endpointLine(r: EndpointReport): string {\n const t = `${r.target.host}:${r.target.port}`;\n const lines: string[] = [`\\n${t} [${r.mode}]`];\n if ((r.mode === \"tls\" || r.mode === \"smtp\") && r.tls) {\n if (r.tls.error) lines.push(` ${r.mode}: ${r.tls.error}`);\n else\n lines.push(\n ` ${r.tls.protocol ?? \"?\"} · ${r.tls.cipher ?? \"?\"} · KEX ${r.tls.kexGroup ?? \"?\"} · cert ${\n r.tls.certKeyType ?? \"?\"\n }${r.tls.certKeyBits ? `-${r.tls.certKeyBits}` : \"\"}${\n r.tls.certSigFamily ? ` (sig ${r.tls.certSigFamily})` : \"\"\n }`,\n );\n if (r.hybrid && !r.hybrid.error)\n lines.push(\n ` PQC-hybrid (X25519MLKEM768): ${r.hybrid.hybridSelected ? \"SUPPORTED ✓\" : \"not negotiated\"}`,\n );\n }\n if (r.mode === \"ssh\" && r.ssh) {\n if (r.ssh.error) lines.push(` ssh: ${r.ssh.error}`);\n else\n lines.push(` ${r.ssh.banner ?? \"\"} · PQC KEX: ${r.ssh.pqKexOffered ? \"offered ✓\" : \"none\"}`);\n }\n for (const p of r.positives) lines.push(` ✓ ${p}`);\n for (const f of r.findings) lines.push(` [${f.severity}] ${f.title} — ${f.message}`);\n return lines.join(\"\\n\");\n}\n\nfunction formatHuman(result: RunResult): string {\n const out: string[] = [`qProbe — live post-quantum readiness · qprobe v${VERSION}`];\n for (const r of result.reports) out.push(endpointLine(r));\n const f = result.findings;\n out.push(\n `\\n${f.length} finding${f.length === 1 ? \"\" : \"s\"} · ${result.inventory.hndlCount} HNDL-exposed · readiness ${\n result.inventory.readinessScore\n }/100`,\n );\n return out.join(\"\\n\") + \"\\n\";\n}\n\nasync function main(): Promise<number> {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) {\n console.log(HELP);\n return EXIT.OK;\n }\n if (args.targets.length === 0) {\n console.error(HELP);\n return EXIT.ERROR;\n }\n\n const defaultPort = args.mode === \"ssh\" ? 22 : args.mode === \"smtp\" ? 587 : 443;\n let targets;\n try {\n targets = args.targets.map((t) => parseTarget(t, defaultPort));\n } catch (e) {\n if (e instanceof TargetError) {\n console.error(`qprobe: ${e.message}`);\n return EXIT.ERROR;\n }\n throw e;\n }\n\n let ownedHosts: string[] | undefined;\n if (args.ownedHostsFile) {\n try {\n ownedHosts = parseOwnedHosts(readFileSync(args.ownedHostsFile, \"utf8\"));\n } catch {\n console.error(`qprobe: cannot read ownership manifest ${args.ownedHostsFile}`);\n return EXIT.ERROR;\n }\n }\n\n const startedAt = new Date().toISOString();\n let result: RunResult;\n try {\n result = await runProbe({\n targets,\n mode: args.mode,\n attest: { iOwnThis: args.iOwnThis, ownedHosts },\n servername: args.servername,\n timeoutMs: args.timeoutMs,\n });\n } catch (e) {\n if (e instanceof AttestationError) {\n console.error(`qprobe: ${e.message}`);\n return EXIT.ERROR;\n }\n throw e;\n }\n const finishedAt = new Date().toISOString();\n\n switch (args.format) {\n case \"json\":\n console.log(JSON.stringify(toJsonReport(result, startedAt, finishedAt), null, 2));\n break;\n case \"sarif\":\n console.log(JSON.stringify(toSarifReport(result, startedAt, finishedAt), null, 2));\n break;\n case \"cbom\":\n console.log(JSON.stringify(toCbomReport(result, startedAt, finishedAt), null, 2));\n break;\n default:\n process.stdout.write(formatHuman(result));\n }\n\n return result.findings.length > 0 ? EXIT.FINDINGS : EXIT.OK;\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((e: unknown) => {\n console.error(`qprobe: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(EXIT.ERROR);\n });\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const GROUP_X25519 = 29;
|
|
2
|
+
export declare const GROUP_X25519MLKEM768 = 4588;
|
|
3
|
+
export declare const GROUP_SECP256R1 = 23;
|
|
4
|
+
/**
|
|
5
|
+
* Build a raw ClientHello TLS record advertising the hybrid group. `keyShareGroup`
|
|
6
|
+
* is the group we actually send a key_share for (default x25519), while
|
|
7
|
+
* `supportedGroups` is what we advertise (hybrid first).
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildClientHello(opts: {
|
|
10
|
+
serverName?: string;
|
|
11
|
+
supportedGroups?: number[];
|
|
12
|
+
keyShareGroup?: number;
|
|
13
|
+
}): Buffer;
|
|
14
|
+
/** A parsed TLS record. */
|
|
15
|
+
export interface TlsRecord {
|
|
16
|
+
type: number;
|
|
17
|
+
fragment: Buffer;
|
|
18
|
+
}
|
|
19
|
+
/** Split a buffer into TLS records. Stops at the first truncated record. */
|
|
20
|
+
export declare function parseRecords(buf: Buffer): TlsRecord[];
|
|
21
|
+
/** The result of reading a ServerHello / HelloRetryRequest. */
|
|
22
|
+
export interface ServerHelloInfo {
|
|
23
|
+
isHelloRetryRequest: boolean;
|
|
24
|
+
/** Group named in the key_share extension (selected group), if present. */
|
|
25
|
+
selectedGroup?: number;
|
|
26
|
+
/** Negotiated version from supported_versions (0x0304 for TLS 1.3), if present. */
|
|
27
|
+
negotiatedVersion?: number;
|
|
28
|
+
cipherSuite?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse a ServerHello handshake message body (the bytes AFTER the 4-byte handshake
|
|
32
|
+
* header) and extract the selected group + negotiated version. Handles both a
|
|
33
|
+
* normal ServerHello (key_share carries group + key) and a HelloRetryRequest
|
|
34
|
+
* (key_share carries just the selected group) — in both the first 2 bytes of the
|
|
35
|
+
* key_share body are the group.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseServerHelloBody(body: Buffer): ServerHelloInfo;
|
|
38
|
+
/** Read the first ServerHello/HRR out of a raw response buffer, if any. */
|
|
39
|
+
export declare function readServerHello(raw: Buffer): ServerHelloInfo | undefined;
|
|
40
|
+
//# sourceMappingURL=clienthello.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clienthello.d.ts","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,YAAY,KAAS,CAAC;AACnC,eAAO,MAAM,oBAAoB,OAAS,CAAC;AAC3C,eAAO,MAAM,eAAe,KAAS,CAAC;AAuCtC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,MAAM,CAkDT;AAED,2BAA2B;AAC3B,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CAWrD;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAyClE;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAWxE"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest
|
|
3
|
+
* parser — just enough to detect whether a server supports the post-quantum hybrid
|
|
4
|
+
* key-exchange group X25519MLKEM768 (codepoint 0x11EC, RFC 9370 / draft-kwiatkowski
|
|
5
|
+
* -tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot
|
|
6
|
+
* be detected through `node:tls`; we advertise it in a raw ClientHello and read the
|
|
7
|
+
* group the server selects.
|
|
8
|
+
*
|
|
9
|
+
* Detection trick (no ML-KEM keygen needed): we send `supported_groups =
|
|
10
|
+
* [X25519MLKEM768, x25519]` but a `key_share` ONLY for x25519. A server that
|
|
11
|
+
* supports and prefers the hybrid group answers with a HelloRetryRequest selecting
|
|
12
|
+
* 0x11EC (asking us to resend with that share) — which is proof of support without
|
|
13
|
+
* us performing any ML-KEM. A server that does not proceeds with our x25519 share.
|
|
14
|
+
*
|
|
15
|
+
* This module is pure byte manipulation over Buffers; the socket I/O lives in
|
|
16
|
+
* tls.ts. Every function here is unit-tested with crafted bytes.
|
|
17
|
+
*/
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
// TLS constants.
|
|
20
|
+
export const GROUP_X25519 = 0x001d;
|
|
21
|
+
export const GROUP_X25519MLKEM768 = 0x11ec;
|
|
22
|
+
export const GROUP_SECP256R1 = 0x0017;
|
|
23
|
+
const TLS13_VERSION = 0x0304;
|
|
24
|
+
const HS_CLIENT_HELLO = 0x01;
|
|
25
|
+
const HS_SERVER_HELLO = 0x02;
|
|
26
|
+
const REC_HANDSHAKE = 0x16;
|
|
27
|
+
const EXT_SERVER_NAME = 0x0000;
|
|
28
|
+
const EXT_SUPPORTED_GROUPS = 0x000a;
|
|
29
|
+
const EXT_SIGNATURE_ALGORITHMS = 0x000d;
|
|
30
|
+
const EXT_SUPPORTED_VERSIONS = 0x002b;
|
|
31
|
+
const EXT_KEY_SHARE = 0x0033;
|
|
32
|
+
/** The special ServerHello.random that marks a HelloRetryRequest (RFC 8446 §4.1.3). */
|
|
33
|
+
const HRR_RANDOM = Buffer.from("cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c", "hex");
|
|
34
|
+
function u16(n) {
|
|
35
|
+
const b = Buffer.alloc(2);
|
|
36
|
+
b.writeUInt16BE(n, 0);
|
|
37
|
+
return b;
|
|
38
|
+
}
|
|
39
|
+
/** Prefix `body` with a length field of `bytes` width (1, 2, or 3 bytes). */
|
|
40
|
+
function withLen(bytes, body) {
|
|
41
|
+
const len = Buffer.alloc(bytes);
|
|
42
|
+
if (bytes === 1)
|
|
43
|
+
len.writeUInt8(body.length, 0);
|
|
44
|
+
else if (bytes === 2)
|
|
45
|
+
len.writeUInt16BE(body.length, 0);
|
|
46
|
+
else
|
|
47
|
+
len.writeUIntBE(body.length, 0, 3);
|
|
48
|
+
return Buffer.concat([len, body]);
|
|
49
|
+
}
|
|
50
|
+
/** Build a single extension (type + 2-byte-length body). */
|
|
51
|
+
function ext(type, body) {
|
|
52
|
+
return Buffer.concat([u16(type), withLen(2, body)]);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a raw ClientHello TLS record advertising the hybrid group. `keyShareGroup`
|
|
56
|
+
* is the group we actually send a key_share for (default x25519), while
|
|
57
|
+
* `supportedGroups` is what we advertise (hybrid first).
|
|
58
|
+
*/
|
|
59
|
+
export function buildClientHello(opts) {
|
|
60
|
+
const supportedGroups = opts.supportedGroups ?? [
|
|
61
|
+
GROUP_X25519MLKEM768,
|
|
62
|
+
GROUP_X25519,
|
|
63
|
+
GROUP_SECP256R1,
|
|
64
|
+
];
|
|
65
|
+
const keyShareGroup = opts.keyShareGroup ?? GROUP_X25519;
|
|
66
|
+
const extensions = [];
|
|
67
|
+
if (opts.serverName &&
|
|
68
|
+
opts.serverName.length > 0 &&
|
|
69
|
+
!/^\d+\.\d+\.\d+\.\d+$/.test(opts.serverName)) {
|
|
70
|
+
// server_name: ServerNameList { name_type(0=host_name), HostName }
|
|
71
|
+
const name = Buffer.from(opts.serverName, "ascii");
|
|
72
|
+
const entry = Buffer.concat([Buffer.from([0x00]), withLen(2, name)]);
|
|
73
|
+
extensions.push(ext(EXT_SERVER_NAME, withLen(2, entry)));
|
|
74
|
+
}
|
|
75
|
+
// supported_versions: TLS 1.3 only.
|
|
76
|
+
extensions.push(ext(EXT_SUPPORTED_VERSIONS, withLen(1, u16(TLS13_VERSION))));
|
|
77
|
+
// supported_groups (named_group list).
|
|
78
|
+
extensions.push(ext(EXT_SUPPORTED_GROUPS, withLen(2, Buffer.concat(supportedGroups.map(u16)))));
|
|
79
|
+
// signature_algorithms (a standard modern set).
|
|
80
|
+
const sigAlgs = [0x0403, 0x0804, 0x0807, 0x0401, 0x0805, 0x0806];
|
|
81
|
+
extensions.push(ext(EXT_SIGNATURE_ALGORITHMS, withLen(2, Buffer.concat(sigAlgs.map(u16)))));
|
|
82
|
+
// key_share: one entry for keyShareGroup with a 32-byte X25519-sized public.
|
|
83
|
+
// (We never complete the handshake, so the value need not be a real key.)
|
|
84
|
+
const share = Buffer.concat([u16(keyShareGroup), withLen(2, randomBytes(32))]);
|
|
85
|
+
extensions.push(ext(EXT_KEY_SHARE, withLen(2, share)));
|
|
86
|
+
const cipherSuites = Buffer.concat([u16(0x1301), u16(0x1302), u16(0x1303)]);
|
|
87
|
+
const helloBody = Buffer.concat([
|
|
88
|
+
u16(0x0303), // legacy_version TLS 1.2
|
|
89
|
+
randomBytes(32), // random
|
|
90
|
+
withLen(1, randomBytes(32)), // legacy_session_id (non-empty → "middlebox compat")
|
|
91
|
+
withLen(2, cipherSuites), // cipher_suites
|
|
92
|
+
withLen(1, Buffer.from([0x00])), // compression_methods: null
|
|
93
|
+
withLen(2, Buffer.concat(extensions)), // extensions
|
|
94
|
+
]);
|
|
95
|
+
const handshake = Buffer.concat([Buffer.from([HS_CLIENT_HELLO]), withLen(3, helloBody)]);
|
|
96
|
+
// TLS record: handshake, legacy record version 0x0301.
|
|
97
|
+
return Buffer.concat([Buffer.from([REC_HANDSHAKE]), u16(0x0301), withLen(2, handshake)]);
|
|
98
|
+
}
|
|
99
|
+
/** Split a buffer into TLS records. Stops at the first truncated record. */
|
|
100
|
+
export function parseRecords(buf) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let off = 0;
|
|
103
|
+
while (off + 5 <= buf.length) {
|
|
104
|
+
const type = buf[off];
|
|
105
|
+
const len = buf.readUInt16BE(off + 3);
|
|
106
|
+
if (off + 5 + len > buf.length)
|
|
107
|
+
break;
|
|
108
|
+
out.push({ type, fragment: buf.subarray(off + 5, off + 5 + len) });
|
|
109
|
+
off += 5 + len;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a ServerHello handshake message body (the bytes AFTER the 4-byte handshake
|
|
115
|
+
* header) and extract the selected group + negotiated version. Handles both a
|
|
116
|
+
* normal ServerHello (key_share carries group + key) and a HelloRetryRequest
|
|
117
|
+
* (key_share carries just the selected group) — in both the first 2 bytes of the
|
|
118
|
+
* key_share body are the group.
|
|
119
|
+
*/
|
|
120
|
+
export function parseServerHelloBody(body) {
|
|
121
|
+
let off = 0;
|
|
122
|
+
const need = (n) => {
|
|
123
|
+
if (off + n > body.length)
|
|
124
|
+
throw new RangeError("truncated ServerHello");
|
|
125
|
+
};
|
|
126
|
+
need(2);
|
|
127
|
+
off += 2; // legacy_version
|
|
128
|
+
need(32);
|
|
129
|
+
const random = body.subarray(off, off + 32);
|
|
130
|
+
off += 32;
|
|
131
|
+
const isHRR = random.equals(HRR_RANDOM);
|
|
132
|
+
need(1);
|
|
133
|
+
const sidLen = body[off];
|
|
134
|
+
off += 1;
|
|
135
|
+
need(sidLen);
|
|
136
|
+
off += sidLen; // legacy_session_id_echo
|
|
137
|
+
need(2);
|
|
138
|
+
const cipherSuite = body.readUInt16BE(off);
|
|
139
|
+
off += 2;
|
|
140
|
+
need(1);
|
|
141
|
+
off += 1; // legacy_compression_method
|
|
142
|
+
const info = { isHelloRetryRequest: isHRR, cipherSuite };
|
|
143
|
+
if (off + 2 > body.length)
|
|
144
|
+
return info; // no extensions
|
|
145
|
+
const extTotal = body.readUInt16BE(off);
|
|
146
|
+
off += 2;
|
|
147
|
+
const extEnd = Math.min(off + extTotal, body.length);
|
|
148
|
+
while (off + 4 <= extEnd) {
|
|
149
|
+
const extType = body.readUInt16BE(off);
|
|
150
|
+
const extLen = body.readUInt16BE(off + 2);
|
|
151
|
+
const start = off + 4;
|
|
152
|
+
if (start + extLen > body.length)
|
|
153
|
+
break;
|
|
154
|
+
const extBody = body.subarray(start, start + extLen);
|
|
155
|
+
if (extType === EXT_KEY_SHARE && extBody.length >= 2) {
|
|
156
|
+
info.selectedGroup = extBody.readUInt16BE(0);
|
|
157
|
+
}
|
|
158
|
+
else if (extType === EXT_SUPPORTED_VERSIONS && extBody.length >= 2) {
|
|
159
|
+
info.negotiatedVersion = extBody.readUInt16BE(0);
|
|
160
|
+
}
|
|
161
|
+
off = start + extLen;
|
|
162
|
+
}
|
|
163
|
+
return info;
|
|
164
|
+
}
|
|
165
|
+
/** Read the first ServerHello/HRR out of a raw response buffer, if any. */
|
|
166
|
+
export function readServerHello(raw) {
|
|
167
|
+
for (const rec of parseRecords(raw)) {
|
|
168
|
+
if (rec.type !== REC_HANDSHAKE)
|
|
169
|
+
continue;
|
|
170
|
+
const f = rec.fragment;
|
|
171
|
+
if (f.length < 4)
|
|
172
|
+
continue;
|
|
173
|
+
if (f[0] !== HS_SERVER_HELLO)
|
|
174
|
+
continue;
|
|
175
|
+
const len = f.readUIntBE(1, 3);
|
|
176
|
+
if (4 + len > f.length)
|
|
177
|
+
continue;
|
|
178
|
+
return parseServerHelloBody(f.subarray(4, 4 + len));
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=clienthello.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clienthello.js","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,iBAAiB;AACjB,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,aAAa,GAAG,MAAM,CAAC;AAE7B,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,MAAM,eAAe,GAAG,MAAM,CAAC;AAC/B,MAAM,oBAAoB,GAAG,MAAM,CAAC;AACpC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AACtC,MAAM,aAAa,GAAG,MAAM,CAAC;AAE7B,uFAAuF;AACvF,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAC5B,kEAAkE,EAClE,KAAK,CACN,CAAC;AAEF,SAAS,GAAG,CAAC,CAAS;IACpB,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,6EAA6E;AAC7E,SAAS,OAAO,CAAC,KAAgB,EAAE,IAAY;IAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,CAAC;QAAE,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;SAC3C,IAAI,KAAK,KAAK,CAAC;QAAE,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;;QACnD,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,4DAA4D;AAC5D,SAAS,GAAG,CAAC,IAAY,EAAE,IAAY;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAIhC;IACC,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,IAAI;QAC9C,oBAAoB;QACpB,YAAY;QACZ,eAAe;KAChB,CAAC;IACF,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,YAAY,CAAC;IAEzD,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,IACE,IAAI,CAAC,UAAU;QACf,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QAC1B,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAC7C,CAAC;QACD,mEAAmE;QACnE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACrE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,oCAAoC;IACpC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,uCAAuC;IACvC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhG,gDAAgD;IAChD,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACjE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,wBAAwB,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5F,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAEvD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAE5E,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;QAC9B,GAAG,CAAC,MAAM,CAAC,EAAE,yBAAyB;QACtC,WAAW,CAAC,EAAE,CAAC,EAAE,SAAS;QAC1B,OAAO,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,qDAAqD;QAClF,OAAO,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,gBAAgB;QAC1C,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,4BAA4B;QAC7D,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,aAAa;KACrD,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACzF,uDAAuD;IACvD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;AAC3F,CAAC;AAQD,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACtC,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM;YAAE,MAAM;QACtC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC;QACnE,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAYD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE;QACzB,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,UAAU,CAAC,uBAAuB,CAAC,CAAC;IAC3E,CAAC,CAAC;IACF,IAAI,CAAC,CAAC,CAAC,CAAC;IACR,GAAG,IAAI,CAAC,CAAC,CAAC,iBAAiB;IAC3B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAC5C,GAAG,IAAI,EAAE,CAAC;IACV,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,CAAC,CAAC;IACR,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,GAAG,IAAI,CAAC,CAAC;IACT,IAAI,CAAC,MAAM,CAAC,CAAC;IACb,GAAG,IAAI,MAAM,CAAC,CAAC,yBAAyB;IACxC,IAAI,CAAC,CAAC,CAAC,CAAC;IACR,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3C,GAAG,IAAI,CAAC,CAAC;IACT,IAAI,CAAC,CAAC,CAAC,CAAC;IACR,GAAG,IAAI,CAAC,CAAC,CAAC,4BAA4B;IAEtC,MAAM,IAAI,GAAoB,EAAE,mBAAmB,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC1E,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,gBAAgB;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,GAAG,IAAI,CAAC,CAAC;IACT,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACrD,OAAO,GAAG,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC,MAAM;YAAE,MAAM;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC;QACrD,IAAI,OAAO,KAAK,aAAa,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;aAAM,IAAI,OAAO,KAAK,sBAAsB,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACrE,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;QACD,GAAG,GAAG,KAAK,GAAG,MAAM,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,KAAK,MAAM,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;QACpC,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa;YAAE,SAAS;QACzC,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC;QACvB,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAC3B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,eAAe;YAAE,SAAS;QACvC,MAAM,GAAG,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,MAAM;YAAE,SAAS;QACjC,OAAO,oBAAoB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","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 * Detection trick (no ML-KEM keygen needed): we send `supported_groups =\n * [X25519MLKEM768, x25519]` but a `key_share` ONLY for x25519. A server that\n * supports and prefers the hybrid group answers with a HelloRetryRequest selecting\n * 0x11EC (asking us to resend with that share) — which is proof of support without\n * us performing any ML-KEM. A server that does not proceeds with our x25519 share.\n *\n * This module is pure byte manipulation over Buffers; the socket I/O lives in\n * tls.ts. Every function here is unit-tested with crafted bytes.\n */\nimport { randomBytes } from \"node:crypto\";\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: one entry for keyShareGroup with a 32-byte X25519-sized public.\n // (We never complete the handshake, so the value need not be a real key.)\n const share = Buffer.concat([u16(keyShareGroup), withLen(2, randomBytes(32))]);\n extensions.push(ext(EXT_KEY_SHARE, withLen(2, share)));\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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @quantakrypto/qprobe — active post-quantum readiness probing of live TLS/SSH
|
|
3
|
+
* endpoints you OWN. The programmatic API. See THREAT-MODEL.md for the
|
|
4
|
+
* authorization model; {@link runProbe} refuses to touch the network until
|
|
5
|
+
* {@link authorizeTargets} passes.
|
|
6
|
+
*
|
|
7
|
+
* Findings are `@quantakrypto/core` Findings, so they compose with qScan output
|
|
8
|
+
* and score through the same `buildInventory`.
|
|
9
|
+
*/
|
|
10
|
+
import type { CryptoInventory, Finding } from "@quantakrypto/core";
|
|
11
|
+
import type { Target } from "./target.js";
|
|
12
|
+
import { type AttestationInput } from "./attest.js";
|
|
13
|
+
import { type TlsNegotiated, type HybridSupport } from "./tls.js";
|
|
14
|
+
import { type SshProbeResult } from "./ssh.js";
|
|
15
|
+
export type { Target } from "./target.js";
|
|
16
|
+
export { parseTarget, TargetError } from "./target.js";
|
|
17
|
+
export { authorizeTargets, parseOwnedHosts, AttestationError, type AttestationInput, } from "./attest.js";
|
|
18
|
+
export type { TlsNegotiated, HybridSupport } from "./tls.js";
|
|
19
|
+
export type { SshProbeResult, KexInit } from "./ssh.js";
|
|
20
|
+
export { PQ_SSH_KEX } from "./ssh.js";
|
|
21
|
+
export * from "./clienthello.js";
|
|
22
|
+
export { certSignatureAlgorithm, decodeOid, oidToSignatureFamily } from "./x509.js";
|
|
23
|
+
export { smtpAdvertisesStartTls, smtpReplyComplete } from "./smtp.js";
|
|
24
|
+
export { classifyTls, classifySsh } from "./classify.js";
|
|
25
|
+
export { toScanResult, toSarifReport, toCbomReport, toJsonReport } from "./report.js";
|
|
26
|
+
export type ProbeMode = "tls" | "ssh" | "smtp";
|
|
27
|
+
export interface EndpointReport {
|
|
28
|
+
target: Target;
|
|
29
|
+
mode: ProbeMode;
|
|
30
|
+
tls?: TlsNegotiated;
|
|
31
|
+
hybrid?: HybridSupport;
|
|
32
|
+
ssh?: SshProbeResult;
|
|
33
|
+
/** Human-readable positive signals (good news), e.g. hybrid TLS selected. */
|
|
34
|
+
positives: string[];
|
|
35
|
+
findings: Finding[];
|
|
36
|
+
}
|
|
37
|
+
/** Choose a probe mode for "auto": SSH on 22, SMTP STARTTLS on 25/587, TLS otherwise. */
|
|
38
|
+
export declare function resolveMode(target: Target, mode: ProbeMode | "auto"): ProbeMode;
|
|
39
|
+
export interface RunOptions {
|
|
40
|
+
targets: Target[];
|
|
41
|
+
mode: ProbeMode | "auto";
|
|
42
|
+
attest: AttestationInput;
|
|
43
|
+
servername?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
/** Minimum spacing between endpoint connections (politeness / rate-limit). */
|
|
46
|
+
minIntervalMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface RunResult {
|
|
49
|
+
reports: EndpointReport[];
|
|
50
|
+
findings: Finding[];
|
|
51
|
+
inventory: CryptoInventory;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Authorize (throws {@link AttestationError} / {@link TargetError} on failure — no
|
|
55
|
+
* network I/O happens before a successful authorization) then probe every target
|
|
56
|
+
* sequentially with a minimum interval between connections.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runProbe(opts: RunOptions): Promise<RunResult>;
|
|
59
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAIzD,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAMrB,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEtF,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAK/E;AAkCD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,eAAe,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAenE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildInventory } from "@quantakrypto/core";
|
|
2
|
+
import { authorizeTargets } from "./attest.js";
|
|
3
|
+
import { probeTlsNegotiated, probeHybridSupport, } from "./tls.js";
|
|
4
|
+
import { probeSsh } from "./ssh.js";
|
|
5
|
+
import { probeSmtpStartTls } from "./smtp.js";
|
|
6
|
+
import { classifyTls, classifySsh } from "./classify.js";
|
|
7
|
+
export { parseTarget, TargetError } from "./target.js";
|
|
8
|
+
export { authorizeTargets, parseOwnedHosts, AttestationError, } from "./attest.js";
|
|
9
|
+
export { PQ_SSH_KEX } from "./ssh.js";
|
|
10
|
+
export * from "./clienthello.js"; // pure byte codec — no network
|
|
11
|
+
export { certSignatureAlgorithm, decodeOid, oidToSignatureFamily } from "./x509.js"; // pure DER parse
|
|
12
|
+
export { smtpAdvertisesStartTls, smtpReplyComplete } from "./smtp.js"; // pure SMTP reply framing
|
|
13
|
+
export { classifyTls, classifySsh } from "./classify.js";
|
|
14
|
+
export { toScanResult, toSarifReport, toCbomReport, toJsonReport } from "./report.js";
|
|
15
|
+
/** Choose a probe mode for "auto": SSH on 22, SMTP STARTTLS on 25/587, TLS otherwise. */
|
|
16
|
+
export function resolveMode(target, mode) {
|
|
17
|
+
if (mode !== "auto")
|
|
18
|
+
return mode;
|
|
19
|
+
if (target.port === 22)
|
|
20
|
+
return "ssh";
|
|
21
|
+
if (target.port === 25 || target.port === 587)
|
|
22
|
+
return "smtp";
|
|
23
|
+
return "tls";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Probe a single endpoint. INTERNAL — not exported, because it performs network
|
|
27
|
+
* I/O without checking authorization; only {@link runProbe} (which authorizes
|
|
28
|
+
* first) may reach it. Assumes authorization has already been granted.
|
|
29
|
+
*/
|
|
30
|
+
async function probeEndpoint(target, mode, opts = {}) {
|
|
31
|
+
const positives = [];
|
|
32
|
+
if (mode === "ssh") {
|
|
33
|
+
const ssh = await probeSsh(target.host, target.port, opts.timeoutMs);
|
|
34
|
+
if (ssh.pqKexOffered)
|
|
35
|
+
positives.push("PQC SSH key exchange offered");
|
|
36
|
+
return { target, mode, ssh, positives, findings: classifySsh(target, ssh) };
|
|
37
|
+
}
|
|
38
|
+
if (mode === "smtp") {
|
|
39
|
+
// STARTTLS upgrade, then the same negotiated inspection (no hybrid probe over
|
|
40
|
+
// STARTTLS): classify with an "inconclusive" hybrid result so we don't claim to
|
|
41
|
+
// have tested for a hybrid group we never advertised.
|
|
42
|
+
const tls = await probeSmtpStartTls(target.host, target.port, opts);
|
|
43
|
+
const hybrid = { hybridSelected: false, error: "not probed (SMTP)" };
|
|
44
|
+
return { target, mode, tls, hybrid, positives, findings: classifyTls(target, tls, hybrid) };
|
|
45
|
+
}
|
|
46
|
+
const [tls, hybrid] = await Promise.all([
|
|
47
|
+
probeTlsNegotiated(target.host, target.port, opts),
|
|
48
|
+
probeHybridSupport(target.host, target.port, opts),
|
|
49
|
+
]);
|
|
50
|
+
if (hybrid.hybridSelected)
|
|
51
|
+
positives.push("PQC-hybrid TLS (X25519MLKEM768) selected");
|
|
52
|
+
return { target, mode, tls, hybrid, positives, findings: classifyTls(target, tls, hybrid) };
|
|
53
|
+
}
|
|
54
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
55
|
+
/**
|
|
56
|
+
* Authorize (throws {@link AttestationError} / {@link TargetError} on failure — no
|
|
57
|
+
* network I/O happens before a successful authorization) then probe every target
|
|
58
|
+
* sequentially with a minimum interval between connections.
|
|
59
|
+
*/
|
|
60
|
+
export async function runProbe(opts) {
|
|
61
|
+
authorizeTargets(opts.targets, opts.attest); // gate — throws before any connection
|
|
62
|
+
const reports = [];
|
|
63
|
+
const interval = opts.minIntervalMs ?? 250;
|
|
64
|
+
for (let i = 0; i < opts.targets.length; i++) {
|
|
65
|
+
if (i > 0 && interval > 0)
|
|
66
|
+
await delay(interval);
|
|
67
|
+
const target = opts.targets[i];
|
|
68
|
+
const mode = resolveMode(target, opts.mode);
|
|
69
|
+
reports.push(await probeEndpoint(target, mode, { servername: opts.servername, timeoutMs: opts.timeoutMs }));
|
|
70
|
+
}
|
|
71
|
+
const findings = reports.flatMap((r) => r.findings);
|
|
72
|
+
return { reports, findings, inventory: buildInventory(findings) };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAyB,MAAM,aAAa,CAAC;AACtE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,GAGnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,QAAQ,EAAuB,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GAEjB,MAAM,aAAa,CAAC;AAQrB,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,cAAc,kBAAkB,CAAC,CAAC,+BAA+B;AACjE,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC,CAAC,iBAAiB;AACtG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC,CAAC,0BAA0B;AACjG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAetF,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,IAAwB;IAClE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,MAAM,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,EAAE,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG;QAAE,OAAO,MAAM,CAAC;IAC7D,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,aAAa,CAC1B,MAAc,EACd,IAAe,EACf,OAAoD,EAAE;IAEtD,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACrE,IAAI,GAAG,CAAC,YAAY;YAAE,SAAS,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACrE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;IAC9E,CAAC;IACD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,8EAA8E;QAC9E,gFAAgF;QAChF,sDAAsD;QACtD,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpE,MAAM,MAAM,GAAkB,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;QACpF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;IAC9F,CAAC;IACD,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtC,kBAAkB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC;QAClD,kBAAkB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC;KACnD,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,cAAc;QAAE,SAAS,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IACtF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;AAC9F,CAAC;AAkBD,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEnF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAgB;IAC7C,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,sCAAsC;IAEnF,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,IAAI,GAAG,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,GAAG,CAAC;YAAE,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CACV,MAAM,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAC9F,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * @quantakrypto/qprobe — active post-quantum readiness probing of live TLS/SSH\n * endpoints you OWN. The programmatic API. See THREAT-MODEL.md for the\n * authorization model; {@link runProbe} refuses to touch the network until\n * {@link authorizeTargets} passes.\n *\n * Findings are `@quantakrypto/core` Findings, so they compose with qScan output\n * and score through the same `buildInventory`.\n */\nimport type { CryptoInventory, Finding } from \"@quantakrypto/core\";\nimport { buildInventory } from \"@quantakrypto/core\";\nimport type { Target } from \"./target.js\";\nimport { authorizeTargets, type AttestationInput } from \"./attest.js\";\nimport {\n probeTlsNegotiated,\n probeHybridSupport,\n type TlsNegotiated,\n type HybridSupport,\n} from \"./tls.js\";\nimport { probeSsh, type SshProbeResult } from \"./ssh.js\";\nimport { probeSmtpStartTls } from \"./smtp.js\";\nimport { classifyTls, classifySsh } from \"./classify.js\";\n\nexport type { Target } from \"./target.js\";\nexport { parseTarget, TargetError } from \"./target.js\";\nexport {\n authorizeTargets,\n parseOwnedHosts,\n AttestationError,\n type AttestationInput,\n} from \"./attest.js\";\n// Only TYPES from the network modules are public — the socket-opening functions\n// (probeTlsNegotiated / probeHybridSupport / probeSsh) are intentionally NOT\n// re-exported, so `runProbe` (which authorizes first) is the sole public entry\n// that performs network I/O. This keeps the THREAT-MODEL invariant true at the\n// package API boundary, not just in the CLI.\nexport type { TlsNegotiated, HybridSupport } from \"./tls.js\";\nexport type { SshProbeResult, KexInit } from \"./ssh.js\";\nexport { PQ_SSH_KEX } from \"./ssh.js\";\nexport * from \"./clienthello.js\"; // pure byte codec — no network\nexport { certSignatureAlgorithm, decodeOid, oidToSignatureFamily } from \"./x509.js\"; // pure DER parse\nexport { smtpAdvertisesStartTls, smtpReplyComplete } from \"./smtp.js\"; // pure SMTP reply framing\nexport { classifyTls, classifySsh } from \"./classify.js\";\nexport { toScanResult, toSarifReport, toCbomReport, toJsonReport } from \"./report.js\";\n\nexport type ProbeMode = \"tls\" | \"ssh\" | \"smtp\";\n\nexport interface EndpointReport {\n target: Target;\n mode: ProbeMode;\n tls?: TlsNegotiated;\n hybrid?: HybridSupport;\n ssh?: SshProbeResult;\n /** Human-readable positive signals (good news), e.g. hybrid TLS selected. */\n positives: string[];\n findings: Finding[];\n}\n\n/** Choose a probe mode for \"auto\": SSH on 22, SMTP STARTTLS on 25/587, TLS otherwise. */\nexport function resolveMode(target: Target, mode: ProbeMode | \"auto\"): ProbeMode {\n if (mode !== \"auto\") return mode;\n if (target.port === 22) return \"ssh\";\n if (target.port === 25 || target.port === 587) return \"smtp\";\n return \"tls\";\n}\n\n/**\n * Probe a single endpoint. INTERNAL — not exported, because it performs network\n * I/O without checking authorization; only {@link runProbe} (which authorizes\n * first) may reach it. Assumes authorization has already been granted.\n */\nasync function probeEndpoint(\n target: Target,\n mode: ProbeMode,\n opts: { servername?: string; timeoutMs?: number } = {},\n): Promise<EndpointReport> {\n const positives: string[] = [];\n if (mode === \"ssh\") {\n const ssh = await probeSsh(target.host, target.port, opts.timeoutMs);\n if (ssh.pqKexOffered) positives.push(\"PQC SSH key exchange offered\");\n return { target, mode, ssh, positives, findings: classifySsh(target, ssh) };\n }\n if (mode === \"smtp\") {\n // STARTTLS upgrade, then the same negotiated inspection (no hybrid probe over\n // STARTTLS): classify with an \"inconclusive\" hybrid result so we don't claim to\n // have tested for a hybrid group we never advertised.\n const tls = await probeSmtpStartTls(target.host, target.port, opts);\n const hybrid: HybridSupport = { hybridSelected: false, error: \"not probed (SMTP)\" };\n return { target, mode, tls, hybrid, positives, findings: classifyTls(target, tls, hybrid) };\n }\n const [tls, hybrid] = await Promise.all([\n probeTlsNegotiated(target.host, target.port, opts),\n probeHybridSupport(target.host, target.port, opts),\n ]);\n if (hybrid.hybridSelected) positives.push(\"PQC-hybrid TLS (X25519MLKEM768) selected\");\n return { target, mode, tls, hybrid, positives, findings: classifyTls(target, tls, hybrid) };\n}\n\nexport interface RunOptions {\n targets: Target[];\n mode: ProbeMode | \"auto\";\n attest: AttestationInput;\n servername?: string;\n timeoutMs?: number;\n /** Minimum spacing between endpoint connections (politeness / rate-limit). */\n minIntervalMs?: number;\n}\n\nexport interface RunResult {\n reports: EndpointReport[];\n findings: Finding[];\n inventory: CryptoInventory;\n}\n\nconst delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Authorize (throws {@link AttestationError} / {@link TargetError} on failure — no\n * network I/O happens before a successful authorization) then probe every target\n * sequentially with a minimum interval between connections.\n */\nexport async function runProbe(opts: RunOptions): Promise<RunResult> {\n authorizeTargets(opts.targets, opts.attest); // gate — throws before any connection\n\n const reports: EndpointReport[] = [];\n const interval = opts.minIntervalMs ?? 250;\n for (let i = 0; i < opts.targets.length; i++) {\n if (i > 0 && interval > 0) await delay(interval);\n const target = opts.targets[i];\n const mode = resolveMode(target, opts.mode);\n reports.push(\n await probeEndpoint(target, mode, { servername: opts.servername, timeoutMs: opts.timeoutMs }),\n );\n }\n const findings = reports.flatMap((r) => r.findings);\n return { reports, findings, inventory: buildInventory(findings) };\n}\n"]}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting: turn a qProbe {@link RunResult} into `@quantakrypto/core`'s shared
|
|
3
|
+
* output formats, so live-endpoint findings emit the SAME SARIF 2.1.0 and
|
|
4
|
+
* CycloneDX 1.6 CBOM as qScan (code) and the infra detectors (config). This is
|
|
5
|
+
* what lets the three planes compose into one post-quantum posture: a qScan CBOM
|
|
6
|
+
* and a qProbe CBOM are both CycloneDX 1.6 documents and merge cleanly.
|
|
7
|
+
*
|
|
8
|
+
* The core reporters are findings-based (they derive SARIF `rules[]` and CBOM
|
|
9
|
+
* crypto-assets from the findings themselves), so no registry entry is needed for
|
|
10
|
+
* the `qprobe-*` rules.
|
|
11
|
+
*/
|
|
12
|
+
import type { ScanResult, SarifLog, CycloneDxBom } from "@quantakrypto/core";
|
|
13
|
+
import type { RunResult } from "./index.js";
|
|
14
|
+
/**
|
|
15
|
+
* Adapt a {@link RunResult} to a {@link ScanResult}. `filesScanned` is the number
|
|
16
|
+
* of endpoints probed; `root` is the probed target list.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toScanResult(run: RunResult, startedAt: string, finishedAt: string): ScanResult;
|
|
19
|
+
export declare function toSarifReport(run: RunResult, startedAt: string, finishedAt: string): SarifLog;
|
|
20
|
+
export declare function toCbomReport(run: RunResult, startedAt: string, finishedAt: string): CycloneDxBom;
|
|
21
|
+
export declare function toJsonReport(run: RunResult, startedAt: string, finishedAt: string): Record<string, unknown>;
|
|
22
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAU9F;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,CAE7F;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,CAQhG;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiBzB"}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { toSarif, toJson, toCbom } from "@quantakrypto/core";
|
|
2
|
+
import { VERSION } from "./version.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapt a {@link RunResult} to a {@link ScanResult}. `filesScanned` is the number
|
|
5
|
+
* of endpoints probed; `root` is the probed target list.
|
|
6
|
+
*/
|
|
7
|
+
export function toScanResult(run, startedAt, finishedAt) {
|
|
8
|
+
return {
|
|
9
|
+
root: run.reports.map((r) => `${r.target.host}:${r.target.port}`).join(", ") || "(no targets)",
|
|
10
|
+
findings: run.findings,
|
|
11
|
+
filesScanned: run.reports.length,
|
|
12
|
+
inventory: run.inventory,
|
|
13
|
+
startedAt,
|
|
14
|
+
finishedAt,
|
|
15
|
+
toolVersion: `qprobe ${VERSION}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function toSarifReport(run, startedAt, finishedAt) {
|
|
19
|
+
return toSarif(toScanResult(run, startedAt, finishedAt));
|
|
20
|
+
}
|
|
21
|
+
export function toCbomReport(run, startedAt, finishedAt) {
|
|
22
|
+
const bom = toCbom(toScanResult(run, startedAt, finishedAt));
|
|
23
|
+
// core's toCbom labels the producing tool "qScan"; correct it for qProbe so a
|
|
24
|
+
// merged code+endpoints CBOM attributes each plane to the right tool.
|
|
25
|
+
const tools = bom.metadata.tools
|
|
26
|
+
?.components;
|
|
27
|
+
if (Array.isArray(tools))
|
|
28
|
+
for (const t of tools)
|
|
29
|
+
if (t.name === "qScan")
|
|
30
|
+
t.name = "qProbe";
|
|
31
|
+
return bom;
|
|
32
|
+
}
|
|
33
|
+
export function toJsonReport(run, startedAt, finishedAt) {
|
|
34
|
+
// Core's structured JSON (findings + inventory), augmented with the per-endpoint
|
|
35
|
+
// probe detail qProbe uniquely carries (negotiated params, hybrid result).
|
|
36
|
+
const base = toJson(toScanResult(run, startedAt, finishedAt));
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
endpoints: run.reports.map((r) => ({
|
|
40
|
+
target: `${r.target.host}:${r.target.port}`,
|
|
41
|
+
mode: r.mode,
|
|
42
|
+
positives: r.positives,
|
|
43
|
+
tls: r.tls,
|
|
44
|
+
hybrid: r.hybrid,
|
|
45
|
+
ssh: r.ssh
|
|
46
|
+
? { banner: r.ssh.banner, pqKexOffered: r.ssh.pqKexOffered, error: r.ssh.error }
|
|
47
|
+
: undefined,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=report.js.map
|