Skip to content

Commit 3e18934

Browse files
committed
Add binary signature verification
1 parent ac10af8 commit 3e18934

File tree

9 files changed

+363
-9
lines changed

9 files changed

+363
-9
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
- Coder output panel enhancements: All log entries now include timestamps, and you
8+
can filter messages by log level in the panel.
9+
10+
### Added
11+
512
- Update `/openDevContainer` to support all dev container features when hostPath
613
and configFile are provided.
714
- Add `coder.disableUpdateNotifications` setting to disable workspace template
815
update notifications.
9-
- Coder output panel enhancements: All log entries now include timestamps, and you
10-
can filter messages by log level in the panel.
16+
- Add binary signature verification. This can be disabled with
17+
`coder.disableSignatureVerification` if you purposefully run a binary that is
18+
not signed by Coder (for example a binary you built yourself).
1119

1220
## [v1.9.2](https://github.com/coder/vscode-coder/releases/tag/v1.9.2) 2025-06-25
1321

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@
114114
"markdownDescription": "Disable notifications when workspace template updates are available.",
115115
"type": "boolean",
116116
"default": false
117+
},
118+
"coder.disableSignatureVerification": {
119+
"markdownDescription": "Disable Coder CLI signature verification, which can be useful if you run an unsigned fork of the binary.",
120+
"type": "boolean",
121+
"default": false
117122
}
118123
}
119124
},
@@ -289,6 +294,7 @@
289294
"jsonc-parser": "^3.3.1",
290295
"memfs": "^4.17.1",
291296
"node-forge": "^1.3.1",
297+
"openpgp": "^6.2.0",
292298
"pretty-bytes": "^6.1.1",
293299
"proxy-agent": "^6.4.0",
294300
"semver": "^7.7.1",

pgp-public.key

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
-----BEGIN PGP PUBLIC KEY BLOCK-----
2+
3+
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
98+
=dLmT
99+
-----END PGP PUBLIC KEY BLOCK-----

src/api-helper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated";
33
import { ErrorEvent } from "eventsource";
44
import { z } from "zod";
55

6-
export function errToStr(error: unknown, def: string) {
6+
export function errToStr(
7+
error: unknown,
8+
def: string = "No error message provided",
9+
) {
710
if (error instanceof Error && error.message) {
811
return error.message;
912
} else if (isApiError(error)) {

src/cliManager.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ export type RemovalResult = { fileName: string; error: unknown };
7878

7979
/**
8080
* Remove binaries in the same directory as the specified path that have a
81-
* .old-* or .temp-* extension. Return a list of files and the errors trying to
82-
* remove them, when applicable.
81+
* .old-* or .temp-* extension along with signatures (files ending in .asc).
82+
* Return a list of files and the errors trying to remove them, when applicable.
8383
*/
8484
export async function rmOld(binPath: string): Promise<RemovalResult[]> {
8585
const binDir = path.dirname(binPath);
@@ -88,7 +88,11 @@ export async function rmOld(binPath: string): Promise<RemovalResult[]> {
8888
const results: RemovalResult[] = [];
8989
for (const file of files) {
9090
const fileName = path.basename(file);
91-
if (fileName.includes(".old-") || fileName.includes(".temp-")) {
91+
if (
92+
fileName.includes(".old-") ||
93+
fileName.includes(".temp-") ||
94+
fileName.endsWith(".asc")
95+
) {
9296
try {
9397
await fs.rm(path.join(binDir, file), { force: true });
9498
results.push({ fileName, error: undefined });

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
4949

5050
const output = vscode.window.createOutputChannel("Coder", { log: true });
5151
const storage = new Storage(
52+
vscodeProposed,
5253
output,
5354
ctx.globalState,
5455
ctx.secrets,

src/pgp.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { createReadStream, promises as fs } from "fs";
2+
import * as openpgp from "openpgp";
3+
import * as path from "path";
4+
import { Readable } from "stream";
5+
import * as vscode from "vscode";
6+
import { errToStr } from "./api-helper";
7+
8+
export type Key = openpgp.Key;
9+
10+
export enum VerificationErrorCode {
11+
/* The signature does not match. */
12+
Invalid = "Invalid",
13+
/* Failed to read the signature or the file to verify. */
14+
Read = "Read",
15+
}
16+
17+
export class VerificationError extends Error {
18+
constructor(
19+
public readonly code: VerificationErrorCode,
20+
message: string,
21+
) {
22+
super(message);
23+
}
24+
25+
summary(): string {
26+
switch (this.code) {
27+
case VerificationErrorCode.Invalid:
28+
return "Signature does not match";
29+
default:
30+
return "Failed to read signature";
31+
}
32+
}
33+
}
34+
35+
/**
36+
* Return the public keys bundled with the plugin.
37+
*/
38+
export async function readPublicKeys(
39+
logger: vscode.LogOutputChannel,
40+
): Promise<Key[]> {
41+
const keyFile = path.join(__dirname, "../pgp-public.key");
42+
logger.info("Reading public key", keyFile);
43+
const armoredKeys = await fs.readFile(keyFile, "utf8");
44+
return openpgp.readKeys({ armoredKeys });
45+
}
46+
47+
/**
48+
* Given public keys, a path to a file to verify, and a path to a detached
49+
* signature, verify the file's signature. Return true if valid, otherwise
50+
* return VerificationError.
51+
*/
52+
export async function verifySignature(
53+
logger: vscode.LogOutputChannel,
54+
publicKeys: openpgp.Key[],
55+
cliPath: string,
56+
signaturePath: string,
57+
): Promise<true | VerificationError> {
58+
try {
59+
logger.info("Reading signature", signaturePath);
60+
const armoredSignature = await fs.readFile(signaturePath, "utf8");
61+
const signature = await openpgp.readSignature({ armoredSignature });
62+
63+
logger.info("Verifying signature of", cliPath);
64+
const message = await openpgp.createMessage({
65+
// openpgpjs only accepts web readable streams.
66+
binary: Readable.toWeb(createReadStream(cliPath)),
67+
});
68+
const verificationResult = await openpgp.verify({
69+
message,
70+
signature,
71+
verificationKeys: publicKeys,
72+
});
73+
for await (const _ of verificationResult.data) {
74+
// The docs indicate this data must be consumed; it triggers the
75+
// verification of the data.
76+
}
77+
try {
78+
const { verified } = verificationResult.signatures[0];
79+
await verified; // Throws on invalid signature.
80+
logger.info("Binary signature matches");
81+
} catch (e) {
82+
const error = `Unable to verify the authenticity of the binary: ${errToStr(e)}. The binary may have been tampered with.`;
83+
logger.warn(error);
84+
return new VerificationError(VerificationErrorCode.Invalid, error);
85+
}
86+
} catch (e) {
87+
const error = `Failed to read signature or binary: ${errToStr(e)}.`;
88+
logger.warn(error);
89+
return new VerificationError(VerificationErrorCode.Read, error);
90+
}
91+
return true;
92+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy