@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.
- package/README.md +1 -1
- package/dist/args.d.ts +5 -4
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +14 -2
- package/dist/args.js.map +1 -1
- package/dist/attest.d.ts.map +1 -1
- package/dist/attest.js +21 -2
- package/dist/attest.js.map +1 -1
- package/dist/cli.js +9 -2
- package/dist/cli.js.map +1 -1
- package/dist/clienthello.d.ts +2 -0
- package/dist/clienthello.d.ts.map +1 -1
- package/dist/clienthello.js +36 -12
- package/dist/clienthello.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -7
- package/dist/index.js.map +1 -1
- package/dist/mlkem768.d.ts +15 -0
- package/dist/mlkem768.d.ts.map +1 -0
- package/dist/mlkem768.js +71 -0
- package/dist/mlkem768.js.map +1 -0
- package/dist/postgres.d.ts +9 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +79 -0
- package/dist/postgres.js.map +1 -0
- package/dist/smtp.d.ts.map +1 -1
- package/dist/smtp.js +5 -1
- package/dist/smtp.js.map +1 -1
- package/dist/ssh.d.ts.map +1 -1
- package/dist/ssh.js +4 -0
- package/dist/ssh.js.map +1 -1
- package/dist/starttls.d.ts +24 -0
- package/dist/starttls.d.ts.map +1 -0
- package/dist/starttls.js +98 -0
- package/dist/starttls.js.map +1 -0
- package/dist/tls.d.ts.map +1 -1
- package/dist/tls.js +17 -1
- package/dist/tls.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/x509.d.ts +2 -1
- package/dist/x509.d.ts.map +1 -1
- package/dist/x509.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ npx @quantakrypto/qprobe --owned-hosts hosts.txt api.example.com:8443 --json
|
|
|
50
50
|
|---|---|
|
|
51
51
|
| `--i-own-this` | Attest you are authorized to probe the target(s). Required (or `--owned-hosts`). |
|
|
52
52
|
| `--owned-hosts <file>` | Ownership manifest (one host per line, `#` comments). Every target must be listed. |
|
|
53
|
-
| `--tls` / `--ssh` | Force a probe mode
|
|
53
|
+
| `--tls` / `--ssh` / `--smtp` / `--imap` / `--pop3` / `--postgres` | Force a probe mode. Default: auto by well-known port — SSH `:22`, SMTP STARTTLS `:25`/`:587`, IMAP STARTTLS `:143`, POP3 STLS `:110`, PostgreSQL SSLRequest `:5432`, else direct TLS (which covers HTTPS `:443`, IMAPS `:993`, and DNS-over-TLS `:853`). |
|
|
54
54
|
| `--servername <name>` | TLS SNI server name (default: the host; omitted for bare IPs). |
|
|
55
55
|
| `--timeout <ms>` | Per-connection timeout (default: 8000). |
|
|
56
56
|
| `--format <human\|json\|sarif\|cbom>` | Output format (default: human). `--json` / `--sarif` / `--cbom` are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM are the **same formats qScan emits**. |
|
package/dist/args.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Hand-rolled CLI argument parser (zero dependencies), mirroring qScan's approach:
|
|
3
3
|
* the parser is pure and total — it never throws and never does I/O.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type ProbeModeArg = "tls" | "ssh" | "smtp" | "imap" | "pop3" | "postgres" | "auto";
|
|
6
|
+
type FormatArg = "human" | "json" | "sarif" | "cbom";
|
|
7
|
+
interface CliArgs {
|
|
8
8
|
targets: string[];
|
|
9
9
|
mode: ProbeModeArg;
|
|
10
10
|
iOwnThis: boolean;
|
|
@@ -15,5 +15,6 @@ export interface CliArgs {
|
|
|
15
15
|
help: boolean;
|
|
16
16
|
}
|
|
17
17
|
export declare function parseArgs(argv: readonly string[]): CliArgs;
|
|
18
|
-
export declare const HELP = "qprobe \u2014 active post-quantum readiness probing of live TLS/SSH endpoints you OWN.\n\nUSAGE\n qprobe [options] <host[:port]> [host[:port] ...]\n\nAUTHORIZATION (required \u2014 one of)\n --i-own-this Attest you are authorized to probe the given endpoint(s).\n --owned-hosts <file> Ownership manifest (one host per line, # comments).\n\n Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR\n blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.\n\nOPTIONS\n --tls | --ssh | --smtp
|
|
18
|
+
export declare const HELP = "qprobe \u2014 active post-quantum readiness probing of live TLS/SSH endpoints you OWN.\n\nUSAGE\n qprobe [options] <host[:port]> [host[:port] ...]\n\nAUTHORIZATION (required \u2014 one of)\n --i-own-this Attest you are authorized to probe the given endpoint(s).\n --owned-hosts <file> Ownership manifest (one host per line, # comments).\n\n Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR\n blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.\n\nOPTIONS\n --tls | --ssh | --smtp | --imap | --pop3 | --postgres\n Force a probe mode (default: auto by well-known port \u2014\n SSH :22, SMTP :25/:587, IMAP :143, POP3 :110,\n PostgreSQL :5432, else direct TLS \u2014 which covers HTTPS\n :443, IMAPS :993, DoT :853).\n --servername <name> TLS SNI server name (default: the host).\n --timeout <ms> Per-connection timeout (default: 8000).\n --format <human|json|sarif|cbom>\n Output format (default: human). --json / --sarif / --cbom\n are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM compose\n with qScan output (same formats).\n -h, --help Show this help.\n\nEXAMPLES\n qprobe --i-own-this example.com # TLS 443: hybrid KEX + cert posture\n qprobe --ssh --i-own-this git.example.com # SSH 22: KEXINIT algorithms\n qprobe --owned-hosts hosts.txt api.example.com:8443 --json\n\nqprobe reports the negotiated reality; it never modifies an endpoint (\"engine disposes\").";
|
|
19
|
+
export {};
|
|
19
20
|
//# sourceMappingURL=args.d.ts.map
|
package/dist/args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;AACnF,KAAK,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAErD,UAAU,OAAO;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAkE1D;AAED,eAAO,MAAM,IAAI,ioDA+ByE,CAAC"}
|
package/dist/args.js
CHANGED
|
@@ -30,6 +30,15 @@ export function parseArgs(argv) {
|
|
|
30
30
|
case "--smtp":
|
|
31
31
|
args.mode = "smtp";
|
|
32
32
|
break;
|
|
33
|
+
case "--imap":
|
|
34
|
+
args.mode = "imap";
|
|
35
|
+
break;
|
|
36
|
+
case "--pop3":
|
|
37
|
+
args.mode = "pop3";
|
|
38
|
+
break;
|
|
39
|
+
case "--postgres":
|
|
40
|
+
args.mode = "postgres";
|
|
41
|
+
break;
|
|
33
42
|
case "--owned-hosts":
|
|
34
43
|
args.ownedHostsFile = argv[++i];
|
|
35
44
|
break;
|
|
@@ -75,8 +84,11 @@ AUTHORIZATION (required — one of)
|
|
|
75
84
|
blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.
|
|
76
85
|
|
|
77
86
|
OPTIONS
|
|
78
|
-
--tls | --ssh | --smtp
|
|
79
|
-
|
|
87
|
+
--tls | --ssh | --smtp | --imap | --pop3 | --postgres
|
|
88
|
+
Force a probe mode (default: auto by well-known port —
|
|
89
|
+
SSH :22, SMTP :25/:587, IMAP :143, POP3 :110,
|
|
90
|
+
PostgreSQL :5432, else direct TLS — which covers HTTPS
|
|
91
|
+
:443, IMAPS :993, DoT :853).
|
|
80
92
|
--servername <name> TLS SNI server name (default: the host).
|
|
81
93
|
--timeout <ms> Per-connection timeout (default: 8000).
|
|
82
94
|
--format <human|json|sarif|cbom>
|
package/dist/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,MAAM,UAAU,SAAS,CAAC,IAAuB;IAC/C,MAAM,IAAI,GAAY;QACpB,OAAO,EAAE,EAAE;QACX,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,KAAK;KACZ,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,QAAQ,CAAC,EAAE,CAAC;YACV,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACrB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;gBACnB,MAAM;YACR,KAAK,eAAe;gBAClB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC5B,MAAM;YACR,KAAK,WAAW;gBACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;gBACrD,MAAM;YACR,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO;oBAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpF,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR,KAAK,SAAS;gBACZ,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;gBACtB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR;gBACE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM;QACV,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,MAAM,UAAU,SAAS,CAAC,IAAuB;IAC/C,MAAM,IAAI,GAAY;QACpB,OAAO,EAAE,EAAE;QACX,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,KAAK;KACZ,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,QAAQ,CAAC,EAAE,CAAC;YACV,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACrB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;gBACnB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;gBACnB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;gBACnB,MAAM;YACR,KAAK,YAAY;gBACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;gBACvB,MAAM;YACR,KAAK,eAAe;gBAClB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC5B,MAAM;YACR,KAAK,WAAW;gBACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;gBACrD,MAAM;YACR,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO;oBAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpF,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR,KAAK,SAAS;gBACZ,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;gBACtB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR;gBACE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM;QACV,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0FA+BsE,CAAC","sourcesContent":["/**\n * Hand-rolled CLI argument parser (zero dependencies), mirroring qScan's approach:\n * the parser is pure and total — it never throws and never does I/O.\n */\n\ntype ProbeModeArg = \"tls\" | \"ssh\" | \"smtp\" | \"imap\" | \"pop3\" | \"postgres\" | \"auto\";\ntype FormatArg = \"human\" | \"json\" | \"sarif\" | \"cbom\";\n\ninterface CliArgs {\n targets: string[];\n mode: ProbeModeArg;\n iOwnThis: boolean;\n ownedHostsFile?: string;\n servername?: string;\n timeoutMs: number;\n format: FormatArg;\n help: boolean;\n}\n\nexport function parseArgs(argv: readonly string[]): CliArgs {\n const args: CliArgs = {\n targets: [],\n mode: \"auto\",\n iOwnThis: false,\n timeoutMs: 8000,\n format: \"human\",\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n switch (a) {\n case \"-h\":\n case \"--help\":\n args.help = true;\n break;\n case \"--i-own-this\":\n args.iOwnThis = true;\n break;\n case \"--tls\":\n args.mode = \"tls\";\n break;\n case \"--ssh\":\n args.mode = \"ssh\";\n break;\n case \"--smtp\":\n args.mode = \"smtp\";\n break;\n case \"--imap\":\n args.mode = \"imap\";\n break;\n case \"--pop3\":\n args.mode = \"pop3\";\n break;\n case \"--postgres\":\n args.mode = \"postgres\";\n break;\n case \"--owned-hosts\":\n args.ownedHostsFile = argv[++i];\n break;\n case \"--servername\":\n args.servername = argv[++i];\n break;\n case \"--timeout\":\n args.timeoutMs = Number(argv[++i]) || args.timeoutMs;\n break;\n case \"--format\": {\n const v = argv[++i];\n if (v === \"json\" || v === \"sarif\" || v === \"cbom\" || v === \"human\") args.format = v;\n break;\n }\n case \"--json\":\n args.format = \"json\";\n break;\n case \"--sarif\":\n args.format = \"sarif\";\n break;\n case \"--cbom\":\n args.format = \"cbom\";\n break;\n default:\n if (a && !a.startsWith(\"-\")) args.targets.push(a);\n break;\n }\n }\n return args;\n}\n\nexport const HELP = `qprobe — active post-quantum readiness probing of live TLS/SSH endpoints you OWN.\n\nUSAGE\n qprobe [options] <host[:port]> [host[:port] ...]\n\nAUTHORIZATION (required — one of)\n --i-own-this Attest you are authorized to probe the given endpoint(s).\n --owned-hosts <file> Ownership manifest (one host per line, # comments).\n\n Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR\n blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.\n\nOPTIONS\n --tls | --ssh | --smtp | --imap | --pop3 | --postgres\n Force a probe mode (default: auto by well-known port —\n SSH :22, SMTP :25/:587, IMAP :143, POP3 :110,\n PostgreSQL :5432, else direct TLS — which covers HTTPS\n :443, IMAPS :993, DoT :853).\n --servername <name> TLS SNI server name (default: the host).\n --timeout <ms> Per-connection timeout (default: 8000).\n --format <human|json|sarif|cbom>\n Output format (default: human). --json / --sarif / --cbom\n are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM compose\n with qScan output (same formats).\n -h, --help Show this help.\n\nEXAMPLES\n qprobe --i-own-this example.com # TLS 443: hybrid KEX + cert posture\n qprobe --ssh --i-own-this git.example.com # SSH 22: KEXINIT algorithms\n qprobe --owned-hosts hosts.txt api.example.com:8443 --json\n\nqprobe reports the negotiated reality; it never modifies an endpoint (\"engine disposes\").`;\n"]}
|
package/dist/attest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAoBD,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAUtD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAsB3F"}
|
package/dist/attest.js
CHANGED
|
@@ -4,14 +4,33 @@ export class AttestationError extends Error {
|
|
|
4
4
|
this.name = "AttestationError";
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract the host from a manifest line, matching {@link parseTarget}'s host
|
|
9
|
+
* normalization so a manifest entry and a target reduce to the SAME host string.
|
|
10
|
+
* Handles `host`, `host:port`, bracketed IPv6 `[::1]` / `[::1]:443`, and bare IPv6
|
|
11
|
+
* `2001:db8::1`. (Previously IPv6 lines kept their brackets/port or were truncated
|
|
12
|
+
* at the first colon, so no IPv6 endpoint could ever match the manifest.)
|
|
13
|
+
*/
|
|
14
|
+
function manifestHost(line) {
|
|
15
|
+
if (line.startsWith("[")) {
|
|
16
|
+
const end = line.indexOf("]");
|
|
17
|
+
return end < 0 ? line : line.slice(1, end); // [ipv6] or [ipv6]:port -> ipv6
|
|
18
|
+
}
|
|
19
|
+
const idx = line.lastIndexOf(":");
|
|
20
|
+
// Exactly one ':' -> host:port. Multiple ':' with no brackets -> bare IPv6 (no port).
|
|
21
|
+
if (idx >= 0 && line.indexOf(":") === idx)
|
|
22
|
+
return line.slice(0, idx);
|
|
23
|
+
return line;
|
|
24
|
+
}
|
|
7
25
|
/** Parse an ownership manifest file's text into a host allow-list. */
|
|
8
26
|
export function parseOwnedHosts(text) {
|
|
9
27
|
return (text
|
|
10
28
|
.split("\n")
|
|
11
29
|
.map((l) => l.trim())
|
|
12
30
|
.filter((l) => l !== "" && !l.startsWith("#"))
|
|
13
|
-
// A manifest line may be `host` or `host:port
|
|
14
|
-
|
|
31
|
+
// A manifest line may be `host` or `host:port` (IPv6 bracketed or bare);
|
|
32
|
+
// ownership is per host, normalized the same way targets are.
|
|
33
|
+
.map(manifestHost));
|
|
15
34
|
}
|
|
16
35
|
/**
|
|
17
36
|
* Authorize a set of targets or throw {@link AttestationError}. Returns silently
|
package/dist/attest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attest.js","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AASD,
|
|
1
|
+
{"version":3,"file":"attest.js","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AASD;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,gCAAgC;IAC9E,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,sFAAsF;IACtF,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,CACL,IAAI;SACD,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC9C,yEAAyE;QACzE,8DAA8D;SAC7D,GAAG,CAAC,YAAY,CAAC,CACrB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA0B,EAAE,MAAwB;IACnF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,yBAAyB,CAAC,CAAC;IAEhF,IAAI,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,gBAAgB,CACxB,mBAAmB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC;gBACxF,uFAAuF,CAC1F,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO;IAE5B,MAAM,IAAI,gBAAgB,CACxB,yFAAyF;QACvF,2FAA2F;QAC3F,sBAAsB,CACzB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Ownership attestation — the first-class authorization gate for active probing.\n *\n * qProbe MUST NOT connect to an endpoint until the operator has attested they are\n * authorized to test it. Attestation comes from exactly one of:\n * - `--i-own-this` : an explicit per-run attestation for the target(s).\n * - `--owned-hosts <file>` : an ownership manifest (one host per line, `#`\n * comments allowed); every target host must appear.\n *\n * With neither, {@link authorizeTargets} throws and NOTHING connects. This is a\n * code-enforced control, not a prompt: see THREAT-MODEL.md §Authorization.\n */\nimport type { Target } from \"./target.js\";\n\nexport class AttestationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AttestationError\";\n }\n}\n\nexport interface AttestationInput {\n /** `--i-own-this` was passed. */\n iOwnThis: boolean;\n /** Parsed contents of an ownership manifest, if `--owned-hosts` was given. */\n ownedHosts?: readonly string[];\n}\n\n/**\n * Extract the host from a manifest line, matching {@link parseTarget}'s host\n * normalization so a manifest entry and a target reduce to the SAME host string.\n * Handles `host`, `host:port`, bracketed IPv6 `[::1]` / `[::1]:443`, and bare IPv6\n * `2001:db8::1`. (Previously IPv6 lines kept their brackets/port or were truncated\n * at the first colon, so no IPv6 endpoint could ever match the manifest.)\n */\nfunction manifestHost(line: string): string {\n if (line.startsWith(\"[\")) {\n const end = line.indexOf(\"]\");\n return end < 0 ? line : line.slice(1, end); // [ipv6] or [ipv6]:port -> ipv6\n }\n const idx = line.lastIndexOf(\":\");\n // Exactly one ':' -> host:port. Multiple ':' with no brackets -> bare IPv6 (no port).\n if (idx >= 0 && line.indexOf(\":\") === idx) return line.slice(0, idx);\n return line;\n}\n\n/** Parse an ownership manifest file's text into a host allow-list. */\nexport function parseOwnedHosts(text: string): string[] {\n return (\n text\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l !== \"\" && !l.startsWith(\"#\"))\n // A manifest line may be `host` or `host:port` (IPv6 bracketed or bare);\n // ownership is per host, normalized the same way targets are.\n .map(manifestHost)\n );\n}\n\n/**\n * Authorize a set of targets or throw {@link AttestationError}. Returns silently\n * when every target is covered by the attestation. No network I/O happens here or\n * anywhere upstream of a successful return.\n */\nexport function authorizeTargets(targets: readonly Target[], attest: AttestationInput): void {\n if (targets.length === 0) throw new AttestationError(\"no targets to authorize\");\n\n if (attest.ownedHosts && attest.ownedHosts.length > 0) {\n const owned = new Set(attest.ownedHosts.map((h) => h.toLowerCase()));\n const missing = targets.filter((t) => !owned.has(t.host.toLowerCase()));\n if (missing.length > 0) {\n throw new AttestationError(\n `not authorized: ${missing.map((t) => t.host).join(\", \")} not in the ownership manifest. ` +\n `Add the host(s) to the manifest, or pass --i-own-this only for endpoints you control.`,\n );\n }\n return;\n }\n\n if (attest.iOwnThis) return;\n\n throw new AttestationError(\n \"refusing to probe: no ownership attestation. Pass --i-own-this (endpoints you control) \" +\n \"or --owned-hosts <manifest>. Active probing of endpoints you do not own may be unlawful; \" +\n \"see THREAT-MODEL.md.\",\n );\n}\n"]}
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ const EXIT = { OK: 0, FINDINGS: 1, ERROR: 2 };
|
|
|
15
15
|
function endpointLine(r) {
|
|
16
16
|
const t = `${r.target.host}:${r.target.port}`;
|
|
17
17
|
const lines = [`\n${t} [${r.mode}]`];
|
|
18
|
-
if (
|
|
18
|
+
if (r.mode !== "ssh" && r.tls) {
|
|
19
19
|
if (r.tls.error)
|
|
20
20
|
lines.push(` ${r.mode}: ${r.tls.error}`);
|
|
21
21
|
else
|
|
@@ -53,7 +53,14 @@ async function main() {
|
|
|
53
53
|
console.error(HELP);
|
|
54
54
|
return EXIT.ERROR;
|
|
55
55
|
}
|
|
56
|
-
const
|
|
56
|
+
const DEFAULT_PORTS = {
|
|
57
|
+
ssh: 22,
|
|
58
|
+
smtp: 587,
|
|
59
|
+
imap: 143,
|
|
60
|
+
pop3: 110,
|
|
61
|
+
postgres: 5432,
|
|
62
|
+
};
|
|
63
|
+
const defaultPort = DEFAULT_PORTS[args.mode] ?? 443;
|
|
57
64
|
let targets;
|
|
58
65
|
try {
|
|
59
66
|
targets = args.targets.map((t) => parseTarget(t, defaultPort));
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAuC,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAW,CAAC;AAEvD,SAAS,YAAY,CAAC,CAAiB;IACrC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,CAAC,
|
|
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 !== \"ssh\" && 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 DEFAULT_PORTS: Record<string, number> = {\n ssh: 22,\n smtp: 587,\n imap: 143,\n pop3: 110,\n postgres: 5432,\n };\n const defaultPort = DEFAULT_PORTS[args.mode] ?? 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"]}
|
package/dist/clienthello.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clienthello.d.ts","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"clienthello.d.ts","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"AA0BA,8EAA8E;AAC9E,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAGD,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,CA6DT;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"}
|
package/dist/clienthello.js
CHANGED
|
@@ -6,16 +6,29 @@
|
|
|
6
6
|
* be detected through `node:tls`; we advertise it in a raw ClientHello and read the
|
|
7
7
|
* group the server selects.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* We advertise `supported_groups = [X25519MLKEM768, x25519, …]` AND send a
|
|
10
|
+
* WELL-FORMED X25519MLKEM768 key_share (`ML-KEM-768.ek (1184) || X25519.pk (32)`,
|
|
11
|
+
* per draft-ietf-tls-ecdhe-mlkem) alongside an x25519 share. A supporting server
|
|
12
|
+
* then selects 0x11EC DIRECTLY in its ServerHello (we read the selected group) —
|
|
13
|
+
* this catches servers that support-but-don't-*prefer* the hybrid group, which the
|
|
14
|
+
* older HelloRetryRequest-only inference missed. The ML-KEM ek is well-formed but
|
|
15
|
+
* throwaway (see mlkem768.ts): qProbe never completes the handshake, so the ML-KEM
|
|
16
|
+
* shared secret is neither computed nor needed. A server that does not support the
|
|
17
|
+
* group falls back to our x25519 share (or answers with a HelloRetryRequest, which
|
|
18
|
+
* the parser still reads).
|
|
14
19
|
*
|
|
15
|
-
* This module is
|
|
16
|
-
*
|
|
20
|
+
* This module is byte manipulation over Buffers (the X25519 key and ML-KEM encoding
|
|
21
|
+
* come from node:crypto / mlkem768.ts); the socket I/O lives in tls.ts. The pure
|
|
22
|
+
* functions are unit-tested with crafted bytes.
|
|
17
23
|
*/
|
|
18
|
-
import { randomBytes } from "node:crypto";
|
|
24
|
+
import { randomBytes, generateKeyPairSync } from "node:crypto";
|
|
25
|
+
import { wellFormedMlKem768Ek } from "./mlkem768.js";
|
|
26
|
+
/** A raw 32-byte X25519 public key (from node:crypto — a real, valid key). */
|
|
27
|
+
export function x25519RawPublic() {
|
|
28
|
+
const { publicKey } = generateKeyPairSync("x25519");
|
|
29
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
30
|
+
return Buffer.from(jwk.x ?? "", "base64url");
|
|
31
|
+
}
|
|
19
32
|
// TLS constants.
|
|
20
33
|
export const GROUP_X25519 = 0x001d;
|
|
21
34
|
export const GROUP_X25519MLKEM768 = 0x11ec;
|
|
@@ -79,10 +92,21 @@ export function buildClientHello(opts) {
|
|
|
79
92
|
// signature_algorithms (a standard modern set).
|
|
80
93
|
const sigAlgs = [0x0403, 0x0804, 0x0807, 0x0401, 0x0805, 0x0806];
|
|
81
94
|
extensions.push(ext(EXT_SIGNATURE_ALGORITHMS, withLen(2, Buffer.concat(sigAlgs.map(u16)))));
|
|
82
|
-
// key_share:
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
// key_share: send a REAL X25519 public plus a WELL-FORMED X25519MLKEM768 hybrid
|
|
96
|
+
// share, so a server that supports the hybrid group can select it DIRECTLY (not
|
|
97
|
+
// only via a HelloRetryRequest). For X25519MLKEM768 the client share is
|
|
98
|
+
// `ML-KEM-768.ek (1184) || X25519.pk (32)` (draft-ietf-tls-ecdhe-mlkem). We never
|
|
99
|
+
// complete the handshake, so the ML-KEM secret is neither computed nor needed.
|
|
100
|
+
void keyShareGroup; // retained for API compat; the share set is now fixed
|
|
101
|
+
const x25519Pub = x25519RawPublic();
|
|
102
|
+
const hybridShareVal = Buffer.concat([wellFormedMlKem768Ek(), x25519Pub]);
|
|
103
|
+
const shareEntries = Buffer.concat([
|
|
104
|
+
u16(GROUP_X25519MLKEM768),
|
|
105
|
+
withLen(2, hybridShareVal),
|
|
106
|
+
u16(GROUP_X25519),
|
|
107
|
+
withLen(2, x25519Pub),
|
|
108
|
+
]);
|
|
109
|
+
extensions.push(ext(EXT_KEY_SHARE, withLen(2, shareEntries)));
|
|
86
110
|
const cipherSuites = Buffer.concat([u16(0x1301), u16(0x1302), u16(0x1303)]);
|
|
87
111
|
const helloBody = Buffer.concat([
|
|
88
112
|
u16(0x0303), // legacy_version TLS 1.2
|
package/dist/clienthello.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clienthello.js","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest\n * parser — just enough to detect whether a server supports the post-quantum hybrid\n * key-exchange group X25519MLKEM768 (codepoint 0x11EC, RFC 9370 / draft-kwiatkowski\n * -tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot\n * be detected through `node:tls`; we advertise it in a raw ClientHello and read the\n * group the server selects.\n *\n * 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"]}
|
|
1
|
+
{"version":3,"file":"clienthello.js","sourceRoot":"","sources":["../src/clienthello.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal, hand-rolled TLS 1.3 ClientHello builder + ServerHello/HelloRetryRequest\n * parser — just enough to detect whether a server supports the post-quantum hybrid\n * key-exchange group X25519MLKEM768 (codepoint 0x11EC, RFC 9370 / draft-kwiatkowski\n * -tls-ecdhe-mlkem). Node's bundled OpenSSL does not offer this group, so it cannot\n * be detected through `node:tls`; we advertise it in a raw ClientHello and read the\n * group the server selects.\n *\n * We advertise `supported_groups = [X25519MLKEM768, x25519, …]` AND send a\n * WELL-FORMED X25519MLKEM768 key_share (`ML-KEM-768.ek (1184) || X25519.pk (32)`,\n * per draft-ietf-tls-ecdhe-mlkem) alongside an x25519 share. A supporting server\n * then selects 0x11EC DIRECTLY in its ServerHello (we read the selected group) —\n * this catches servers that support-but-don't-*prefer* the hybrid group, which the\n * older HelloRetryRequest-only inference missed. The ML-KEM ek is well-formed but\n * throwaway (see mlkem768.ts): qProbe never completes the handshake, so the ML-KEM\n * shared secret is neither computed nor needed. A server that does not support the\n * group falls back to our x25519 share (or answers with a HelloRetryRequest, which\n * the parser still reads).\n *\n * This module is byte manipulation over Buffers (the X25519 key and ML-KEM encoding\n * come from node:crypto / mlkem768.ts); the socket I/O lives in tls.ts. The pure\n * functions are unit-tested with crafted bytes.\n */\nimport { randomBytes, generateKeyPairSync } from \"node:crypto\";\nimport { wellFormedMlKem768Ek } from \"./mlkem768.js\";\n\n/** A raw 32-byte X25519 public key (from node:crypto — a real, valid key). */\nexport function x25519RawPublic(): Buffer {\n const { publicKey } = generateKeyPairSync(\"x25519\");\n const jwk = publicKey.export({ format: \"jwk\" }) as { x?: string };\n return Buffer.from(jwk.x ?? \"\", \"base64url\");\n}\n\n// TLS constants.\nexport const GROUP_X25519 = 0x001d;\nexport const GROUP_X25519MLKEM768 = 0x11ec;\nexport const GROUP_SECP256R1 = 0x0017;\nconst TLS13_VERSION = 0x0304;\n\nconst HS_CLIENT_HELLO = 0x01;\nconst HS_SERVER_HELLO = 0x02;\nconst REC_HANDSHAKE = 0x16;\n\nconst EXT_SERVER_NAME = 0x0000;\nconst EXT_SUPPORTED_GROUPS = 0x000a;\nconst EXT_SIGNATURE_ALGORITHMS = 0x000d;\nconst EXT_SUPPORTED_VERSIONS = 0x002b;\nconst EXT_KEY_SHARE = 0x0033;\n\n/** The special ServerHello.random that marks a HelloRetryRequest (RFC 8446 §4.1.3). */\nconst HRR_RANDOM = Buffer.from(\n \"cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c\",\n \"hex\",\n);\n\nfunction u16(n: number): Buffer {\n const b = Buffer.alloc(2);\n b.writeUInt16BE(n, 0);\n return b;\n}\n\n/** Prefix `body` with a length field of `bytes` width (1, 2, or 3 bytes). */\nfunction withLen(bytes: 1 | 2 | 3, body: Buffer): Buffer {\n const len = Buffer.alloc(bytes);\n if (bytes === 1) len.writeUInt8(body.length, 0);\n else if (bytes === 2) len.writeUInt16BE(body.length, 0);\n else len.writeUIntBE(body.length, 0, 3);\n return Buffer.concat([len, body]);\n}\n\n/** Build a single extension (type + 2-byte-length body). */\nfunction ext(type: number, body: Buffer): Buffer {\n return Buffer.concat([u16(type), withLen(2, body)]);\n}\n\n/**\n * Build a raw ClientHello TLS record advertising the hybrid group. `keyShareGroup`\n * is the group we actually send a key_share for (default x25519), while\n * `supportedGroups` is what we advertise (hybrid first).\n */\nexport function buildClientHello(opts: {\n serverName?: string;\n supportedGroups?: number[];\n keyShareGroup?: number;\n}): Buffer {\n const supportedGroups = opts.supportedGroups ?? [\n GROUP_X25519MLKEM768,\n GROUP_X25519,\n GROUP_SECP256R1,\n ];\n const keyShareGroup = opts.keyShareGroup ?? GROUP_X25519;\n\n const extensions: Buffer[] = [];\n\n if (\n opts.serverName &&\n opts.serverName.length > 0 &&\n !/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(opts.serverName)\n ) {\n // server_name: ServerNameList { name_type(0=host_name), HostName }\n const name = Buffer.from(opts.serverName, \"ascii\");\n const entry = Buffer.concat([Buffer.from([0x00]), withLen(2, name)]);\n extensions.push(ext(EXT_SERVER_NAME, withLen(2, entry)));\n }\n\n // supported_versions: TLS 1.3 only.\n extensions.push(ext(EXT_SUPPORTED_VERSIONS, withLen(1, u16(TLS13_VERSION))));\n\n // supported_groups (named_group list).\n extensions.push(ext(EXT_SUPPORTED_GROUPS, withLen(2, Buffer.concat(supportedGroups.map(u16)))));\n\n // signature_algorithms (a standard modern set).\n const sigAlgs = [0x0403, 0x0804, 0x0807, 0x0401, 0x0805, 0x0806];\n extensions.push(ext(EXT_SIGNATURE_ALGORITHMS, withLen(2, Buffer.concat(sigAlgs.map(u16)))));\n\n // key_share: send a REAL X25519 public plus a WELL-FORMED X25519MLKEM768 hybrid\n // share, so a server that supports the hybrid group can select it DIRECTLY (not\n // only via a HelloRetryRequest). For X25519MLKEM768 the client share is\n // `ML-KEM-768.ek (1184) || X25519.pk (32)` (draft-ietf-tls-ecdhe-mlkem). We never\n // complete the handshake, so the ML-KEM secret is neither computed nor needed.\n void keyShareGroup; // retained for API compat; the share set is now fixed\n const x25519Pub = x25519RawPublic();\n const hybridShareVal = Buffer.concat([wellFormedMlKem768Ek(), x25519Pub]);\n const shareEntries = Buffer.concat([\n u16(GROUP_X25519MLKEM768),\n withLen(2, hybridShareVal),\n u16(GROUP_X25519),\n withLen(2, x25519Pub),\n ]);\n extensions.push(ext(EXT_KEY_SHARE, withLen(2, shareEntries)));\n\n const cipherSuites = Buffer.concat([u16(0x1301), u16(0x1302), u16(0x1303)]);\n\n const helloBody = Buffer.concat([\n u16(0x0303), // legacy_version TLS 1.2\n randomBytes(32), // random\n withLen(1, randomBytes(32)), // legacy_session_id (non-empty → \"middlebox compat\")\n withLen(2, cipherSuites), // cipher_suites\n withLen(1, Buffer.from([0x00])), // compression_methods: null\n withLen(2, Buffer.concat(extensions)), // extensions\n ]);\n\n const handshake = Buffer.concat([Buffer.from([HS_CLIENT_HELLO]), withLen(3, helloBody)]);\n // TLS record: handshake, legacy record version 0x0301.\n return Buffer.concat([Buffer.from([REC_HANDSHAKE]), u16(0x0301), withLen(2, handshake)]);\n}\n\n/** A parsed TLS record. */\nexport interface TlsRecord {\n type: number;\n fragment: Buffer;\n}\n\n/** Split a buffer into TLS records. Stops at the first truncated record. */\nexport function parseRecords(buf: Buffer): TlsRecord[] {\n const out: TlsRecord[] = [];\n let off = 0;\n while (off + 5 <= buf.length) {\n const type = buf[off];\n const len = buf.readUInt16BE(off + 3);\n if (off + 5 + len > buf.length) break;\n out.push({ type, fragment: buf.subarray(off + 5, off + 5 + len) });\n off += 5 + len;\n }\n return out;\n}\n\n/** The result of reading a ServerHello / HelloRetryRequest. */\nexport interface ServerHelloInfo {\n isHelloRetryRequest: boolean;\n /** Group named in the key_share extension (selected group), if present. */\n selectedGroup?: number;\n /** Negotiated version from supported_versions (0x0304 for TLS 1.3), if present. */\n negotiatedVersion?: number;\n cipherSuite?: number;\n}\n\n/**\n * Parse a ServerHello handshake message body (the bytes AFTER the 4-byte handshake\n * header) and extract the selected group + negotiated version. Handles both a\n * normal ServerHello (key_share carries group + key) and a HelloRetryRequest\n * (key_share carries just the selected group) — in both the first 2 bytes of the\n * key_share body are the group.\n */\nexport function parseServerHelloBody(body: Buffer): ServerHelloInfo {\n let off = 0;\n const need = (n: number) => {\n if (off + n > body.length) throw new RangeError(\"truncated ServerHello\");\n };\n need(2);\n off += 2; // legacy_version\n need(32);\n const random = body.subarray(off, off + 32);\n off += 32;\n const isHRR = random.equals(HRR_RANDOM);\n need(1);\n const sidLen = body[off];\n off += 1;\n need(sidLen);\n off += sidLen; // legacy_session_id_echo\n need(2);\n const cipherSuite = body.readUInt16BE(off);\n off += 2;\n need(1);\n off += 1; // legacy_compression_method\n\n const info: ServerHelloInfo = { isHelloRetryRequest: isHRR, cipherSuite };\n if (off + 2 > body.length) return info; // no extensions\n const extTotal = body.readUInt16BE(off);\n off += 2;\n const extEnd = Math.min(off + extTotal, body.length);\n while (off + 4 <= extEnd) {\n const extType = body.readUInt16BE(off);\n const extLen = body.readUInt16BE(off + 2);\n const start = off + 4;\n if (start + extLen > body.length) break;\n const extBody = body.subarray(start, start + extLen);\n if (extType === EXT_KEY_SHARE && extBody.length >= 2) {\n info.selectedGroup = extBody.readUInt16BE(0);\n } else if (extType === EXT_SUPPORTED_VERSIONS && extBody.length >= 2) {\n info.negotiatedVersion = extBody.readUInt16BE(0);\n }\n off = start + extLen;\n }\n return info;\n}\n\n/** Read the first ServerHello/HRR out of a raw response buffer, if any. */\nexport function readServerHello(raw: Buffer): ServerHelloInfo | undefined {\n for (const rec of parseRecords(raw)) {\n if (rec.type !== REC_HANDSHAKE) continue;\n const f = rec.fragment;\n if (f.length < 4) continue;\n if (f[0] !== HS_SERVER_HELLO) continue;\n const len = f.readUIntBE(1, 3);\n if (4 + len > f.length) continue;\n return parseServerHelloBody(f.subarray(4, 4 + len));\n }\n return undefined;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,10 @@ export { certSignatureAlgorithm, decodeOid, oidToSignatureFamily } from "./x509.
|
|
|
23
23
|
export { smtpAdvertisesStartTls, smtpReplyComplete } from "./smtp.js";
|
|
24
24
|
export { classifyTls, classifySsh } from "./classify.js";
|
|
25
25
|
export { toScanResult, toSarifReport, toCbomReport, toJsonReport } from "./report.js";
|
|
26
|
-
export type ProbeMode = "tls" | "ssh" | "smtp";
|
|
26
|
+
export type ProbeMode = "tls" | "ssh" | "smtp" | "imap" | "pop3" | "postgres";
|
|
27
|
+
/** Re-export the pure STARTTLS/SSLRequest builders for testing (not the probes). */
|
|
28
|
+
export { IMAP_DIALOG, POP3_DIALOG } from "./starttls.js";
|
|
29
|
+
export { sslRequestFrame } from "./postgres.js";
|
|
27
30
|
export interface EndpointReport {
|
|
28
31
|
target: Target;
|
|
29
32
|
mode: ProbeMode;
|
|
@@ -34,7 +37,11 @@ export interface EndpointReport {
|
|
|
34
37
|
positives: string[];
|
|
35
38
|
findings: Finding[];
|
|
36
39
|
}
|
|
37
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Choose a probe mode for "auto" from the well-known port: SSH on 22, SMTP
|
|
42
|
+
* STARTTLS on 25/587, IMAP on 143, POP3 on 110, PostgreSQL on 5432, TLS otherwise
|
|
43
|
+
* (which covers direct-TLS services like HTTPS 443, IMAPS 993, and DoT 853).
|
|
44
|
+
*/
|
|
38
45
|
export declare function resolveMode(target: Target, mode: ProbeMode | "auto"): ProbeMode;
|
|
39
46
|
export interface RunOptions {
|
|
40
47
|
targets: Target[];
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAMzD,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,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9E,oFAAoF;AACpF,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,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;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAQ/E;AAyCD,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
CHANGED
|
@@ -3,6 +3,8 @@ import { authorizeTargets } from "./attest.js";
|
|
|
3
3
|
import { probeTlsNegotiated, probeHybridSupport, } from "./tls.js";
|
|
4
4
|
import { probeSsh } from "./ssh.js";
|
|
5
5
|
import { probeSmtpStartTls } from "./smtp.js";
|
|
6
|
+
import { probeLineStartTls, IMAP_DIALOG, POP3_DIALOG } from "./starttls.js";
|
|
7
|
+
import { probePostgresSsl } from "./postgres.js";
|
|
6
8
|
import { classifyTls, classifySsh } from "./classify.js";
|
|
7
9
|
export { parseTarget, TargetError } from "./target.js";
|
|
8
10
|
export { authorizeTargets, parseOwnedHosts, AttestationError, } from "./attest.js";
|
|
@@ -12,7 +14,14 @@ export { certSignatureAlgorithm, decodeOid, oidToSignatureFamily } from "./x509.
|
|
|
12
14
|
export { smtpAdvertisesStartTls, smtpReplyComplete } from "./smtp.js"; // pure SMTP reply framing
|
|
13
15
|
export { classifyTls, classifySsh } from "./classify.js";
|
|
14
16
|
export { toScanResult, toSarifReport, toCbomReport, toJsonReport } from "./report.js";
|
|
15
|
-
/**
|
|
17
|
+
/** Re-export the pure STARTTLS/SSLRequest builders for testing (not the probes). */
|
|
18
|
+
export { IMAP_DIALOG, POP3_DIALOG } from "./starttls.js";
|
|
19
|
+
export { sslRequestFrame } from "./postgres.js";
|
|
20
|
+
/**
|
|
21
|
+
* Choose a probe mode for "auto" from the well-known port: SSH on 22, SMTP
|
|
22
|
+
* STARTTLS on 25/587, IMAP on 143, POP3 on 110, PostgreSQL on 5432, TLS otherwise
|
|
23
|
+
* (which covers direct-TLS services like HTTPS 443, IMAPS 993, and DoT 853).
|
|
24
|
+
*/
|
|
16
25
|
export function resolveMode(target, mode) {
|
|
17
26
|
if (mode !== "auto")
|
|
18
27
|
return mode;
|
|
@@ -20,6 +29,12 @@ export function resolveMode(target, mode) {
|
|
|
20
29
|
return "ssh";
|
|
21
30
|
if (target.port === 25 || target.port === 587)
|
|
22
31
|
return "smtp";
|
|
32
|
+
if (target.port === 143)
|
|
33
|
+
return "imap";
|
|
34
|
+
if (target.port === 110)
|
|
35
|
+
return "pop3";
|
|
36
|
+
if (target.port === 5432)
|
|
37
|
+
return "postgres";
|
|
23
38
|
return "tls";
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
@@ -35,12 +50,21 @@ async function probeEndpoint(target, mode, opts = {}) {
|
|
|
35
50
|
positives.push("PQC SSH key exchange offered");
|
|
36
51
|
return { target, mode, ssh, positives, findings: classifySsh(target, ssh) };
|
|
37
52
|
}
|
|
38
|
-
if (mode === "smtp") {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
if (mode === "smtp" || mode === "imap" || mode === "pop3" || mode === "postgres") {
|
|
54
|
+
// Upgrade to TLS via the protocol's own mechanism (STARTTLS / STLS / SSLRequest),
|
|
55
|
+
// then the same negotiated inspection. No hybrid probe over an upgraded socket, so
|
|
56
|
+
// classify with an "inconclusive" hybrid result rather than claim we tested a
|
|
57
|
+
// hybrid group we never advertised.
|
|
58
|
+
let tls;
|
|
59
|
+
if (mode === "smtp")
|
|
60
|
+
tls = await probeSmtpStartTls(target.host, target.port, opts);
|
|
61
|
+
else if (mode === "imap")
|
|
62
|
+
tls = await probeLineStartTls(target.host, target.port, IMAP_DIALOG, opts);
|
|
63
|
+
else if (mode === "pop3")
|
|
64
|
+
tls = await probeLineStartTls(target.host, target.port, POP3_DIALOG, opts);
|
|
65
|
+
else
|
|
66
|
+
tls = await probePostgresSsl(target.host, target.port, opts);
|
|
67
|
+
const hybrid = { hybridSelected: false, error: `not probed (${mode})` };
|
|
44
68
|
return { target, mode, tls, hybrid, positives, findings: classifyTls(target, tls, hybrid) };
|
|
45
69
|
}
|
|
46
70
|
const [tls, hybrid] = await Promise.all([
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","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"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","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 { probeLineStartTls, IMAP_DIALOG, POP3_DIALOG } from \"./starttls.js\";\nimport { probePostgresSsl } from \"./postgres.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\" | \"imap\" | \"pop3\" | \"postgres\";\n\n/** Re-export the pure STARTTLS/SSLRequest builders for testing (not the probes). */\nexport { IMAP_DIALOG, POP3_DIALOG } from \"./starttls.js\";\nexport { sslRequestFrame } from \"./postgres.js\";\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/**\n * Choose a probe mode for \"auto\" from the well-known port: SSH on 22, SMTP\n * STARTTLS on 25/587, IMAP on 143, POP3 on 110, PostgreSQL on 5432, TLS otherwise\n * (which covers direct-TLS services like HTTPS 443, IMAPS 993, and DoT 853).\n */\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 if (target.port === 143) return \"imap\";\n if (target.port === 110) return \"pop3\";\n if (target.port === 5432) return \"postgres\";\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\" || mode === \"imap\" || mode === \"pop3\" || mode === \"postgres\") {\n // Upgrade to TLS via the protocol's own mechanism (STARTTLS / STLS / SSLRequest),\n // then the same negotiated inspection. No hybrid probe over an upgraded socket, so\n // classify with an \"inconclusive\" hybrid result rather than claim we tested a\n // hybrid group we never advertised.\n let tls: TlsNegotiated;\n if (mode === \"smtp\") tls = await probeSmtpStartTls(target.host, target.port, opts);\n else if (mode === \"imap\")\n tls = await probeLineStartTls(target.host, target.port, IMAP_DIALOG, opts);\n else if (mode === \"pop3\")\n tls = await probeLineStartTls(target.host, target.port, POP3_DIALOG, opts);\n else tls = await probePostgresSsl(target.host, target.port, opts);\n const hybrid: HybridSupport = { hybridSelected: false, error: `not probed (${mode})` };\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"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ML_KEM_Q = 3329;
|
|
2
|
+
/** Encoded encapsulation-key length: 384·k bytes of ByteEncode₁₂ + 32-byte ρ. */
|
|
3
|
+
export declare const ML_KEM_768_EK_BYTES: number;
|
|
4
|
+
/** ByteEncode₁₂: pack 256 12-bit coefficients (each in [0, q)) into 384 bytes. */
|
|
5
|
+
export declare function byteEncode12(coeffs: readonly number[]): Buffer;
|
|
6
|
+
/** ByteDecode₁₂: inverse of {@link byteEncode12} (for tests). */
|
|
7
|
+
export declare function byteDecode12(bytes: Buffer): number[];
|
|
8
|
+
/**
|
|
9
|
+
* Build a WELL-FORMED ML-KEM-768 encapsulation key (1184 bytes): `ByteEncode₁₂` of
|
|
10
|
+
* a random in-range `t̂` (k=3 polynomials) followed by a 32-byte ρ. Accepted by a
|
|
11
|
+
* FIPS 203 encaps input check; the corresponding secret is not recoverable by us
|
|
12
|
+
* (and is not needed — we only observe which group the server selects).
|
|
13
|
+
*/
|
|
14
|
+
export declare function wellFormedMlKem768Ek(): Buffer;
|
|
15
|
+
//# sourceMappingURL=mlkem768.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mlkem768.d.ts","sourceRoot":"","sources":["../src/mlkem768.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,QAAQ,OAAO,CAAC;AAG7B,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,QAAe,CAAC;AAEhD,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAW9D;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAQpD;AAcD;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C"}
|