Skip to content

Commit a57e99f

Browse files
committed
feat(extensions): a verifier for Open VSX signatures, with the registry's key pinned
Every LevelCode release so far refuses every signed extension from Open VSX: Cannot install '…' extension because LevelCode cannot verify the extension signature. Signature verification was not executed. and, with no dialog at all, fails to update any installed extension at every start. The editor verifies a download by loading a module named @vscode/vsce-sign and calling its verify(). That module is Microsoft's — closed, licensed for use "only with" Microsoft's own products — so it is not in Code-OSS and not in any build made from it. With nothing to load the editor cannot verify, and a built app that cannot verify refuses. This is the module LevelCode will load in that slot (modules/extension-signature). It is not in a build yet; the next commit puts it there. What it verifies. Open VSX signs each package: beside every .vsix it serves an archive holding .signature.sig, a 64-byte Ed25519 signature over the .vsix bytes. verify() accepts a package only if a key it trusts made that signature over exactly those bytes. Everything else is a refusal, as one of the result codes the editor already has words for — never an exception. The key is pinned, not fetched. Open VSX serves its public key too, and the existing open-source verifier downloads it by default, which asks the server being checked for the answer. The keys this module trusts are in keys.json beside it and are read from nowhere else. The one key there was checked before it went in: every one of 762 extensions sampled on 2026-10-04 (the newest, the oldest back to 2020, the most and the least downloaded) names it, none is unsigned, and the signatures of 64 of them — 87 MB of packages — verify with it. What a pass means: the file is what Open VSX published. Not that the publisher signed it, and not that the extension is safe. The archive is parsed before anything about it is known, so the reader takes nothing on trust. Every offset and length is checked against the buffer; the signature must be 64 bytes before it is read; nothing inflates past what it declares; zip64, encrypted and multi-disk archives, other compression methods and a second signature entry are refused rather than handled. The manifest in the archive is not signed. It is consulted only to word a refusal — a damaged download reads PackageIntegrityCheckFailed, an intact package under an unknown key reads Untrusted — and can never lift one. Plain Node, no dependencies, 300 lines. The signature check runs off the event loop; the editor's shared process has other work. Tests: modules/extension-signature/test/, 31 cases. Keys are generated and archives written by hand, so each lie an archive can tell is told on purpose. Every prefix of an archive, and every archive with one byte changed, is put to the reader. Two real packages from Open VSX (MIT; attributed in NOTICE and in the fixtures' README) show that the pinned key verifies what the registry really serves, and the shipped verifier is run with every socket refused. scripts/test-extensions.sh now discovers modules/*/test/*.test.js too. The module's tests cannot live under extensions/, whose test folders ship in the app. 45 mutations of the module and its keys: 43 fail a case. The two that survive are the two catch-alls that turn an exception nobody expects into a refusal; every read is bounds-checked before it is made, so nothing is known to reach them. Two checks the mutations showed to decide nothing were removed instead of tested. 50 suites pass on macOS (Node 24) and in a Linux container (Node 18).
1 parent f8b6193 commit a57e99f

11 files changed

Lines changed: 906 additions & 7 deletions

‎NOTICE‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ LevelCode is not produced by, endorsed by, or affiliated with Microsoft.
1212
"Visual Studio Code" and related marks are trademarks of Microsoft.
1313
The upstream Code-OSS source is fetched at build time and is not redistributed
1414
in this repository.
15+
16+
modules/extension-signature/test/fixtures/ holds two extension packages as
17+
published on Open VSX (https://open-vsx.org), unmodified, used only as test
18+
data and not shipped in the app:
19+
- git-rebase-syntax 0.0.1, MIT License, Copyright (c) 2020 Jérome Perrin
20+
- systemd-unit-file 1.0.6, MIT License, Copyright (c) 2018 Takashi Oguma
21+
Each package carries its own licence text; see the README in that folder.

‎modules/extension-signature/index.js‎

Lines changed: 309 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"_comment": "The Open VSX signing keys this build of LevelCode trusts. A signature is accepted only if one of these made it; the key the registry serves over the network is never used. Adding or removing a key is a security decision, not a config tweak: docs/EXTENSION-SIGNATURES.md says how a key is checked before it goes in here and what to do when Open VSX changes its key. `id` is the registry's own name for the key (the last segment of the public-key URL); `publicKey` is the key itself, base64 of its SPKI DER — the body of the PEM the registry serves; `registry` is the gallery these keys are for, and must be the one branding/product.overlay.json points the editor at. The other fields are provenance for a reader and are not used by the code.",
3+
4+
"keys": [
5+
{
6+
"id": "14ccb407-4e79-41ed-be5a-6d608325c45a",
7+
"publicKey": "MCowBQYDK2VwAyEAje+vAaSS1zHV5WHCJSa5UXvxRo6+yerEU3IEmtuEuF4=",
8+
"registry": "https://open-vsx.org",
9+
"source": "https://open-vsx.org/api/-/public-key/14ccb407-4e79-41ed-be5a-6d608325c45a",
10+
"pinned": "2026-10-04",
11+
"evidence": "Fetched over TLS from open-vsx.org on 2026-10-04. Named as the signing key by every one of 762 extensions sampled that day — the newest, the oldest (back to March 2020), the most and the least downloaded; none was unsigned and none named another key. The signatures of 64 of them, 87 MB of packages published between 2020 and that day, were verified against the downloaded files with this key."
12+
}
13+
]
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@vscode/vsce-sign",
3+
"version": "0.0.0-levelcode",
4+
"private": true,
5+
"description": "LevelCode's extension signature verifier for Open VSX. It stands where the editor looks for Microsoft's module of this name, which only Microsoft's products may use and which no build from the open source contains. See index.js.",
6+
"license": "MIT",
7+
"main": "index.js",
8+
"levelcode": "modules/extension-signature"
9+
}

‎modules/extension-signature/test/extensionSignature.test.js‎

Lines changed: 515 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Fixtures: real packages from Open VSX
2+
3+
Two extension packages exactly as [Open VSX](https://open-vsx.org) serves them, each with the
4+
signature archive the registry serves beside it. They are here so the suite can show, offline, that
5+
the key LevelCode pins verifies what the registry really publishes — and that one changed byte is
6+
refused. `scripts/extension-signature.mjs check` uses the first pair for the same purpose on a built app.
7+
8+
They are other people's work, redistributed unmodified under their own licences. Each package
9+
carries its licence text inside (`extension/LICENCE`, `extension/LICENSE.txt`).
10+
11+
| File | From | Licence | Copyright |
12+
| --- | --- | --- | --- |
13+
| `perrinjerome.git-rebase-syntax-0.0.1.vsix` + `.sigzip` | https://open-vsx.org/extension/perrinjerome/git-rebase-syntax · https://github.com/perrinjerome/git-rebase-syntax | MIT | © 2020 Jérome Perrin |
14+
| `coolbear.systemd-unit-file-1.0.6.vsix` + `.sigzip` | https://open-vsx.org/extension/coolbear/systemd-unit-file · https://github.com/bearmini/vscode-systemd-unit-file | MIT | © 2018 Takashi Oguma |
15+
16+
Chosen for being small, old, plainly licensed, and unrelated to LevelCode. Downloaded 2026-10-04 and
17+
signed by Open VSX key `14ccb407-4e79-41ed-be5a-6d608325c45a`.
18+
19+
## SHA-256
20+
21+
The suite checks the files against this list.
22+
23+
```
24+
a5417f80c1d595651c36ea09deff10a4a32f18920d109e07ed8f21ab986632ce perrinjerome.git-rebase-syntax-0.0.1.vsix
25+
a5d0c14abab355c71d9c7cf589a5ac1666fdd24a1614a19a7cd1d8ddab7b9af7 perrinjerome.git-rebase-syntax-0.0.1.sigzip
26+
b707f0152c9f00274a88163586a07e4e062065f8edee3a1dda9c1f925f283319 coolbear.systemd-unit-file-1.0.6.vsix
27+
2784cc4e40f9bb1b3e09aa9ee52a1d676017fa8055517b680b9a1793de20d59b coolbear.systemd-unit-file-1.0.6.sigzip
28+
```
29+
30+
## Replacing them
31+
32+
A fixture is replaced only when Open VSX changes its signing key and the old key is being dropped
33+
from `keys.json` (`docs/EXTENSION-SIGNATURES.md`): the registry signs every package again with the
34+
new key, and these archives, made with the old one, stop verifying. Download both files of a
35+
package again, update the list above, and run the suite.
36+
37+
```bash
38+
cd modules/extension-signature/test/fixtures
39+
for f in perrinjerome/git-rebase-syntax/0.0.1/file/perrinjerome.git-rebase-syntax-0.0.1 \
40+
coolbear/systemd-unit-file/1.0.6/file/coolbear.systemd-unit-file-1.0.6; do
41+
curl -fsSLO "https://open-vsx.org/api/$f.vsix" && curl -fsSLO "https://open-vsx.org/api/$f.sigzip"
42+
done
43+
shasum -a 256 *.vsix *.sigzip
44+
```
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)