Skip to content

Commit 5da84f0

Browse files
committed
build: the app ships the signature verifier — installed, checked, and asked directly
The module from the previous commit does nothing until a built app can load it. scripts/extension-signature.mjs puts it there, and proves it works. install <app-code-folder> Copies three files to node_modules/@vscode/vsce-sign in the BUILT app: the name the editor imports. Nothing in the editor's source is patched, so there is nothing to re-apply on a Code-OSS bump — the same move as strip-proprietary.mjs. build-macos.sh passes --replace: the checkout decides what a build ships. make-dmg.sh does not: an app that already carries a LevelCode verifier keeps it, because the keys an app trusts are its build's and not the signer's. A module of that name that is not ours is an error. check <app-code-folder> Fails unless the built editor still names @vscode/vsce-sign, the name resolves to our module from the very bundles that name it, and the module — imported by that name from those folders, in a process of its own — accepts a real Open VSX package and refuses it with one byte changed. Both build-macos.sh and make-dmg.sh run it: an upstream change that would bring "not executed" back fails the build, and an app that cannot verify is not signed. smoke <LevelCode.app> Asks the app. Its own command line installs one four-kilobyte extension into throwaway folders, as a command-line process with no window, and the app's log must say the signature verified. release.yml runs it on each arch after the build. It fails only on the app's own words; a registry that cannot be reached is a warning. registry The watch that pinning needs. The day Open VSX signs with another key, every LevelCode already installed refuses what the new key signs, until a release carries it. This asks the registry which key its newest extensions name and verifies some of them for real. Exit 1 is evidence: a key that is not pinned, signatures that stopped verifying, or no signatures at all. Not reaching the registry is a warning, or exit 2 with --strict. release.yml runs it in the test gate, before the hour of macOS build. docs/EXTENSION-SIGNATURES.md is the whole account: what a pass means, what a user sees for each refusal, how the pinned key was checked, and the runbook for the two days this will need attention — Open VSX changing its key, and a Code-OSS bump. The limits are listed there: "Install Anyway" and extensions.verifySignature are still upstream's; the dialog shows only the result code, with the reason in the log at trace level; its "Learn More" still opens Microsoft's page. Checked against the real thing, short of a rebuild. The code folder of the shipped 1.3.1 app was copied and run by the installed executable: as shipped check fails; smoke fails with "not executed" — the bug after install check passes; smoke passes; Claude Code, EditorConfig and GitLens install, each with "Success. Executed: true" in the app's own log another key pinned check fails; smoke fails with 'Untrusted'; nothing is installed and `registry` against open-vsx.org itself: all 30 newest extensions name the pinned key, and two were downloaded and verified. NOT checked: a gulp build with this step in it, and the smoke step on a GitHub runner. The step fails a build only when the app itself reports a signature failure; anything else it cannot make sense of is a warning. Tests: the suite goes from 31 to 56 cases. The app, the registry and the network are stand-ins there, and what the script does with their answers is what is pinned. 63 mutations of the script, the two shell scripts, the workflow, the fixtures and the runbook's heading: each fails a case. 50 suites pass on macOS (Node 24) and in a Linux container (Node 18).
1 parent a57e99f commit 5da84f0

10 files changed

Lines changed: 1271 additions & 10 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,16 @@ jobs:
4141
node-version: "24"
4242
- name: Extension unit tests
4343
# The gate itself lives in a script, so the pull-request workflow (test.yml) runs exactly what
44-
# this does. It discovers every extensions/*/test/*.test.js and fails if it finds none.
44+
# this does. It discovers every extensions/*/test/*.test.js and modules/*/test/*.test.js, and
45+
# fails if it finds none.
4546
run: ./scripts/test-extensions.sh
47+
- name: Open VSX still signs with a key this release trusts
48+
# The app verifies extensions against Open VSX's signing key, and the key is pinned in the app
49+
# (modules/extension-signature/keys.json). If the registry has moved to another key, the app
50+
# about to be built could not install or update one extension: stop here, an hour early.
51+
# Fails only on evidence — a registry that cannot be reached is a warning. What to do when it
52+
# fails: docs/EXTENSION-SIGNATURES.md.
53+
run: node scripts/extension-signature.mjs registry
4654

