Skip to content

Commit 5c9bce0

Browse files
committed
fix(registry): #103 review — a redirect is followed by hand, and only to where the registry keeps its files
Two findings from the review. One was right, and wider than it said. The other was a true sentence in the doc that nobody could check from this repository. 1. The registry check went wherever it was redirected. It refused to START a request anywhere but the registry, and then handed the request to fetch(), which follows a redirect anywhere. The comment beside it — "only addresses on the registry itself are followed" — was not true, and not only under attack: Open VSX answers every download with a 302 to its content host, so on every ordinary run each package and each signature archive came from openvsx.eclipsecontent.org, an origin the check had never been told about. Watching the reviewed code against the real registry shows it: of five requests sent to open-vsx.org, four were answered by the other host. "Reject redirects", the first remedy offered, would have left the check unable to download anything. Redirects are followed by hand instead (redirect: 'manual'): each destination is checked BEFORE anything is sent to it, and passes only if it is the registry itself or a host named in CONTENT_ORIGINS — one entry, the content host — at most three redirects deep. The list is written down rather than learned from the answer, for the reason the key is: a host the check is merely told about is a host anyone who can answer for the registry could choose. The price is pinning's price, paid the same way. The day Open VSX moves its files, nothing changes for users and this check can no longer download anything. It then says where the registry redirects and what to change: "could not check", which is a warning in the release gate and a failed run in the daily watch after three tries. docs/EXTENSION-SIGNATURES.md has a section for that day. Nothing caught this because the stand-in registry in the suite served files directly: it did not behave like the registry it stood in for. It now answers a download with a redirect to a content host, as Open VSX does, and like fetch() it follows a redirect itself unless told not to — so a caller that forgets to say so is seen going where it is sent. 2. "The editor checks [.signature.p7s] exists." It does, but not in any code in this repository, and the verifier here does not; the reviewer could see only the half that says no such thing. The editor's downloader looks for the entry before it hands an archive to any verifier (downloadSignatureArchive in extensionDownloader.ts, Code-OSS 1.126) and discards an archive without it as a failed download. The doc now says who checks, where, and that the module does not read the entry; a case in the suite pins that an archive without it is not the module's to refuse. Nothing changed in behaviour for this one. A limit it brought out is written down too: the daily watch verifies with the module, so what only the editor asks of an archive is covered by `smoke`, at release time. Run against open-vsx.org after the change: every request leaves in manual mode and is answered by the host it was sent to; status ok, two packages verified. With the content host taken off the list: "could not check", the message names openvsx.eclipsecontent.org, and only open-vsx.org was contacted. Tests: 56 to 59 cases. The three new ones fail on the reviewed script; the other 56 pass on it. 19 mutations of the new code each fail a case. 50 suites pass on macOS (Node 24) and in a Linux container (Node 18).
1 parent d4d58f2 commit 5c9bce0

5 files changed

Lines changed: 241 additions & 32 deletions

File tree

