Skip to content

Commit 5790e24

Browse files
committed
feat(runtime): add VP_NODE_SKIP_SIGNATURE_VERIFY escape hatch
Allow skipping PGP signature verification of SHASUMS256.txt via the VP_NODE_SKIP_SIGNATURE_VERIFY env var, so a future keyring or certificate issue can be temporarily bypassed without blocking installs. The SHA-256 checksum is still verified (integrity preserved, only authenticity dropped), and a warning is printed on every skipped install. Mirrors asdf's NODEJS_CHECK_SIGNATURES and mise's signature opt-out; env-var only, no config or CI flag, so the secure path stays the unconditional default.
1 parent 7c7a9fb commit 5790e24

5 files changed

Lines changed: 67 additions & 1 deletion

File tree

crates/vite_js_runtime/src/runtime.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ async fn resolve_shasums_content(
127127
signature: Option<&ShasumsSignature>,
128128
archive_filename: &str,
129129
) -> Result<String, Error> {
130+
// Escape hatch: skip PGP verification entirely (e.g. to work around a stale
131+
// keyring or a signature-fetch failure). The SHA-256 checksum still runs, so
132+
// this only drops authenticity, not integrity. Warn loudly, including in CI,
133+
// since it is a deliberate security downgrade the operator opted into.
134+
if vite_shared::EnvConfig::get().node_skip_signature_verify {
135+
vite_shared::output::warn(&format!(
136+
"PGP signature verification skipped for {archive_filename} \
137+
(VP_NODE_SKIP_SIGNATURE_VERIFY set); verifying SHA-256 checksum only"
138+
));
139+
return download_text(plain_url).await;
140+
}
130141
let Some(signature) = signature else {
131142
// No signature is published for this source (e.g. unofficial musl builds).
132143
log_checksum_only(archive_filename);

crates/vite_shared/src/env_config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ pub struct EnvConfig {
6868
/// Env: `VP_NODE_DIST_MIRROR`
6969
pub node_dist_mirror: Option<String>,
7070

71+
/// Skip PGP signature verification of the Node.js `SHASUMS256.txt` (escape
72+
/// hatch); the SHA-256 checksum is still verified.
73+
///
74+
/// Env: `VP_NODE_SKIP_SIGNATURE_VERIFY`
75+
pub node_skip_signature_verify: bool,
76+
7177
/// Whether running in a CI environment.
7278
///
7379
/// Env: `CI`
@@ -132,6 +138,8 @@ impl EnvConfig {
132138
.trim_end_matches('/')
133139
.to_string(),
134140
node_dist_mirror: std::env::var(env_vars::VP_NODE_DIST_MIRROR).ok(),
141+
node_skip_signature_verify: std::env::var(env_vars::VP_NODE_SKIP_SIGNATURE_VERIFY)
142+
.is_ok(),
135143
is_ci: std::env::var("CI").is_ok(),
136144
bypass_shim: std::env::var(env_vars::VP_BYPASS).is_ok(),
137145
debug_shim: std::env::var(env_vars::VP_DEBUG_SHIM).is_ok(),
@@ -218,6 +226,7 @@ impl EnvConfig {
218226
vite_plus_home: None,
219227
npm_registry: "https://registry.npmjs.org".into(),
220228
node_dist_mirror: None,
229+
node_skip_signature_verify: false,
221230
is_ci: false,
222231
bypass_shim: false,
223232
debug_shim: false,
@@ -269,6 +278,7 @@ mod tests {
269278
assert_eq!(config.npm_registry, "https://registry.npmjs.org");
270279
assert!(!config.is_ci);
271280
assert!(!config.bypass_shim);
281+
assert!(!config.node_skip_signature_verify);
272282
}
273283

274284
#[test]

crates/vite_shared/src/env_vars.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ pub const NPM_CONFIG_REGISTRY_UPPER: &str = "NPM_CONFIG_REGISTRY";
2727
/// Node.js distribution mirror URL for downloads.
2828
pub const VP_NODE_DIST_MIRROR: &str = "VP_NODE_DIST_MIRROR";
2929

30+
/// Skip PGP signature verification of `SHASUMS256.txt` when set (escape hatch).
31+
/// The SHA-256 checksum is still verified.
32+
pub const VP_NODE_SKIP_SIGNATURE_VERIFY: &str = "VP_NODE_SKIP_SIGNATURE_VERIFY";
33+
3034
/// Override Node.js version (takes highest priority in version resolution).
3135
pub const VP_NODE_VERSION: &str = "VP_NODE_VERSION";
3236

docs/guide/env.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,15 @@ VP_NODE_DIST_MIRROR=https://my-mirror.example.com/nodejs/dist vp env default lts
153153
# Set it permanently in your shell profile (.bashrc, .zshrc, etc.)
154154
echo 'export VP_NODE_DIST_MIRROR=https://my-mirror.example.com/nodejs/dist' >> ~/.zshrc
155155
```
156+
157+
## Node.js Signature Verification
158+
159+
When installing Node.js from the official `nodejs.org` distribution, Vite+ downloads the PGP-signed `SHASUMS256.txt.asc` and verifies it against the bundled Node.js release keys before trusting any checksum. This protects against a tampered `SHASUMS256.txt` paired with a matching malicious archive. The SHA-256 checksum of the downloaded archive is always verified afterward.
160+
161+
Custom mirrors (`VP_NODE_DIST_MIRROR`) that publish only the plain `SHASUMS256.txt` fall back to checksum-only verification. A mirror that does publish a `.asc` still has its signature verified, and an invalid signature is a hard error.
162+
163+
If a future keyring or certificate issue blocks downloads, set `VP_NODE_SKIP_SIGNATURE_VERIFY` to temporarily bypass PGP verification. The SHA-256 checksum is still verified, and Vite+ prints a warning when the signature check is skipped:
164+
165+
```bash
166+
VP_NODE_SKIP_SIGNATURE_VERIFY=1 vp env install 22
167+
```

rfcs/verify-node-shasums-signature.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ practice (mise has a recurring stream of GPG key-import/encoding failures).
6868
Vite+ is the only one that verifies the signature in-process with no external
6969
`gpg`. That is what justifies bundling rPGP and the keyring (~1.2 MiB added to
7070
the `vp` binary): it brings the strongest guarantee available while keeping the
71-
zero-dependency, cross-platform install that `vp` requires.
71+
zero-dependency, cross-platform install that `vp` requires. Like asdf
72+
(`NODEJS_CHECK_SIGNATURES`) and mise, Vite+ also exposes an opt-out
73+
(`VP_NODE_SKIP_SIGNATURE_VERIFY`) for when key/certificate issues would
74+
otherwise block installs; see [Escape hatch](#escape-hatch-skipping-signature-verification).
7275

7376
[asdf-nodejs]: https://github.com/asdf-vm/asdf-nodejs
7477
[mise]: https://mise.jdx.dev/
@@ -174,6 +177,24 @@ The signature fetch/verify stays outside the content-integrity retry loop, like
174177
the existing SHASUMS fetch/parse: signature failures are permanent, and
175178
`download_text` already retries the network layer.
176179

180+
### Escape hatch: skipping signature verification
181+
182+
Setting `VP_NODE_SKIP_SIGNATURE_VERIFY` (to any value) skips PGP verification
183+
entirely and uses the plain `SHASUMS256.txt`, regardless of `required`. This is
184+
a deliberate opt-out for the case where a future keyring or certificate problem
185+
would otherwise block installs, mirroring asdf's `NODEJS_CHECK_SIGNATURES=no`
186+
and mise's `verify_checksums`/signature settings. Two properties keep it from
187+
silently weakening the install:
188+
189+
- The SHA-256 checksum of the archive against `SHASUMS256.txt` is still
190+
verified, so integrity (archive matches its SHASUMS) is preserved; only
191+
authenticity (the SHASUMS came from a Node.js releaser) is dropped.
192+
- It prints a warning on every skipped install so the weaker mode is visible in
193+
logs, not silent.
194+
195+
It is an env-var only switch: there is no config-file or CI flag, keeping the
196+
secure path the unconditional default.
197+
177198
## Trust model and limitations
178199

179200
The trust boundary is the curated set of Node.js release keys plus honoring key
@@ -212,6 +233,14 @@ before the trust anchor is updated, and PR CI (the `vite_js_runtime` tests)
212233
confirms every vendored key still parses. The same script can be run locally to
213234
refresh the keyring on demand.
214235

236+
### Operator opt-out
237+
238+
`VP_NODE_SKIP_SIGNATURE_VERIFY` (see [Escape hatch](#escape-hatch-skipping-signature-verification))
239+
lets an operator lower the trust boundary to integrity-only (SHA-256 against
240+
`SHASUMS256.txt`), forgoing authenticity. It is off by default, requires an
241+
explicit env var, and warns on every skipped install, so the weaker mode is an
242+
opt-in choice rather than a silent default.
243+
215244
### `rsa` advisory
216245

217246
Pulling in `pgp` transitively adds the `rsa` crate, which carries

0 commit comments

Comments
 (0)