4755
build:
4856
name: Build ${{ matrix.arch }}
@@ -85,6 +93,12 @@ jobs:
8593
VSCODE_TAG: ${{ github.event.inputs.vscode_tag }}
8694
- name: Build LevelCode.app (${{ matrix.arch }}, proprietary stripped)
8795
run: ./scripts/build-macos.sh ${{ matrix.arch }}
96+
- name: The built app installs an extension from Open VSX
97+
# The one check that asks the app itself. Its own command line installs one four-kilobyte
98+
# extension into throwaway folders, and its log must show the signature verified. Every
99+
# LevelCode release before this step existed failed it: none could verify an extension.
100+
# Fails only on the app's own words; a registry that cannot be reached is a warning.
101+
run: node scripts/extension-signature.mjs smoke "VSCode-darwin-${{ matrix.arch }}/LevelCode.app"
88102
- name: Zip the unsigned app
89103
run: ditto -c -k --sequesterRsrc --keepParent "VSCode-darwin-${{ matrix.arch }}/LevelCode.app" "UNSIGNED-LevelCode-${{ matrix.arch }}.app.zip"
90104
- name: Upload app artifact

‎CLAUDE.md‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ lives in the tracked repo and is re-applied onto a clean clone:
1818
| `extensions/levelcode-npp-pack/`, `extensions/levelcode-ai/` | `vscode/extensions/*` | `apply-branding.mjs` (copy) |
1919
| `branding/product.overlay.json`, `branding/icons/` | `vscode/product.json`, `vscode/resources/...` | `apply-branding.mjs` (merge/copy) |
2020
| `patches/levelcode-core.patch` | edits to `vscode/src/**`, `vscode/build/**` | `bootstrap.sh` (`git apply`) |
21+
| `modules/extension-signature/` | the BUILT app's `node_modules/@vscode/vsce-sign` (never `vscode/`) | `extension-signature.mjs install` (from `build-macos.sh`, `make-dmg.sh`) |
2122

