Skip to content

Commit 387563c

Browse files
committed
Add extension signature verification service
1 parent 65c18df commit 387563c

14 files changed

+518
-44
lines changed

src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { SharedProcessEnvironmentService } from 'vs/platform/sharedProcess/node/
3232
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';
3333
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
3434
import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
35+
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
3536
import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
3637
import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService';
3738
import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
@@ -313,6 +314,7 @@ class SharedProcessMain extends Disposable {
313314
// Extension Management
314315
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
315316
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
317+
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService));
316318
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true));
317319

318320
// Extension Gallery

src/vs/code/node/cliProcessMain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro
2424
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
2525
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
2626
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
27+
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
2728
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
2829
import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
2930
import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService';
@@ -181,6 +182,7 @@ class CliMain extends Disposable {
181182
// Extensions
182183
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
183184
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
185+
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService));
184186
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true));
185187
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true));
186188

src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface IInstallExtensionTask {
2828
readonly identifier: IExtensionIdentifier;
2929
readonly source: IGalleryExtension | URI;
3030
readonly operation: InstallOperation;
31+
wasVerified?: boolean;
3132
run(): Promise<{ local: ILocalExtension; metadata: Metadata }>;
3233
waitUntilTaskIsFinished(): Promise<{ local: ILocalExtension; metadata: Metadata }>;
3334
cancel(): void;
@@ -243,6 +244,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
243244
const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000;
244245
reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
245246
extensionData: getGalleryExtensionTelemetryData(task.source),
247+
wasVerified: task.wasVerified,
246248
duration: new Date().getTime() - startTime,
247249
durationSinceUpdate
248250
});
@@ -256,7 +258,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
256258
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped });
257259
} catch (error) {
258260
if (!URI.isUri(task.source)) {
259-
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error });
261+
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', {
262+
extensionData: getGalleryExtensionTelemetryData(task.source),
263+
wasVerified: task.wasVerified,
264+
duration: new Date().getTime() - startTime,
265+
error
266+
});
260267
}
261268
this.logService.error('Error while installing the extension:', task.identifier.id);
262269
throw error;
@@ -693,16 +700,32 @@ function toExtensionManagementError(error: Error): ExtensionManagementError {
693700
return e;
694701
}
695702

696-
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void {
697-
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined;
703+
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, wasVerified, duration, error, durationSinceUpdate }: { extensionData: any; wasVerified?: boolean; duration?: number; durationSinceUpdate?: number; error?: Error }): void {
704+
let errorcode: ExtensionManagementErrorCode | undefined;
705+
let errorcodeDetail: string | undefined;
706+
707+
if (error) {
708+
if (error instanceof ExtensionManagementError) {
709+
errorcode = error.code;
710+
711+
if (error.code === ExtensionManagementErrorCode.Signature) {
712+
errorcodeDetail = error.message;
713+
}
714+
} else {
715+
errorcode = ExtensionManagementErrorCode.Internal;
716+
}
717+
}
718+
698719
/* __GDPR__
699720
"extensionGallery:install" : {
700721
"owner": "sandy081",
701722
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
702723
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
703724
"durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
704725
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
726+
"errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
705727
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
728+
"wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
706729
"${include}": [
707730
"${GalleryExtensionTelemetryData}"
708731
]
@@ -725,12 +748,14 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName:
725748
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
726749
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
727750
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
751+
"errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
752+
"wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
728753
"${include}": [
729754
"${GalleryExtensionTelemetryData}"
730755
]
731756
}
732757
*/
733-
telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate });
758+
telemetryService.publicLog(eventName, { ...extensionData, wasVerified, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate });
734759
}
735760

736761
export abstract class AbstractExtensionTask<T> {

src/vs/platform/extensionManagement/common/extensionGalleryService.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ const AssetType = {
193193
Manifest: 'Microsoft.VisualStudio.Code.Manifest',
194194
VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage',
195195
License: 'Microsoft.VisualStudio.Services.Content.License',
196-
Repository: 'Microsoft.VisualStudio.Services.Links.Source'
196+
Repository: 'Microsoft.VisualStudio.Services.Links.Source',
197+
Signature: 'Microsoft.VisualStudio.Services.VsixSignature'
197198
};
198199

199200
const PropertyType = {
@@ -505,7 +506,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
505506
repository: getRepositoryAsset(version),
506507
download: getDownloadAsset(version),
507508
icon: getVersionAsset(version, AssetType.Icon),
508-
coreTranslations: getCoreTranslationAssets(version)
509+
signature: getVersionAsset(version, AssetType.Signature),
510+
coreTranslations: getCoreTranslationAssets(version),
509511
};
510512

511513
return {
@@ -542,6 +544,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
542544
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
543545
hasReleaseVersion: true,
544546
preview: getIsPreview(galleryExtension.flags),
547+
isSigned: !!assets.signature
545548
};
546549
}
547550