‎CLAUDE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ module for that slot. Full account + runbooks: `docs/EXTENSION-SIGNATURES.md`.
104104
temp folders). It is the check that would have caught the original bug; CI runs it after each build.
105105
- **`registry`** watches for Open VSX changing its key (daily workflow + release gate). Exit 1 means
106106
evidence, and every shipped build is refusing extensions until a release carries the new key.
107+
It asks the registry and the one host the registry redirects downloads to (`CONTENT_ORIGINS`),
108+
following redirects by hand; a redirect anywhere else is not followed, and it says so.
107109
- **Do not "fix" a refusal by turning verification off** (`extensions.verifySignature`, a patch like
108110
VSCodium's). And a run from source proves nothing here: the editor only enforces on a built app.
109111
- Its tests live in `modules/extension-signature/test/` — outside `extensions/`, whose `test/` folders

‎docs/EXTENSION-SIGNATURES.md‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Extension signatures
22

33
How LevelCode verifies the extensions it installs from Open VSX, what that does and does not
4-
prove, and what to do on the two days it will need attention: when Open VSX changes its signing
5-
key, and when Code-OSS is bumped.
4+
prove, and what to do on the days it will need attention: when Open VSX changes its signing key
5+
or moves its files, and when Code-OSS is bumped.
66

77
## What was wrong
88

@@ -30,7 +30,7 @@ Open VSX signs every package it serves. Next to each `.vsix` is a signature arch
3030
| --- | --- |
3131
| `.signature.sig` | A 64-byte Ed25519 signature over the bytes of the `.vsix` |
3232
| `.signature.manifest` | JSON: the size and SHA-256 of the package and of each file in it. Not signed. |
33-
| `.signature.p7s` | Empty. The slot Microsoft's format keeps its signature in; the editor checks it exists. |
33+
| `.signature.p7s` | Empty. It is the slot Microsoft's format keeps its signature in. The editor requires the entry to be there: its downloader looks for it before handing an archive to any verifier (`downloadSignatureArchive` in `extensionDownloader.ts`), and discards an archive without it as a failed download. The module does not read it. There is nothing in it to check, and an archive without it never reaches the module in the app. |
3434

3535
`modules/extension-signature` is LevelCode's own module for the slot the editor loads from. It
3636
reads `.signature.sig`, and accepts the package only if `crypto.verify` says one of the keys it
@@ -94,7 +94,13 @@ All four commands are `node scripts/extension-signature.mjs <command>`.
9494
`Extension signature verification result … Success. Executed: true`. No window opens and
9595
nothing of an installed LevelCode is touched. Run against 1.3.1 it fails with "not executed",
9696
which is the bug.
97-
- **`registry`** is described below.
97+
- **`registry`** asks Open VSX which key its newest extensions name, then downloads a couple of
98+
them and verifies them with the pinned keys. Every request starts at the registry. The registry
99+
answers a download with a redirect to the host it keeps its files on, so that redirect is
100+
followed, by hand: each destination is checked before anything is sent to it, and only the
101+
registry and the hosts named in `CONTENT_ORIGINS` (in the script; today
102+
`openvsx.eclipsecontent.org`) pass. What its answers mean is under "When Open VSX changes its
103+
key" and "When Open VSX moves its files".
98104

99105
`<app-code-folder>` is `LevelCode.app/Contents/Resources/app`.
100106

@@ -218,6 +224,38 @@ with `--strict`.
218224
If Open VSX has stopped signing altogether, there is no key to add. That is a decision, not a
219225
runbook step: ship with verification relaxed, or wait. Raise it before doing either.
220226

227+
## When Open VSX moves its files
228+
229+
Open VSX does not serve a package from `open-vsx.org`. Asked for one, it answers with a redirect
230+
to its content host, today `openvsx.eclipsecontent.org`. The editor follows such a redirect
231+
wherever it leads, and for the editor that is safe: whatever arrives is verified before it is
232+
installed. `registry` is stricter. It runs on a release runner, the address it would be sent to
233+
comes from the server it is checking, and it has no reason to go anywhere it was not told about.
234+
It follows a redirect only to the registry itself or to a host listed in `CONTENT_ORIGINS` in
235+
`scripts/extension-signature.mjs`, at most three redirects deep.
236+
237+
So the day Open VSX moves its files, nothing changes for users, and `registry` can no longer
238+
download anything to verify. It says so, and names the host:
239+
240+
```
241+
could not check: none of the 30 newest signed extensions on https://open-vsx.org could be
242+
downloaded to verify: https://open-vsx.org redirects to https://<new host>, which is neither
243+
the registry nor a host it is known to keep its files on. …
244+
```
245+
246+
In the release gate that is a warning and the release goes on. In the daily workflow it fails
247+
the run after three tries, on purpose: a watch that cannot see would let a changed key pass.
248+
249+
What to do: make sure the new host is Open VSX's — the same places as step 1 above, and the
250+
`location` of a download asked for from two networks:
251+
252+
```bash
253+
curl -sI https://open-vsx.org/api/perrinjerome/git-rebase-syntax/0.0.1/file/perrinjerome.git-rebase-syntax-0.0.1.vsix
254+
```
255+
256+
Then add it to `CONTENT_ORIGINS`, and take the old host off once downloads no longer go there.
257+
Nothing ships to users for this: the list is used by this check and by nothing in the app.
258+
221259
## When Code-OSS is bumped
222260

223261
The module depends on four things in the editor, none of which LevelCode controls:
@@ -261,5 +299,8 @@ go, not be kept beside it.
261299
it cannot be streamed; Open VSX's own signer has the same constraint. A 300 MB extension costs
262300
300 MB for a moment. The signature check itself runs off the event loop.
263301
- **The keys are only as good as the day they were pinned.** See "The pinned key".
302+
- **`registry` checks signatures with the module, not through the editor.** Something only the
303+
editor asks of an archive — that the empty `.signature.p7s` is in it — is covered by `smoke`,
304+
which runs at release time, and not by the daily watch.
264305
- **macOS only today.** The module is plain JavaScript; a Linux or Windows build would run the
265306
same `install` and `check` against its own code folder.

‎modules/extension-signature/index.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
* Open VSX signs each extension file with an Ed25519 key and serves the signature in an archive
1616
* beside it: `.signature.sig` (the 64-byte signature over the .vsix bytes), `.signature.manifest`
1717
* (sizes and digests, of the package and of every file in it) and an empty `.signature.p7s`, the
18-
* slot Microsoft's format uses. The check is one call — crypto.verify over the whole file — and it
19-
* passes only for a file the registry signed, byte for byte.
18+
* slot Microsoft's format uses. (The editor's downloader insists that entry is present before it
19+
* calls any verifier; this module does not read it.) The check is one call — crypto.verify over
20+
* the whole file — and it passes only for a file the registry signed, byte for byte.
2021
*
2122
* THE KEY IS NOT ASKED FOR, IT IS KNOWN
2223
*

0 commit comments

Comments
 (0)