neon 2.47.0 → 3.1.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 +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
package/dist/psql/wire/tls.js
CHANGED
|
@@ -1,602 +1,441 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Two responsibilities:
|
|
5
|
-
*
|
|
6
|
-
* 1. Drive the PG-flavoured SSL handshake. Postgres negotiates TLS
|
|
7
|
-
* *before* the protocol startup: client sends an `SSLRequest`
|
|
8
|
-
* (8-byte fixed message), server replies with a single byte —
|
|
9
|
-
* 'S' to accept, 'N' to refuse. On 'S' we wrap the existing socket
|
|
10
|
-
* with `tls.connect({ socket })`; on 'N' we either bail (require/
|
|
11
|
-
* verify-*) or fall through plaintext (prefer/allow/disable).
|
|
12
|
-
*
|
|
13
|
-
* 2. Extract `tls-server-end-point` channel-binding material from the
|
|
14
|
-
* negotiated TLS session for SCRAM-SHA-256-PLUS. Per RFC 5929 §4 the
|
|
15
|
-
* data is the hash of the peer certificate computed with the cert's
|
|
16
|
-
* own signature hash, unless that hash is MD5 or SHA-1 — in which
|
|
17
|
-
* case the binding uses SHA-256. libpq's policy (`fe-secure-openssl.c`
|
|
18
|
-
* `PgChannelBinding`) is "always SHA-256 of the DER cert", which is
|
|
19
|
-
* also what the PG server expects. We follow libpq.
|
|
20
|
-
*
|
|
21
|
-
* Notes:
|
|
22
|
-
* - We deliberately do NOT validate the server cert here; that's the
|
|
23
|
-
* caller's responsibility (pass `tlsOpts.rejectUnauthorized` etc.). The
|
|
24
|
-
* ssl-mode → tls-options mapping lives in `connection.ts`.
|
|
25
|
-
* - `verify-ca` and `verify-full` differ only in hostname checking, which
|
|
26
|
-
* Node's `tls.connect` performs automatically when `checkServerIdentity`
|
|
27
|
-
* is the default and `servername` is set. The connection layer wires
|
|
28
|
-
* `servername` to the configured host before calling us.
|
|
29
|
-
*/
|
|
30
|
-
import { createHash, createPrivateKey } from "node:crypto";
|
|
31
|
-
import { appendFileSync, promises as fs } from "node:fs";
|
|
1
|
+
import { SSLRequest } from "./protocol.js";
|
|
2
|
+
import { appendFileSync, promises } from "node:fs";
|
|
32
3
|
import * as path from "node:path";
|
|
4
|
+
import { createHash, createPrivateKey } from "node:crypto";
|
|
33
5
|
import * as tls from "node:tls";
|
|
34
|
-
|
|
6
|
+
//#region src/psql/wire/tls.ts
|
|
7
|
+
/**
|
|
8
|
+
* TLS negotiation for the psql wire layer (WP-02).
|
|
9
|
+
*
|
|
10
|
+
* Two responsibilities:
|
|
11
|
+
*
|
|
12
|
+
* 1. Drive the PG-flavoured SSL handshake. Postgres negotiates TLS
|
|
13
|
+
* *before* the protocol startup: client sends an `SSLRequest`
|
|
14
|
+
* (8-byte fixed message), server replies with a single byte —
|
|
15
|
+
* 'S' to accept, 'N' to refuse. On 'S' we wrap the existing socket
|
|
16
|
+
* with `tls.connect({ socket })`; on 'N' we either bail (require/
|
|
17
|
+
* verify-*) or fall through plaintext (prefer/allow/disable).
|
|
18
|
+
*
|
|
19
|
+
* 2. Extract `tls-server-end-point` channel-binding material from the
|
|
20
|
+
* negotiated TLS session for SCRAM-SHA-256-PLUS. Per RFC 5929 §4 the
|
|
21
|
+
* data is the hash of the peer certificate computed with the cert's
|
|
22
|
+
* own signature hash, unless that hash is MD5 or SHA-1 — in which
|
|
23
|
+
* case the binding uses SHA-256. libpq's policy (`fe-secure-openssl.c`
|
|
24
|
+
* `PgChannelBinding`) is "always SHA-256 of the DER cert", which is
|
|
25
|
+
* also what the PG server expects. We follow libpq.
|
|
26
|
+
*
|
|
27
|
+
* Notes:
|
|
28
|
+
* - We deliberately do NOT validate the server cert here; that's the
|
|
29
|
+
* caller's responsibility (pass `tlsOpts.rejectUnauthorized` etc.). The
|
|
30
|
+
* ssl-mode → tls-options mapping lives in `connection.ts`.
|
|
31
|
+
* - `verify-ca` and `verify-full` differ only in hostname checking, which
|
|
32
|
+
* Node's `tls.connect` performs automatically when `checkServerIdentity`
|
|
33
|
+
* is the default and `servername` is set. The connection layer wires
|
|
34
|
+
* `servername` to the configured host before calling us.
|
|
35
|
+
*/
|
|
35
36
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!raw || raw.length === 0) {
|
|
46
|
-
throw new Error("TLS channel binding: peer certificate has no DER bytes");
|
|
47
|
-
}
|
|
48
|
-
return createHash("sha256").update(raw).digest();
|
|
37
|
+
* Hash of the peer cert for `tls-server-end-point`. libpq always uses SHA-256
|
|
38
|
+
* of the DER-encoded certificate; we match that.
|
|
39
|
+
*
|
|
40
|
+
* Exposed for tests so we can stub the peer cert.
|
|
41
|
+
*/
|
|
42
|
+
function computeChannelBindingData(cert) {
|
|
43
|
+
const raw = cert.raw;
|
|
44
|
+
if (!raw || raw.length === 0) throw new Error("TLS channel binding: peer certificate has no DER bytes");
|
|
45
|
+
return createHash("sha256").update(raw).digest();
|
|
49
46
|
}
|
|
50
47
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
const reply = await sendSslRequest(socket);
|
|
90
|
-
if (reply === "S") {
|
|
91
|
-
const mergedOpts = await loadTlsFileOptions(tlsOpts, fileOpts, sslMode);
|
|
92
|
-
return upgradeToTls(socket, mergedOpts, fileOpts.sslkeylogfile, fileOpts.sslkey);
|
|
93
|
-
}
|
|
94
|
-
// reply === 'N': server refused TLS.
|
|
95
|
-
if (sslMode === "require" ||
|
|
96
|
-
sslMode === "verify-ca" ||
|
|
97
|
-
sslMode === "verify-full") {
|
|
98
|
-
throw new Error(`SSL connection required (sslmode=${sslMode}) but server refused (replied 'N')`);
|
|
99
|
-
}
|
|
100
|
-
// 'allow' / 'prefer': fall back to plain text.
|
|
101
|
-
return { kind: "plain", socket };
|
|
48
|
+
* Send SSLRequest, read the 1-byte server response, and either upgrade the
|
|
49
|
+
* socket to TLS or stay plain (depending on `sslMode`).
|
|
50
|
+
*
|
|
51
|
+
* `tlsOpts` is passed through to `tls.connect` — the connection layer fills
|
|
52
|
+
* in `host`, `servername`, `ca`, `rejectUnauthorized`, etc. before calling.
|
|
53
|
+
*
|
|
54
|
+
* `fileOpts` carries libpq-style PEM file paths (`sslcert`, `sslkey`,
|
|
55
|
+
* `sslrootcert`, `sslcrl`). Each present path is read from disk before the
|
|
56
|
+
* TLS handshake and threaded into the corresponding tls.connect option
|
|
57
|
+
* (`cert` / `key` / `ca` / `crl`). Read failures bubble out as
|
|
58
|
+
* `could not read ssl<…>: <message>` so the caller sees the libpq diagnostic
|
|
59
|
+
* shape rather than a bare ENOENT.
|
|
60
|
+
*
|
|
61
|
+
* `fileOpts.sslkeylogfile`, when set, is pre-checked for writability here and
|
|
62
|
+
* wired to a `'keylog'` listener on the upgraded socket so TLS session keys
|
|
63
|
+
* are appended for offline decryption.
|
|
64
|
+
*
|
|
65
|
+
* `negotiation` selects how TLS is started (libpq `sslnegotiation`):
|
|
66
|
+
* - `'postgres'` (default): send `SSLRequest` and await the 'S'/'N' reply.
|
|
67
|
+
* - `'direct'`: skip `SSLRequest` and start the TLS handshake immediately
|
|
68
|
+
* on the raw socket (PG 17+). The caller must have set
|
|
69
|
+
* `tlsOpts.ALPNProtocols` to `['postgresql']`; there is no plaintext
|
|
70
|
+
* fallback, so a server that does not speak TLS surfaces the handshake
|
|
71
|
+
* failure rather than a quiet downgrade.
|
|
72
|
+
*/
|
|
73
|
+
async function negotiateTls(socket, sslMode, tlsOpts = {}, fileOpts = {}, negotiation = "postgres") {
|
|
74
|
+
if (sslMode === "disable") return {
|
|
75
|
+
kind: "plain",
|
|
76
|
+
socket
|
|
77
|
+
};
|
|
78
|
+
if (negotiation === "direct") return upgradeToTls(socket, await loadTlsFileOptions(tlsOpts, fileOpts, sslMode), fileOpts.sslkeylogfile, fileOpts.sslkey, true);
|
|
79
|
+
if (await sendSslRequest(socket) === "S") return upgradeToTls(socket, await loadTlsFileOptions(tlsOpts, fileOpts, sslMode), fileOpts.sslkeylogfile, fileOpts.sslkey);
|
|
80
|
+
if (sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full") throw new Error(`SSL connection required (sslmode=${sslMode}) but server refused (replied 'N')`);
|
|
81
|
+
return {
|
|
82
|
+
kind: "plain",
|
|
83
|
+
socket
|
|
84
|
+
};
|
|
102
85
|
}
|
|
103
86
|
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
else if (systemCas.length > 1) {
|
|
154
|
-
merged.ca = systemCas;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
merged.ca = await readPem("sslrootcert", fileOpts.sslrootcert, "CERTIFICATE");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// libpq `sslcertmode` gates whether the client cert/key are sent.
|
|
162
|
-
// - `disable`: skip loading them entirely, even when configured.
|
|
163
|
-
// - `require`: a cert MUST be configured (we only honour an explicit
|
|
164
|
-
// `sslcert`, not libpq's default `~/.postgresql/postgresql.crt`).
|
|
165
|
-
// - `allow` / unset: current behaviour (send when present).
|
|
166
|
-
const certMode = fileOpts.sslcertmode ?? "allow";
|
|
167
|
-
const clientCert = fileOpts.sslcert !== undefined && fileOpts.sslcert !== ""
|
|
168
|
-
? fileOpts.sslcert
|
|
169
|
-
: undefined;
|
|
170
|
-
if (certMode === "require" && clientCert === undefined) {
|
|
171
|
-
throw new Error(`sslcertmode value "require" requires a client certificate`);
|
|
172
|
-
}
|
|
173
|
-
if (certMode !== "disable") {
|
|
174
|
-
if (clientCert !== undefined) {
|
|
175
|
-
merged.cert = await readPem("sslcert", clientCert, "CERTIFICATE");
|
|
176
|
-
}
|
|
177
|
-
if (fileOpts.sslkey !== undefined && fileOpts.sslkey !== "") {
|
|
178
|
-
await assertKeyPermissions(fileOpts.sslkey);
|
|
179
|
-
merged.key = await readPem("sslkey", fileOpts.sslkey, "PRIVATE KEY");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// CRLs come from a single file (`sslcrl`) and/or every file in a directory
|
|
183
|
-
// (`sslcrldir`). Node's `crl` option accepts an array of PEM buffers, so we
|
|
184
|
-
// collect each source and only set `crl` when at least one was read.
|
|
185
|
-
const crls = [];
|
|
186
|
-
if (fileOpts.sslcrl !== undefined && fileOpts.sslcrl !== "") {
|
|
187
|
-
crls.push(await readPem("sslcrl", fileOpts.sslcrl));
|
|
188
|
-
}
|
|
189
|
-
if (fileOpts.sslcrldir !== undefined && fileOpts.sslcrldir !== "") {
|
|
190
|
-
crls.push(...(await readCrlDir(fileOpts.sslcrldir)));
|
|
191
|
-
}
|
|
192
|
-
if (crls.length === 1) {
|
|
193
|
-
merged.crl = crls[0];
|
|
194
|
-
}
|
|
195
|
-
else if (crls.length > 1) {
|
|
196
|
-
merged.crl = crls;
|
|
197
|
-
}
|
|
198
|
-
// sslpassword is plumbed through verbatim — OpenSSL applies it when it
|
|
199
|
-
// sees an encrypted key. Empty string is "no passphrase" (libpq's
|
|
200
|
-
// convention) so we skip it.
|
|
201
|
-
if (fileOpts.sslpassword !== undefined && fileOpts.sslpassword !== "") {
|
|
202
|
-
merged.passphrase = fileOpts.sslpassword;
|
|
203
|
-
}
|
|
204
|
-
// sslkeylogfile is not a tls.connect option; the keylog listener is wired
|
|
205
|
-
// in `upgradeToTls`. We pre-check it here (the home of file diagnostics)
|
|
206
|
-
// by opening it for append, so an unwritable path fails fast at connect
|
|
207
|
-
// time with `could not open sslkeylogfile "<path>": <reason>` rather than
|
|
208
|
-
// silently dropping keys mid-handshake.
|
|
209
|
-
if (fileOpts.sslkeylogfile !== undefined && fileOpts.sslkeylogfile !== "") {
|
|
210
|
-
await assertKeyLogFileWritable(fileOpts.sslkeylogfile);
|
|
211
|
-
}
|
|
212
|
-
return merged;
|
|
87
|
+
* Read each non-empty file path in `fileOpts` and merge the bytes onto a
|
|
88
|
+
* shallow copy of `tlsOpts`. Each ENOENT / EACCES / permission error is
|
|
89
|
+
* surfaced as `could not read ssl<file>: <reason>` so users immediately
|
|
90
|
+
* know which option pointed at a bad path.
|
|
91
|
+
*
|
|
92
|
+
* Behaviour-defining details:
|
|
93
|
+
*
|
|
94
|
+
* - `sslrootcert` is only read when `sslMode` is `verify-ca` /
|
|
95
|
+
* `verify-full`. Lower modes (require / prefer / allow) accept the
|
|
96
|
+
* server cert without consulting the trust anchor, matching libpq's
|
|
97
|
+
* policy of never opening the file in those modes. Tests can pass
|
|
98
|
+
* `'verify-ca'` as the sentinel to force the eager read for any
|
|
99
|
+
* non-disable mode (the default if omitted).
|
|
100
|
+
* - `sslpassword` is plumbed into `tls.connect` as `passphrase`; if
|
|
101
|
+
* unset OpenSSL leaves an encrypted key un-decryptable and the
|
|
102
|
+
* handshake errors with a "bad decrypt" diagnostic.
|
|
103
|
+
*
|
|
104
|
+
* Exported for tests (`tls.test.ts` swaps in a mocked `tls.connect`).
|
|
105
|
+
*/
|
|
106
|
+
async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
107
|
+
const merged = { ...tlsOpts };
|
|
108
|
+
if ((sslMode === void 0 || sslMode === "verify-ca" || sslMode === "verify-full") && fileOpts.sslrootcert !== void 0 && fileOpts.sslrootcert !== "") if (fileOpts.sslrootcert === "system") {
|
|
109
|
+
const systemCas = [];
|
|
110
|
+
const sslCertFile = process.env.SSL_CERT_FILE;
|
|
111
|
+
if (sslCertFile !== void 0 && sslCertFile !== "") systemCas.push(await readPem("sslrootcert", sslCertFile, "CERTIFICATE"));
|
|
112
|
+
const sslCertDir = process.env.SSL_CERT_DIR;
|
|
113
|
+
if (sslCertDir !== void 0 && sslCertDir !== "") systemCas.push(...await readCaDir(sslCertDir));
|
|
114
|
+
if (systemCas.length === 1) merged.ca = systemCas[0];
|
|
115
|
+
else if (systemCas.length > 1) merged.ca = systemCas;
|
|
116
|
+
} else merged.ca = await readPem("sslrootcert", fileOpts.sslrootcert, "CERTIFICATE");
|
|
117
|
+
const certMode = fileOpts.sslcertmode ?? "allow";
|
|
118
|
+
const clientCert = fileOpts.sslcert !== void 0 && fileOpts.sslcert !== "" ? fileOpts.sslcert : void 0;
|
|
119
|
+
if (certMode === "require" && clientCert === void 0) throw new Error(`sslcertmode value "require" requires a client certificate`);
|
|
120
|
+
if (certMode !== "disable") {
|
|
121
|
+
if (clientCert !== void 0) merged.cert = await readPem("sslcert", clientCert, "CERTIFICATE");
|
|
122
|
+
if (fileOpts.sslkey !== void 0 && fileOpts.sslkey !== "") {
|
|
123
|
+
await assertKeyPermissions(fileOpts.sslkey);
|
|
124
|
+
merged.key = await readPem("sslkey", fileOpts.sslkey, "PRIVATE KEY");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const crls = [];
|
|
128
|
+
if (fileOpts.sslcrl !== void 0 && fileOpts.sslcrl !== "") crls.push(await readPem("sslcrl", fileOpts.sslcrl));
|
|
129
|
+
if (fileOpts.sslcrldir !== void 0 && fileOpts.sslcrldir !== "") crls.push(...await readCrlDir(fileOpts.sslcrldir));
|
|
130
|
+
if (crls.length === 1) merged.crl = crls[0];
|
|
131
|
+
else if (crls.length > 1) merged.crl = crls;
|
|
132
|
+
if (fileOpts.sslpassword !== void 0 && fileOpts.sslpassword !== "") merged.passphrase = fileOpts.sslpassword;
|
|
133
|
+
if (fileOpts.sslkeylogfile !== void 0 && fileOpts.sslkeylogfile !== "") await assertKeyLogFileWritable(fileOpts.sslkeylogfile);
|
|
134
|
+
return merged;
|
|
213
135
|
}
|
|
214
136
|
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
137
|
+
* Pre-flight the `sslkeylogfile` target by opening it for append (creating
|
|
138
|
+
* it if absent) and immediately closing the handle. Surfaces libpq-style
|
|
139
|
+
* `could not open sslkeylogfile "<path>": <reason>` on any failure (e.g. an
|
|
140
|
+
* unwritable directory) before the handshake starts.
|
|
141
|
+
*/
|
|
220
142
|
async function assertKeyLogFileWritable(filePath) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new Error(`could not open sslkeylogfile "${filePath}": ${reason}`);
|
|
228
|
-
}
|
|
143
|
+
try {
|
|
144
|
+
await (await promises.open(filePath, "a")).close();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
147
|
+
throw new Error(`could not open sslkeylogfile "${filePath}": ${reason}`);
|
|
148
|
+
}
|
|
229
149
|
}
|
|
230
150
|
/** True when the bytes already carry a `-----BEGIN ...-----` PEM header. */
|
|
231
151
|
function isPemArmored(bytes) {
|
|
232
|
-
|
|
233
|
-
// whitespace libpq tolerates) for the armor marker. DER is binary and will
|
|
234
|
-
// not contain this token at the front.
|
|
235
|
-
const head = bytes.subarray(0, 64).toString("latin1");
|
|
236
|
-
return head.includes("-----BEGIN");
|
|
152
|
+
return bytes.subarray(0, 64).toString("latin1").includes("-----BEGIN");
|
|
237
153
|
}
|
|
238
154
|
/**
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
155
|
+
* Wrap raw DER bytes in the requested PEM armor: base64 the DER, split into
|
|
156
|
+
* 64-char lines (the PEM convention), and bracket with the BEGIN/END markers.
|
|
157
|
+
*/
|
|
242
158
|
function derToPem(der, armor) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const body = lines.join("\n");
|
|
246
|
-
const pem = `-----BEGIN ${armor}-----\n${body}\n-----END ${armor}-----\n`;
|
|
247
|
-
return Buffer.from(pem, "ascii");
|
|
159
|
+
const pem = `-----BEGIN ${armor}-----\n${(der.toString("base64").match(/.{1,64}/g) ?? []).join("\n")}\n-----END ${armor}-----\n`;
|
|
160
|
+
return Buffer.from(pem, "ascii");
|
|
248
161
|
}
|
|
249
162
|
/**
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
163
|
+
* Convert a DER-encoded private key to canonical PKCS#8 PEM, the way libpq
|
|
164
|
+
* relies on OpenSSL to sniff `sslkey` format. A DER key may be PKCS#8
|
|
165
|
+
* (`PrivateKeyInfo`), PKCS#1 (bare RSA `RSAPrivateKey`), or SEC1 (bare EC
|
|
166
|
+
* `ECPrivateKey`) — and the encoding `openssl pkey -outform der` produces for
|
|
167
|
+
* RSA differs across OpenSSL versions (3.0.x emits a form that blind PKCS#8
|
|
168
|
+
* armor cannot load: `DECODER routines::unsupported`). Rather than guess the
|
|
169
|
+
* armor, let `crypto.createPrivateKey` decode each candidate type and
|
|
170
|
+
* re-export a single canonical PKCS#8 PEM that `tls.connect` always accepts.
|
|
171
|
+
*/
|
|
259
172
|
function derPrivateKeyToPem(der) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
173
|
+
let lastErr;
|
|
174
|
+
for (const type of [
|
|
175
|
+
"pkcs8",
|
|
176
|
+
"pkcs1",
|
|
177
|
+
"sec1"
|
|
178
|
+
]) try {
|
|
179
|
+
const pem = createPrivateKey({
|
|
180
|
+
key: der,
|
|
181
|
+
format: "der",
|
|
182
|
+
type
|
|
183
|
+
}).export({
|
|
184
|
+
format: "pem",
|
|
185
|
+
type: "pkcs8"
|
|
186
|
+
});
|
|
187
|
+
return typeof pem === "string" ? Buffer.from(pem, "ascii") : pem;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
lastErr = err;
|
|
190
|
+
}
|
|
191
|
+
const reason = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
192
|
+
throw new Error(`sslkey is DER but could not be decoded as PKCS#8, PKCS#1, or SEC1: ${reason}`);
|
|
273
193
|
}
|
|
274
194
|
/**
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
195
|
+
* Read a libpq SSL file, returning PEM bytes ready for `tls.connect`. If the
|
|
196
|
+
* file is already PEM-armored it's returned verbatim; otherwise it's treated
|
|
197
|
+
* as DER and converted in-memory using {@link derToPem} with `derArmor`
|
|
198
|
+
* (matching libpq's PEM-or-DER auto-detection). When `derArmor` is omitted the
|
|
199
|
+
* file is returned as-is even if not PEM (used for CRLs, where DER conversion
|
|
200
|
+
* is out of scope).
|
|
201
|
+
*/
|
|
282
202
|
async function readPem(label, filePath, derArmor) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Private keys need format-aware decoding (PKCS#8 / PKCS#1 / SEC1);
|
|
293
|
-
// certs are a single ASN.1 shape and wrap directly.
|
|
294
|
-
return derArmor === "PRIVATE KEY"
|
|
295
|
-
? derPrivateKeyToPem(bytes)
|
|
296
|
-
: derToPem(bytes, derArmor);
|
|
297
|
-
}
|
|
298
|
-
return bytes;
|
|
203
|
+
let bytes;
|
|
204
|
+
try {
|
|
205
|
+
bytes = await promises.readFile(filePath);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
208
|
+
throw new Error(`could not read ${label} "${filePath}": ${reason}`);
|
|
209
|
+
}
|
|
210
|
+
if (derArmor !== void 0 && !isPemArmored(bytes)) return derArmor === "PRIVATE KEY" ? derPrivateKeyToPem(bytes) : derToPem(bytes, derArmor);
|
|
211
|
+
return bytes;
|
|
299
212
|
}
|
|
300
213
|
/**
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
214
|
+
* libpq-style permission guard for the client private key (`sslkey`). libpq
|
|
215
|
+
* (`fe-secure-openssl.c`) `stat()`s the key file and refuses to load it when
|
|
216
|
+
* it is a regular file with any group or world access bits set, unless it is
|
|
217
|
+
* root-owned with at most `u=rw,g=r` (0640). Mirroring that keeps an
|
|
218
|
+
* accidentally world-readable key from being used silently.
|
|
219
|
+
*
|
|
220
|
+
* The check is a no-op on Windows, where the POSIX mode bits are not
|
|
221
|
+
* meaningful (matching libpq, which `#ifndef WIN32`-guards the same check),
|
|
222
|
+
* and when the key path is a directory / special file (only regular files
|
|
223
|
+
* carry a private key here).
|
|
224
|
+
*/
|
|
312
225
|
async function assertKeyPermissions(keyPath) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const mode = stat.mode & 0o777;
|
|
327
|
-
const groupOrWorld = mode & 0o077;
|
|
328
|
-
if (groupOrWorld === 0)
|
|
329
|
-
return;
|
|
330
|
-
// Root-owned keys are allowed to be u=rw,g=r (0640) or less, matching
|
|
331
|
-
// libpq's relaxed allowance for system-managed keys: no bits outside the
|
|
332
|
-
// 0640 mask may be set.
|
|
333
|
-
if (stat.uid === 0 && (mode & ~0o640) === 0) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
throw new Error(`private key file "${keyPath}" has group or world access`);
|
|
226
|
+
if (process.platform === "win32") return;
|
|
227
|
+
let stat;
|
|
228
|
+
try {
|
|
229
|
+
stat = await promises.stat(keyPath);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
232
|
+
throw new Error(`could not read sslkey "${keyPath}": ${reason}`);
|
|
233
|
+
}
|
|
234
|
+
if (!stat.isFile()) return;
|
|
235
|
+
const mode = stat.mode & 511;
|
|
236
|
+
if ((mode & 63) === 0) return;
|
|
237
|
+
if (stat.uid === 0 && (mode & -417) === 0) return;
|
|
238
|
+
throw new Error(`private key file "${keyPath}" has group or world access`);
|
|
337
239
|
}
|
|
338
240
|
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
241
|
+
* Read every regular file in an `sslcrldir` directory and return their PEM
|
|
242
|
+
* bytes. Subdirectories are skipped (libpq's c_rehash-style directory only
|
|
243
|
+
* holds hashed CRL files). A failure to list the directory or read any file
|
|
244
|
+
* surfaces with the `sslcrldir` label so the caller sees which option was
|
|
245
|
+
* misconfigured.
|
|
246
|
+
*/
|
|
345
247
|
async function readCrlDir(dirPath) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
return out;
|
|
248
|
+
let entries;
|
|
249
|
+
try {
|
|
250
|
+
entries = await promises.readdir(dirPath, { withFileTypes: true });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
253
|
+
throw new Error(`could not read sslcrldir "${dirPath}": ${reason}`);
|
|
254
|
+
}
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isFile()) continue;
|
|
258
|
+
out.push(await readPem("sslcrldir", path.join(dirPath, entry.name)));
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
361
261
|
}
|
|
362
262
|
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
return out;
|
|
263
|
+
* Read every regular file in an OpenSSL `$SSL_CERT_DIR` (the hashed-dir
|
|
264
|
+
* convention honoured by `sslrootcert=system`) and return their PEM bytes.
|
|
265
|
+
* Mirrors {@link readCrlDir}: subdirectories are skipped and a DER-format
|
|
266
|
+
* file is auto-converted to PEM. A failure to list the directory or read any
|
|
267
|
+
* file surfaces as `could not read SSL_CERT_DIR "<path>": <reason>`.
|
|
268
|
+
*
|
|
269
|
+
* Exported for tests.
|
|
270
|
+
*/
|
|
271
|
+
async function readCaDir(dirPath) {
|
|
272
|
+
let entries;
|
|
273
|
+
try {
|
|
274
|
+
entries = await promises.readdir(dirPath, { withFileTypes: true });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
277
|
+
throw new Error(`could not read SSL_CERT_DIR "${dirPath}": ${reason}`);
|
|
278
|
+
}
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
if (!entry.isFile()) continue;
|
|
282
|
+
out.push(await readPem("SSL_CERT_DIR", path.join(dirPath, entry.name), "CERTIFICATE"));
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
387
285
|
}
|
|
388
286
|
/**
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
socket.on("data", onData);
|
|
423
|
-
socket.on("error", onError);
|
|
424
|
-
socket.write(SSLRequest());
|
|
425
|
-
});
|
|
287
|
+
* Send SSLRequest and pull off the 1-byte server response. The byte is
|
|
288
|
+
* outside the regular framed protocol (it has no length / type header), so
|
|
289
|
+
* we can't reuse MessageParser; instead we peel off one byte and push any
|
|
290
|
+
* remainder back onto the socket via `unshift`.
|
|
291
|
+
*
|
|
292
|
+
* Exported for tests.
|
|
293
|
+
*/
|
|
294
|
+
function sendSslRequest(socket) {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const onError = (err) => {
|
|
297
|
+
cleanup();
|
|
298
|
+
reject(err);
|
|
299
|
+
};
|
|
300
|
+
const onData = (chunk) => {
|
|
301
|
+
if (chunk.length === 0) return;
|
|
302
|
+
const first = String.fromCharCode(chunk[0]);
|
|
303
|
+
if (first !== "S" && first !== "N") {
|
|
304
|
+
cleanup();
|
|
305
|
+
reject(/* @__PURE__ */ new Error(`Unexpected SSLRequest response byte 0x${chunk[0].toString(16)}`));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (chunk.length > 1) socket.unshift(chunk.subarray(1));
|
|
309
|
+
cleanup();
|
|
310
|
+
resolve(first);
|
|
311
|
+
};
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
socket.removeListener("data", onData);
|
|
314
|
+
socket.removeListener("error", onError);
|
|
315
|
+
};
|
|
316
|
+
socket.on("data", onData);
|
|
317
|
+
socket.on("error", onError);
|
|
318
|
+
socket.write(SSLRequest());
|
|
319
|
+
});
|
|
426
320
|
}
|
|
427
321
|
/**
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `DEPTH_ZERO_SELF_SIGNED_CERT`,
|
|
474
|
-
// `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED`, etc. libpq collapses
|
|
475
|
-
// them all to `certificate verify failed`.
|
|
476
|
-
const isVerifyFailure = code !== undefined &&
|
|
477
|
-
code !== "ERR_TLS_CERT_ALTNAME_INVALID" &&
|
|
478
|
-
/CERT|SIGNATURE|SELF_SIGNED|UNABLE_TO|CHAIN|EXPIRED|NOT_YET_VALID|INVALID_CA/.test(code);
|
|
479
|
-
if (isVerifyFailure || /certificate verify failed/i.test(msg)) {
|
|
480
|
-
const mapped = new Error("certificate verify failed");
|
|
481
|
-
mapped.cause = err;
|
|
482
|
-
return mapped;
|
|
483
|
-
}
|
|
484
|
-
return err;
|
|
322
|
+
* Translate a Node/OpenSSL TLS handshake error into libpq-style wording so
|
|
323
|
+
* our diagnostics match upstream psql/libpq exactly (the cases asserted by
|
|
324
|
+
* upstream `001_ssltests.pl`). Unrecognised errors pass through unchanged.
|
|
325
|
+
* The original error is preserved on `cause` for callers that introspect.
|
|
326
|
+
*
|
|
327
|
+
* - Chain-verification failures (`ERR_TLS_CERT_ALTNAME_INVALID` excluded)
|
|
328
|
+
* → `certificate verify failed` (libpq's `SSL error: certificate verify
|
|
329
|
+
* failed`).
|
|
330
|
+
* - Hostname mismatch (`ERR_TLS_CERT_ALTNAME_INVALID`, Node's
|
|
331
|
+
* "Hostname/IP does not match certificate's altnames") →
|
|
332
|
+
* `server certificate for "<host>" does not match host name "<host>"`.
|
|
333
|
+
* - Encrypted-key decrypt failures (`ERR_OSSL_BAD_DECRYPT`, or an OpenSSL
|
|
334
|
+
* message containing `bad decrypt`) → libpq's
|
|
335
|
+
* `could not load private key file "<path>": <openssl text>` shape. The
|
|
336
|
+
* raw OpenSSL text (which carries the `bad decrypt` token upstream's
|
|
337
|
+
* `001_ssltests.pl` matches on) is preserved in the message tail. When the
|
|
338
|
+
* key path is unknown the path segment is omitted but the `bad decrypt`
|
|
339
|
+
* token is still surfaced.
|
|
340
|
+
*
|
|
341
|
+
* `keyPath`, when supplied, is the configured `sslkey` path — used only to
|
|
342
|
+
* fill libpq's `private key file "<path>"` phrasing on a decrypt failure.
|
|
343
|
+
*
|
|
344
|
+
* Exported for unit tests.
|
|
345
|
+
*/
|
|
346
|
+
function mapTlsHandshakeError(err, servername, keyPath) {
|
|
347
|
+
const code = err.code;
|
|
348
|
+
const msg = err.message;
|
|
349
|
+
if (code === "ERR_OSSL_BAD_DECRYPT" || /bad decrypt/i.test(msg)) {
|
|
350
|
+
const where = keyPath !== void 0 && keyPath !== "" ? `private key file "${keyPath}"` : "private key file";
|
|
351
|
+
const mapped = /* @__PURE__ */ new Error(`could not load ${where}: ${msg}`);
|
|
352
|
+
mapped.cause = err;
|
|
353
|
+
return mapped;
|
|
354
|
+
}
|
|
355
|
+
if (code === "ERR_TLS_CERT_ALTNAME_INVALID") {
|
|
356
|
+
const host = servername ?? "";
|
|
357
|
+
const mapped = /* @__PURE__ */ new Error(`server certificate for "${host}" does not match host name "${host}"`);
|
|
358
|
+
mapped.cause = err;
|
|
359
|
+
return mapped;
|
|
360
|
+
}
|
|
361
|
+
if (code !== void 0 && code !== "ERR_TLS_CERT_ALTNAME_INVALID" && /CERT|SIGNATURE|SELF_SIGNED|UNABLE_TO|CHAIN|EXPIRED|NOT_YET_VALID|INVALID_CA/.test(code) || /certificate verify failed/i.test(msg)) {
|
|
362
|
+
const mapped = /* @__PURE__ */ new Error("certificate verify failed");
|
|
363
|
+
mapped.cause = err;
|
|
364
|
+
return mapped;
|
|
365
|
+
}
|
|
366
|
+
return err;
|
|
485
367
|
}
|
|
486
368
|
/** Pull the SNI `servername` from the TLS options for error messages. */
|
|
487
369
|
function getServername(tlsOpts) {
|
|
488
|
-
|
|
489
|
-
? tlsOpts.servername
|
|
490
|
-
: undefined;
|
|
370
|
+
return typeof tlsOpts.servername === "string" ? tlsOpts.servername : void 0;
|
|
491
371
|
}
|
|
492
|
-
function upgradeToTls(socket, tlsOpts, sslkeylogfile, keyPath,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const x509 = established.getPeerX509Certificate?.();
|
|
541
|
-
if (x509?.raw && x509.raw.length > 0) {
|
|
542
|
-
channelBindingData = createHash("sha256")
|
|
543
|
-
.update(x509.raw)
|
|
544
|
-
.digest();
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
// `detailed = true` gets us the full peer cert chain. Some
|
|
548
|
-
// Node/OpenSSL combinations leave `.raw` undefined on the legacy
|
|
549
|
-
// API when `rejectUnauthorized: false`; in that case we have to
|
|
550
|
-
// accept that channel binding is unavailable.
|
|
551
|
-
const peerCert = established.getPeerCertificate(true);
|
|
552
|
-
if (peerCert?.raw && peerCert.raw.length > 0) {
|
|
553
|
-
channelBindingData =
|
|
554
|
-
computeChannelBindingData(peerCert);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
catch {
|
|
559
|
-
// Best-effort: a missing peer cert => no channel binding. SASL
|
|
560
|
-
// path will fall back to SCRAM-SHA-256 (non-PLUS).
|
|
561
|
-
channelBindingData = null;
|
|
562
|
-
}
|
|
563
|
-
resolve({
|
|
564
|
-
kind: "tls",
|
|
565
|
-
socket: established,
|
|
566
|
-
channelBindingData,
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
catch (err) {
|
|
571
|
-
reject(mapTlsHandshakeError(err instanceof Error ? err : new Error(String(err)), getServername(tlsOpts), keyPath));
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
tlsSocket.on("error", onError);
|
|
575
|
-
// libpq `sslkeylogfile`: append each emitted key-log line so the
|
|
576
|
-
// handshake can be decrypted offline. The path was pre-checked for
|
|
577
|
-
// writability in loadTlsFileOptions.
|
|
578
|
-
if (sslkeylogfile !== undefined && sslkeylogfile !== "") {
|
|
579
|
-
attachKeyLogListener(tlsSocket, sslkeylogfile);
|
|
580
|
-
}
|
|
581
|
-
});
|
|
372
|
+
function upgradeToTls(socket, tlsOpts, sslkeylogfile, keyPath, requireAlpn = false) {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
let tlsSocket;
|
|
375
|
+
const cleanup = () => {
|
|
376
|
+
tlsSocket?.removeListener("error", onError);
|
|
377
|
+
};
|
|
378
|
+
const onError = (err) => {
|
|
379
|
+
cleanup();
|
|
380
|
+
reject(mapTlsHandshakeError(err, getServername(tlsOpts), keyPath));
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
tlsSocket = tls.connect({
|
|
384
|
+
...tlsOpts,
|
|
385
|
+
socket
|
|
386
|
+
}, () => {
|
|
387
|
+
cleanup();
|
|
388
|
+
const established = tlsSocket;
|
|
389
|
+
if (established === void 0) return;
|
|
390
|
+
if (requireAlpn && established.alpnProtocol !== "postgresql") {
|
|
391
|
+
cleanup();
|
|
392
|
+
established.destroy();
|
|
393
|
+
reject(/* @__PURE__ */ new Error("direct SSL connection requires ALPN, but the server did not negotiate the \"postgresql\" protocol"));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let channelBindingData = null;
|
|
397
|
+
try {
|
|
398
|
+
const x509 = established.getPeerX509Certificate?.();
|
|
399
|
+
if (x509?.raw && x509.raw.length > 0) channelBindingData = createHash("sha256").update(x509.raw).digest();
|
|
400
|
+
else {
|
|
401
|
+
const peerCert = established.getPeerCertificate(true);
|
|
402
|
+
if (peerCert?.raw && peerCert.raw.length > 0) channelBindingData = computeChannelBindingData(peerCert);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
channelBindingData = null;
|
|
406
|
+
}
|
|
407
|
+
resolve({
|
|
408
|
+
kind: "tls",
|
|
409
|
+
socket: established,
|
|
410
|
+
channelBindingData
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
} catch (err) {
|
|
414
|
+
reject(mapTlsHandshakeError(err instanceof Error ? err : new Error(String(err)), getServername(tlsOpts), keyPath));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
tlsSocket.on("error", onError);
|
|
418
|
+
if (sslkeylogfile !== void 0 && sslkeylogfile !== "") attachKeyLogListener(tlsSocket, sslkeylogfile);
|
|
419
|
+
});
|
|
582
420
|
}
|
|
583
421
|
/**
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
422
|
+
* Wire a TLSSocket's `'keylog'` event to append each emitted key-log line to
|
|
423
|
+
* `filePath`. Node emits one already-newline-terminated Buffer per event. A
|
|
424
|
+
* write that fails after the pre-check (e.g. the directory was removed
|
|
425
|
+
* mid-session) is re-emitted as a socket `'error'` rather than crashing the
|
|
426
|
+
* process.
|
|
427
|
+
*
|
|
428
|
+
* Accepts a minimal event-emitter shape so it can be unit-tested against a
|
|
429
|
+
* fake socket without a real TLS handshake. Exported for tests.
|
|
430
|
+
*/
|
|
431
|
+
function attachKeyLogListener(socket, filePath) {
|
|
432
|
+
socket.on("keylog", (line) => {
|
|
433
|
+
try {
|
|
434
|
+
appendFileSync(filePath, line);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
socket.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
437
|
+
}
|
|
438
|
+
});
|
|
602
439
|
}
|
|
440
|
+
//#endregion
|
|
441
|
+
export { attachKeyLogListener, computeChannelBindingData, loadTlsFileOptions, mapTlsHandshakeError, negotiateTls, readCaDir, sendSslRequest };
|