@@ -1031,6 +1034,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
10311034
log(new Date().getTime() - startTime);
10321035
}
10331036

1037+
async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise<void> {
1038+
if (!extension.assets.signature) {
1039+
return;
1040+
}
1041+
1042+
this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id);
1043+
1044+
const context = await this.getAsset(extension.assets.signature);
1045+
await this.fileService.writeFile(location, context.stream);
1046+
}
1047+
10341048
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
10351049
if (extension.assets.readme) {
10361050
const context = await this.getAsset(extension.assets.readme, {}, token);

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export interface IGalleryExtensionAssets {
178178
repository: IGalleryExtensionAsset | null;
179179
download: IGalleryExtensionAsset;
180180
icon: IGalleryExtensionAsset | null;
181+
signature: IGalleryExtensionAsset | null;
181182
coreTranslations: [string, IGalleryExtensionAsset][];
182183
}
183184

@@ -224,6 +225,7 @@ export interface IGalleryExtension {
224225
preview: boolean;
225226
hasPreReleaseVersion: boolean;
226227
hasReleaseVersion: boolean;
228+
isSigned: boolean;
227229
allTargetPlatforms: TargetPlatform[];
228230
assets: IGalleryExtensionAssets;
229231
properties: IGalleryExtensionProperties;
@@ -335,6 +337,7 @@ export interface IExtensionGalleryService {
335337
getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
336338
getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
337339
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
340+
downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise<void>;
338341
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
339342
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
340343
getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null>;
@@ -390,6 +393,7 @@ export enum ExtensionManagementErrorCode {
390393
CorruptZip = 'CorruptZip',
391394
IncompleteZip = 'IncompleteZip',
392395
Internal = 'Internal',
396+
Signature = 'Signature'
393397
}
394398

395399
export class ExtensionManagementError extends Error {

src/vs/platform/extensionManagement/common/extensionManagementUtil.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
125125
"publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
126126
"isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
127127
"dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
128+
"isSigned": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
128129
"${include}": [
129130
"${GalleryExtensionTelemetryData2}"
130131
]
@@ -140,6 +141,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
140141
publisherDisplayName: extension.publisherDisplayName,
141142
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
142143
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
144+
isSigned: extension.isSigned,
143145
...extension.telemetryData
144146
};
145147
}

src/vs/platform/extensionManagement/node/extensionDownloader.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { Promises } from 'vs/base/common/async';
77
import { getErrorMessage } from 'vs/base/common/errors';
8-
import { Disposable } from 'vs/base/common/lifecycle';
8+
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
99
import { isWindows } from 'vs/base/common/platform';
1010
import { joinPath } from 'vs/base/common/resources';
1111
import * as semver from 'vs/base/common/semver/semver';
@@ -18,12 +18,19 @@ import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/
1818
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
1919
import { ILogService } from 'vs/platform/log/common/log';
2020

21-
export class ExtensionsDownloader extends Disposable {
21+
export interface IExtensionsDownloader extends IDisposable {
22+
delete(location: URI): Promise<void>;
23+
downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ extensionLocation: URI; signatureArchiveLocation?: URI }>;
24+
}
25+
26+
export class ExtensionsDownloader extends Disposable implements IExtensionsDownloader {
2227

2328
readonly extensionsDownloadDir: URI;
2429
private readonly cache: number;
2530
private readonly cleanUpPromise: Promise<void>;
2631

32+
private static readonly SignatureArchiveExtension = '.sigzip';
33+
2734
constructor(
2835
@INativeEnvironmentService environmentService: INativeEnvironmentService,
2936
@IFileService private readonly fileService: IFileService,
@@ -32,21 +39,33 @@ export class ExtensionsDownloader extends Disposable {
3239
) {
3340
super();
3441
this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath);
35-
this.cache = 20; // Cache 20 downloads
42+
this.cache = 20; // Cache 20 downloaded VSIX files
3643
this.cleanUpPromise = this.cleanUp();
3744
}
3845

39-
async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
46+
async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ extensionLocation: URI; signatureArchiveLocation?: URI }> {
4047
await this.cleanUpPromise;
4148
const vsixName = this.getName(extension);
42-
const location = joinPath(this.extensionsDownloadDir, vsixName);
49+
const extensionLocation = joinPath(this.extensionsDownloadDir, vsixName);
50+
let signatureArchiveLocation: URI | undefined;
51+
52+
await this.downloadFile(extensionLocation, extension, operation, 'vsix', this.extensionGalleryService.download);
53+
54+
if (extension.assets.signature) {
55+
signatureArchiveLocation = ExtensionsDownloader.getSignatureArchiveLocation(extensionLocation);
56+
57+
await this.downloadFile(signatureArchiveLocation, extension, operation, 'signature archive', this.extensionGalleryService.downloadSignatureArchive);
58+
}
4359

44-
// Download only if vsix does not exist
60+
return { extensionLocation, signatureArchiveLocation };
61+
}
62+
63+
private async downloadFile(location: URI, extension: IGalleryExtension, operation: InstallOperation, fileType: string, download: (extension: IGalleryExtension, location: URI, operation: InstallOperation) => Promise<void>) {
4564
if (!await this.fileService.exists(location)) {
46-
// Download to temporary location first only if vsix does not exist
65+
// Download to temporary location first only if file does not exist
4766
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
4867
if (!await this.fileService.exists(tempLocation)) {
49-
await this.extensionGalleryService.download(extension, tempLocation, operation);
68+
await download(extension, tempLocation, operation);
5069
}
5170

5271
try {
@@ -57,16 +76,13 @@ export class ExtensionsDownloader extends Disposable {
5776
await this.fileService.del(tempLocation);
5877
} catch (e) { /* ignore */ }
5978
if (error.code === 'ENOTEMPTY') {
60-
this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
79+
this.logService.info(`Rename failed because ${fileType} was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
6180
} else {
62-
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path);
81+
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the ${fileType} from downloaded location`, tempLocation.path);
6382
throw error;
6483
}
6584
}
66-
6785
}
68-
69-
return location;
7086
}
7187

7288
async delete(location: URI): Promise<void> {
@@ -89,15 +105,25 @@ export class ExtensionsDownloader extends Disposable {
89105
private async cleanUp(): Promise<void> {
90106
try {
91107
if (!(await this.fileService.exists(this.extensionsDownloadDir))) {
92-
this.logService.trace('Extension VSIX downlads cache dir does not exist');
108+
this.logService.trace('Extension VSIX downloads cache dir does not exist');
93109
return;
94110
}
95111
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
96112
if (folderStat.children) {
97113
const toDelete: URI[] = [];
98114
const all: [ExtensionKey, IFileStatWithMetadata][] = [];
115+
const signatureArchives = new Set<string>();
116+
99117
for (const stat of folderStat.children) {
100-
const extension = ExtensionKey.parse(stat.name);
118+
const name = stat.name;
119+
120+
if (ExtensionsDownloader.isSignatureArchive(name)) {
121+
const extensionPath = ExtensionsDownloader.getExtensionPath(name);
122+
signatureArchives.add(extensionPath);
123+
continue;
124+
}
125+
126+
const extension = ExtensionKey.parse(name);
101127
if (extension) {
102128
all.push([extension, stat]);
103129
}
@@ -111,16 +137,40 @@ export class ExtensionsDownloader extends Disposable {
111137
}
112138
distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
113139
toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
140+
114141
await Promises.settled(toDelete.map(resource => {
115142
this.logService.trace('Deleting vsix from cache', resource.path);
116-
return this.fileService.del(resource);
143+
144+
let promise = Promise.resolve();
145+
146+
if (signatureArchives.has(resource.fsPath)) {
147+
const signatureArchiveLocation = ExtensionsDownloader.getSignatureArchiveLocation(resource);
148+
149+
promise = promise.then(() => this.fileService.del(signatureArchiveLocation));
150+
}
151+
152+
promise = promise.then(() => this.fileService.del(resource));
153+
154+
return promise;
117155
}));
118156
}
119157
} catch (e) {
120158
this.logService.error(e);
121159
}
122160
}
123161

162+
private static getExtensionPath(signatureArchivePath: string): string {
163+
return signatureArchivePath.substring(0, signatureArchivePath.length - ExtensionsDownloader.SignatureArchiveExtension.length);
164+
}
165+
166+
private static getSignatureArchiveLocation(extensionLocation: URI): URI {
167+
return URI.file(extensionLocation.fsPath + ExtensionsDownloader.SignatureArchiveExtension);
168+
}
169+
170+
private static isSignatureArchive(name: string): boolean {
171+
return name.endsWith(ExtensionsDownloader.SignatureArchiveExtension);
172+
}
173+
124174
private getName(extension: IGalleryExtension): string {
125175
return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid();
126176
}

0 commit comments

Comments
 (0)