2223
So: **edit extensions in `extensions/…`, not `vscode/extensions/…`.** After editing, run
2324
`./scripts/run-dev.sh` (it syncs canonical → checkout first) to test.
@@ -39,8 +40,9 @@ extensions/levelcode-themes/ signature One Dark / One Light themes (JSON, de
3940
extensions/levelcode-hackability/ user init script + Atom/NPP keymap presets + package generator & hot-reload dev loader
4041
extensions/levelcode-sync/ 'levelcode' auth provider that lights up the built-in Settings Sync (LevelCode Sync, S0)
4142
extensions/levelcode-updater/ notify-only update checker (polls the update feed; never auto-applies)
43+
modules/extension-signature/ Open VSX signature verifier the built app loads as @vscode/vsce-sign, + the signing keys it trusts
4244
patches/levelcode-core.patch our core source edits, applied on bootstrap
43-
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, editor-identity.mjs, build-macos.sh, make-dmg.sh, make-icon.sh; atom (CLI launcher) + install-level.sh
45+
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, editor-identity.mjs, extension-signature.mjs, build-macos.sh, make-dmg.sh, make-icon.sh; atom (CLI launcher) + install-level.sh
4446
tools/ dependency-free reference servers: sync-server (/v1 Settings-Sync), update-server (/api/update feed)
4547
vscode/ GITIGNORED upstream Code-OSS checkout (generated)
4648
```
@@ -82,6 +84,31 @@ To macOS a run from source and the installed LevelCode used to be ONE app — sa
8284
- A LaunchServices handler must live outside temp folders — a bundle under `/tmp` is registered but
8385
never chosen. Tests therefore run on fixtures and do not register anything (`test/editorIdentity.test.js`).
8486

87+
## Extensions are verified (keep it that way)
88+
89+
Every release up to 1.3.1 refused every signed extension from Open VSX — "Signature verification was
90+
not executed" — and silently never updated one. The editor verifies through `import('@vscode/vsce-sign')`,
91+
a Microsoft-only module no open-source build contains. `modules/extension-signature` is LevelCode's
92+
module for that slot. Full account + runbooks: `docs/EXTENSION-SIGNATURES.md`.
93+
94+
- **What it checks:** Open VSX's Ed25519 signature over the `.vsix` bytes. A repository signature —
95+
"this is what Open VSX published" — not the publisher's, and not a statement about safety.
96+
- **The key is pinned, never fetched:** `modules/extension-signature/keys.json`. Adding or removing a
97+
key is a security decision; the doc says how a key is checked first. The module may `require` only
98+
`node:crypto`/`fs`/`path`/`zlib`, and its suite runs it with every socket refused.
99+
- **No core patch.** `extension-signature.mjs install` copies three files into the BUILT app (like
100+
`strip-proprietary.mjs`). `check` then fails the build unless the built editor still asks for that
101+
name, the name resolves to our module from the bundles that ask, and the module — imported the way
102+
the editor imports it — accepts a real package and refuses a changed one.
103+
- **`smoke <LevelCode.app>`** asks the app itself (its command line installs one tiny extension into
104+
temp folders). It is the check that would have caught the original bug; CI runs it after each build.
105+
- **`registry`** watches for Open VSX changing its key (the release gate runs it). Exit 1 means
106+
evidence, and every shipped build is refusing extensions until a release carries the new key.
107+
- **Do not "fix" a refusal by turning verification off** (`extensions.verifySignature`, a patch like
108+
VSCodium's). And a run from source proves nothing here: the editor only enforces on a built app.
109+
- Its tests live in `modules/extension-signature/test/` — outside `extensions/`, whose `test/` folders
110+
ship in the app. `scripts/test-extensions.sh` discovers `modules/*/test/*.test.js` too.
111+
85112
## Toolchain (hard requirements — these bit us)
86113

87114
- **Node = `vscode/.nvmrc` (currently 24.15.0)**. Older majors fail to compile native modules.
@@ -189,4 +216,4 @@ big-file mode badge. Files: extension.js + fileOps/lineOps/columnOps/encodingEol
189216
- `// @ts-check` + JSDoc at top of JS files.
190217
- Test JS logic with `node --check` and small unit snippets before wiring into the editor.
191218
- After any change, `./scripts/run-dev.sh` to verify; package with `./scripts/build-macos.sh`.
192-
- Commit `extensions/`, `patches/`, `branding/`, `scripts/`, `docs/`, `PLAN.md`, `CLAUDE.md`. Never commit `vscode/`.
219+
- Commit `extensions/`, `modules/`, `patches/`, `branding/`, `scripts/`, `docs/`, `PLAN.md`, `CLAUDE.md`. Never commit `vscode/`.

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ LevelCode is a **clean overlay on top of Code-OSS**, not a vendored copy of the
3434

3535
- `extensions/` — the first-party extensions (native AI, Notepad++ pack, themes, hackability, sync, updater), plain JS with no build step.
3636
- `branding/` — the LevelCode identity (`product.overlay.json`) + icons, deep-merged onto upstream `product.json`; the extension gallery wired to **Open VSX**.
37+
- `modules/` — what ships in the app outside an extension: the Open VSX signature verifier and the signing keys it trusts.
3738
- `patches/` — the small set of core source patches (each tagged `// [LevelCode]`).
3839
- `scripts/` — bootstrap / run-dev / build / dmg / icon.
3940
- `tools/` — dependency-free reference servers (Settings-Sync feed, update feed).
@@ -59,7 +60,7 @@ Actively built, macOS-first. Working today: the Notepad++ power-editing pack; th
5960

6061
## Why a fork
6162

62-
Code-OSS is MIT-licensed and free to fork, modify, and ship. LevelCode honors the constraints every forker must: the **Microsoft Extension Marketplace is Microsoft-products-only**, so LevelCode points its gallery at **Open VSX** (the Eclipse-run open marketplace); and LevelCode ships its own name, icon, and identity. It is **not** produced by, endorsed by, or affiliated with Microsoft, and the upstream Code-OSS source is fetched at build time — never redistributed in this repository.
63+
Code-OSS is MIT-licensed and free to fork, modify, and ship. LevelCode honors the constraints every forker must: the **Microsoft Extension Marketplace is Microsoft-products-only**, so LevelCode points its gallery at **Open VSX** (the Eclipse-run open marketplace) — and checks every extension it installs or updates against Open VSX's signature, with the signing key pinned in the app rather than fetched ([how, and what that does and does not prove](./docs/EXTENSION-SIGNATURES.md)); and LevelCode ships its own name, icon, and identity. It is **not** produced by, endorsed by, or affiliated with Microsoft, and the upstream Code-OSS source is fetched at build time — never redistributed in this repository.
6364

6465
## License
6566

‎SECURITY.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ LevelCode is built to keep your code and your credentials on your machine.
1212
your API key over plaintext `http://` to a non-local host (https, or an explicit localhost, only).
1313
- **Readable.** The AI layer is plain, dependency-free JS — you can see exactly what is sent to a
1414
provider. Transparency is a design goal, not an afterthought.
15+
- **Extensions are checked on the way in.** Every extension installed or updated from Open VSX is
16+
verified against the registry's signature, with the signing key pinned in the app rather than fetched
17+
from the server being checked. That proves the package is the one Open VSX published. It does not
18+
prove the extension is safe, and it is not the publisher's own signature. See
19+
[`docs/EXTENSION-SIGNATURES.md`](./docs/EXTENSION-SIGNATURES.md).
1520

1621
## Reporting a vulnerability
1722

@@ -25,6 +30,7 @@ you posted through the fix.
2530

2631
## Scope
2732

28-
- **In scope:** the LevelCode first-party extensions and the build / branding kit in this repository.
33+
- **In scope:** the LevelCode first-party extensions, the extension signature verifier
34+
(`modules/extension-signature`), and the build / branding kit in this repository.
2935
- **Out of scope:** upstream Code-OSS / VS Code itself (report those to Microsoft), and third-party
3036
extensions you install from Open VSX.

0 commit comments

Comments
 (0)