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+
mQINBGPGrCwBEAC7SSKQIFoQdt3jYv/1okRdoleepLDG4NfcG52S45Ex3/fUA6Z/
4+
ewHQrx//SN+h1FLpb0zQMyamWrSh2O3dnkWridwlskb5/y8C/6OUdk4L/ZgHeyPO
5+
Ncbyl1hqO8oViakiWt4IxwSYo83eJHxOUiCGZlqV6EpEsaur43BRHnK8EciNeIxF
6+
Bjle3yXH1K3EgGGHpgnSoKe1nSVxtWIwX45d06v+VqnBoI6AyK0Zp+Nn8bL0EnXC
7+
xGYU3XOkC6EmITlhMju1AhxnbkQiy8IUxXiaj3NoPc1khapOcyBybhESjRZHlgu4
8+
ToLZGaypjtfQJgMeFlpua7sJK0ziFMW4wOTX+6Ix/S6XA80dVbl3VEhSMpFCcgI+
9+
OmEd2JuBs6maG+92fCRIzGAClzV8/ifM//JU9D7Qlq6QJpcbNClODlPNDNe7RUEO
10+
b7Bu7dJJS3VhHO9eEen6m6vRE4DNriHT4Zvq1UkHfpJUW7njzkIYRni3eNrsr4Da
11+
U/eeGbVipok4lzZEOQtuaZlX9ytOdGrWEGMGSosTOG6u6KAKJoz7cQGZiz4pZpjR
12+
3N2SIYv59lgpHrIV7UodGx9nzu0EKBhkoulaP1UzH8F16psSaJXRjeyl/YP8Rd2z
13+
SYgZVLjTzkTUXkJT8fQO8zLBEuwA0IiXX5Dl7grfEeShANVrM9LVu8KkUwARAQAB
14+
tC5Db2RlciBSZWxlYXNlIFNpZ25pbmcgS2V5IDxzZWN1cml0eUBjb2Rlci5jb20+
15+
iQJUBBMBCgA+FiEEKMY4lDj2Q3PIwvSKi87Yfbu4ZEsFAmPGrCwCGwMFCQWjmoAF
16+
CwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQi87Yfbu4ZEvrQQ//a3ySdMVhnLP+
17+
KneonV2zuNilTMC2J/MNG7Q0hU+8I9bxCc6DDqcnBBCQkIUwJq3wmelt3nTC8RxI
18+
fv+ggnbdF9pz7Fc91nIJsGlWpH+bu1tSIvKF/rzZA8v6xUblFFfaC7Gsc5P4xk/+
19+
h0XBDAy6K+7+AafgLFpRD08Y0Kf2aMcqdM6c2Zo4IPo6FNrOa66FNkypZdQ4IByW
20+
4kMezZSTp4Phqd9yqGC4m44U8YgzmW9LHgrvS0JyIaRPcQFM31AJ50K3iYRxL1ll
21+
ETqJvbDR8UORNQs3Qs3CEZL588BoDMX2TYObTCG6g9Om5vJT0kgUkjDxQHwbAj6E
22+
z9j8BoWkDT2JNzwdfTbPueuRjO+A+TXA9XZtrzbEYEzh0sD9Bdr7ozSF3JAs4GZS
23+
nqcVlyp7q44ZdePR9L8w0ksth56tBWHfE9hi5jbRDRY2OnkV7y7JtWnBDQx9bCIo
24+
7L7aBT8eirI1ZOnUxHJrnqY5matfWjSDBFW+YmWUkjnzBsa9F4m8jq9MSD3Q/8hN
25+
ksJFrmLQs0/8hnM39tS7kLnAaWeGvbmjnxdeMqZsICxNpbyQrq2AhF4GhWfc+NsZ
26+
yznVagJZ9bIlGsycSXJbsA5GbXDnm172TlodMUbLF9FU8i0vV4Y7q6jKO/VsblKU
27+
F0bhXIRqVLrd9g88IyVyyZozmwbJKIy5Ag0EY8asLAEQAMgI9bMurq6Zic4s5W0u
28+
W6LBDHyZhe+w2a3oT/i2YgTsh8XmIjrNasYYWO67b50JKepA3fk3ZA44w8WJqq+z
29+
HLpslEb2fY5I1HvENUMKjYAUIsswSC21DSBau4yYiRGF0MNqv/MWy5Rjc993vIU4
30+
4TM3mvVhPrYfIkr0jwSbxq8+cm3sBjr0gcBQO57C3w8QkcZ6jefuI7y+1ZeM7X3L
31+
OngmBFJDEutd9LPO/6Is4j/iQfTb8WDR6OmMX3Y04RHrP4sm7jf+3ZZKjcFCZQjr
32+
QA4XHcQyJjnMN34Fn1U7KWopivU+mqViAnVpA643dq9SiBqsl83/R03DrpwKpP7r
33+
6qasUHSUULuS7A4n8+CDwK5KghvrS0hOwMiYoIwZIVPITSUFHPYxrCJK7gU2OHfk
34+
IZHX5m9L5iNwLz958GwzwHuONs5bjMxILbKknRhEBOcbhcpk0jswiPNUrEdipRZY
35+
GR9G9fzD6q4P5heV3kQRqyUUTxdDj8w7jbrwl8sm5zk+TMnPRsu2kg0uwIN1aILm
36+
oVkDN5CiZtg00n2Fu3do5F3YkF0Cz7indx5yySr5iUuoCY0EnpqSwourJ/ZdZA9Y
37+
ZCHjhgjwyPCbxpTGfLj1g25jzQBYn5Wdgr2aHCQcqnU8DKPCnYL9COHJJylgj0vN
38+
NSxyDjNXYYwSrYMqs/91f5xVABEBAAGJAjwEGAEKACYWIQQoxjiUOPZDc8jC9IqL
39+
zth9u7hkSwUCY8asLAIbDAUJBaOagAAKCRCLzth9u7hkSyMvD/0Qal5kwiKDjgBr
40+
i/dtMka+WNBTMb6vKoM759o33YAl22On5WgLr9Uz0cjkJPtzMHxhUo8KQmiPRtsK
41+
dOmG9NI9NttfSeQVbeL8V/DC672fWPKM4TB8X7Kkj56/KI7ueGRokDhXG2pJlhQr
42+
HwzZsAKoCMMnjcquAhHJClK9heIpVLBGFVlmVzJETzxo6fbEU/c7L79+hOrR4BWx
43+
Tg6Dk7mbAGe7BuQLNtw6gcWUVWtHS4iYQtE/4khU1QppC1Z/ZbZ+AJT2TAFXzIaw
44+
0l9tcOh7+TXqsvCLsXN0wrUh1nOdxA81sNWEMY07bG1qgvHyVc7ZYM89/ApK2HP+
45+
bBDIpAsRCGu2MHtrnJIlNE1J14G1mnauR5qIqI3C0R5MPLXOcDtp+gnjFe+PLU+6
46+
rQxJObyOkyEpOvtVtJKfFnpI5bqyl8WEPN0rDaS2A27cGXi5nynSAqoM1xT15W21
47+
uyY2GXY26DIwVfc59wGeclwcM29nS7prRU3KtskjonJ0iQoQebYOHLxy896cK+pK
48+
nnhZx5AQjYiZPsPktSNZjSuOvTZ3g+IDwbCSvmBHcQpitzUOPShTUTs0QjSttzk2
49+
I6WxP9ivoR9yJGsxwNgCgrYdyt5+hyXXW/aUVihnQwizQRbymjJ2/z+I8NRFIeYb
50+
xbtNFaH3WjLnhm9CB/H+Lc8fUj6HaZkCDQRjxt6QARAAsjZuCMjZBaAC1LFMeRcv
51+
9+Ck7T5UNXTL9xQr1jUFZR95I6loWiWvFJ3Uet7gIbgNYY5Dc1gDr1Oqx9KQBjsN
52+
TUahXov5lmjF5mYeyWTDZ5TS8H3o50zQzfZRC1eEbqjiBMLAHv74KD13P62nvzv6
53+
Dejwc7Nwc6aOH3cdZm74kz4EmdobJYRVdd5X9EYH/hdM928SsipKhm44oj3RDGi/
54+
x+ptjW9gr0bnrgCbkyCMNKhnmHSM60I8f4/viRItb+hWRpZYfLxMGTBVunicSXcX
55+
Zh6Fq/DD/yTjzN9N83/NdDvwCyKo5U/kPgD2Ixh5PyJ38cpz6774Awnb/tstCI1g
56+
glnlNbu8Qz84STr3NRZMOgT5h5b5qASOeruG4aVo9euaYJHlnlgcoUmpbEMnwr0L
57+
tREUXSHGXWor7EYPjUQLskIaPl9NCZ3MEw5LhsZTgEdFBnb54dxMSEl7/MYDYhD/
58+
uTIWOJmtsWHmuMmvfxnw5GDEhJnAp4dxUm9BZlJhfnVR07DtTKyEk37+kl6+i0ZQ
59+
yU4HJ2GWItpLfK54E/CH+S91y7wpepb2TMkaFR2fCK0vXTGAXWK+Y+aTD8ZcLB5y
60+
0IYPsvA0by5AFpmXNfWZiZtYvgJ5FAQZNuB5RILg3HsuDq2U4wzp5BoohWtsOzsn
61+
antIUf/bN0D2g+pCySkc5ssAEQEAAbQuQ29kZXIgUmVsZWFzZSBTaWduaW5nIEtl
62+
eSA8c2VjdXJpdHlAY29kZXIuY29tPokCVAQTAQoAPhYhBCHJaxy5UHGIdPZNvWpa
63+
ZxteQKO5BQJjxt6QAhsDBQkFo5qABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ
64+
EGpaZxteQKO5oysP/1rSdvbKMzozvnVZoglnPjnSGStY9Pr2ziGL7eIMk2yt+Orr
65+
j/AwxYIDgsZPQoJEr87eX2dCYtUMM1x+CpZsWu8dDVFLxyZp8nPmhUzcUCFfutw1
66+
UmAVKQkOra9segZtw4HVcSctpdgLw7NHq7vIQm4knIvjWmdC15r1B6/VJJI8CeaR
67+
Zy+ToPr9fKnYs1RNdz+DRDN2521skX1DaInhB/ALeid90rJTRujaP9XeyNb9k32K
68+
qd3h4C0KUGIf0fNKj4mmDlNosX3V/pJZATpFiF8aVPlybHQ2W5xpn1U8FJxE4hgR
69+
rvsZmO685Qwm6p/uRI5Eymfm8JC5OQNt9Kvs/BMhotsW0u+je8UXwnznptMILpVP
70+
+qxNuHUe1MYLdjK21LFF+Pk5O4W1TT6mKcbisOmZuQMG5DxpzUwm1Rs5AX1omuJt
71+
iOrmQEvmrKKWC9qbcmWW1t2scnIJsNtrsvME0UjJFz+RL6UUX3xXlLK6YOUghCr8
72+
gZ7ZPgFqygS6tMu8TAGURzSCfijDh+eZGwqrlvngBIaO5WiNdSXC/J9aE1KThXmX
73+
90A3Gwry+yI2kRS7o8vmghXewPTZbnG0CVHiQIH2yqFNXnhKvhaJt0g04TcnxBte
74+
kiFqRT4K1Bb7pUIlUANmrKo9/zRCxIOopEgRH5cVQ8ZglkT0t5d3ePmAo6h0uQIN
75+
BGPG3pABEADghhNByVoC+qCMo+SErjxz9QYA+tKoAngbgPyxxyB4RD52Z58MwVaP
76+
+Yk0qxJYUBat3dJwiCTlUGG+yTyMOwLl7qSDr53AD5ml0hwJqnLBJ6OUyGE4ax4D
77+
RUVBprKlDltwr98cZDgzvwEhIO2T3tNZ4vySveITj9pLonOrLkAfGXqFOqom+S37
78+
6eZvjKTnEUbT+S0TTynwds70W31sxVUrL62qsUnmoKEnsKXk/7X8CLXWvtNqu9kf
79+
eiXs5Jz4N6RZUqvS0WOaaWG9v1PHukTtb8RyeookhsBqf9fWOlw5foel+NQwGQjz
80+
0D0dDTKxn2Taweq+gWNCRH7/FJNdWa9upZ2fUAjg9hN9Ow8Y5nE3J0YKCBAQTgNa
81+
XNtsiGQjdEKYZslxZKFM34By3LD6IrkcAEPKu9plZthmqhQumqwYRAgB9O56jg3N
82+
GDDRyAMS7y63nNphTSatpOZtPVVMtcBw5jPjMIPFfU2dlfsvmnCvru2dvfAij+Ng
83+
EkwOLNS8rFQHMJSQysmHuAPSYT97Yl022mPrAtb9+hwtCXt3VI6dvIARl2qPyF0D
84+
DMw2fW5E7ivhUr2WEFiBmXunrJvMIYldBzDkkBjamelPjoevR0wfoIn0x1CbSsQi
85+
zbEs3PXHs7nGxb9TZnHY4+J94mYHdSXrImAuH/x97OnlfUpOKPv5lwARAQABiQI8
86+
BBgBCgAmFiEEIclrHLlQcYh09k29alpnG15Ao7kFAmPG3pACGwwFCQWjmoAACgkQ
87+
alpnG15Ao7m2/g//Y/YRM+Qhf71G0MJpAfym6ZqmwsT78qQ8T9w95ZeIRD7UUE8d
88+
tm39kqJTGP6DuHCNYEMs2M88o0SoQsS/7j/8is7H/13F5o40DWjuQphia2BWkB1B
89+
G4QRRIXMlrPX8PS92GDCtGfvxn90Li2FhQGZWlNFwvKUB7+/yLMsZzOwo7BS6PwC
90+
hvI3eC7DBC8sXjJUxsrgFAkxQxSx/njP8f4HdUwhNnB1YA2/5IY5bk8QrXxzrAK1
91+
sbIAjpJdtPYOrZByyyj4ZpRcSm3ngV2n8yd1muJ5u+oRIQoGCdEIaweCj598jNFa
92+
k378ZA11hCyNFHjpPIKnF3tfsQ8vjDatoq4Asy+HXFuo1GA/lvNgNb3Nv4FUozuv
93+
JYJ0KaW73FZXlFBIBkMkRQE8TspHy2v/IGyNXBwKncmkszaiiozBd+T+1NUZgtk5
94+
9o5uKQwLHVnHIU7r/w/oN5LvLawLg2dP/f2u/KoQXMxjwLZncSH4+5tRz4oa/GMn
95+
k4F84AxTIjGfLJeXigyP6xIPQbvJy+8iLRaCpj+v/EPwAedbRV+u0JFeqqikca70
96+
aGN86JBOmwpU87sfFxLI7HdI02DkvlxYYK3vYlA6zEyWaeLZ3VNr6tHcQmOnFe8Q
97+
26gcS0AQcxQZrcWTCZ8DJYF+RnXjSVRmHV/3YDts4JyMKcD6QX8s/3aaldk=
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)