From 387563c02bae68d2d01db380ee2e2458d7053f2b Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Mon, 3 Oct 2022 08:20:27 -0700 Subject: [PATCH] Add extension signature verification service --- .../sharedProcess/sharedProcessMain.ts | 2 + src/vs/code/node/cliProcessMain.ts | 2 + .../abstractExtensionManagementService.ts | 33 ++- .../common/extensionGalleryService.ts | 18 +- .../common/extensionManagement.ts | 4 + .../common/extensionManagementUtil.ts | 2 + .../node/extensionDownloader.ts | 82 ++++-- .../node/extensionManagementService.ts | 99 +++++-- .../extensionSignatureVerificationService.ts | 69 +++++ .../node/installGalleryExtensionTask.test.ts | 241 ++++++++++++++++++ .../node/remoteExtensionHostAgentCli.ts | 2 + src/vs/server/node/serverServices.ts | 2 + .../extensionRecommendationsService.test.ts | 3 + .../extensionsWorkbenchService.test.ts | 3 + 14 files changed, 518 insertions(+), 44 deletions(-) create mode 100644 src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts create mode 100644 src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 5d99003793a..c8f5ed81e9f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -32,6 +32,7 @@ import { SharedProcessEnvironmentService } from 'vs/platform/sharedProcess/node/ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -313,6 +314,7 @@ class SharedProcessMain extends Disposable { // Extension Management services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); // Extension Gallery diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 14dbbbb1783..58b426a94c5 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -24,6 +24,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; @@ -181,6 +182,7 @@ class CliMain extends Disposable { // Extensions services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true)); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 3e91ecf3b2c..5170f8fe611 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -28,6 +28,7 @@ export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; + wasVerified?: boolean; run(): Promise<{ local: ILocalExtension; metadata: Metadata }>; waitUntilTaskIsFinished(): Promise<{ local: ILocalExtension; metadata: Metadata }>; cancel(): void; @@ -243,6 +244,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), + wasVerified: task.wasVerified, duration: new Date().getTime() - startTime, durationSinceUpdate }); @@ -256,7 +258,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (error) { if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error }); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + wasVerified: task.wasVerified, + duration: new Date().getTime() - startTime, + error + }); } this.logService.error('Error while installing the extension:', task.identifier.id); throw error; @@ -693,8 +700,22 @@ function toExtensionManagementError(error: Error): ExtensionManagementError { return e; } -export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void { - const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined; +export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, wasVerified, duration, error, durationSinceUpdate }: { extensionData: any; wasVerified?: boolean; duration?: number; durationSinceUpdate?: number; error?: Error }): void { + let errorcode: ExtensionManagementErrorCode | undefined; + let errorcodeDetail: string | undefined; + + if (error) { + if (error instanceof ExtensionManagementError) { + errorcode = error.code; + + if (error.code === ExtensionManagementErrorCode.Signature) { + errorcodeDetail = error.message; + } + } else { + errorcode = ExtensionManagementErrorCode.Internal; + } + } + /* __GDPR__ "extensionGallery:install" : { "owner": "sandy081", @@ -702,7 +723,9 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName: "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -725,12 +748,14 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName: "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ - telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate }); + telemetryService.publicLog(eventName, { ...extensionData, wasVerified, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate }); } export abstract class AbstractExtensionTask { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e523486bdbd..3e29bd34583 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -193,7 +193,8 @@ const AssetType = { Manifest: 'Microsoft.VisualStudio.Code.Manifest', VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage', License: 'Microsoft.VisualStudio.Services.Content.License', - Repository: 'Microsoft.VisualStudio.Services.Links.Source' + Repository: 'Microsoft.VisualStudio.Services.Links.Source', + Signature: 'Microsoft.VisualStudio.Services.VsixSignature' }; const PropertyType = { @@ -505,7 +506,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller repository: getRepositoryAsset(version), download: getDownloadAsset(version), icon: getVersionAsset(version, AssetType.Icon), - coreTranslations: getCoreTranslationAssets(version) + signature: getVersionAsset(version, AssetType.Signature), + coreTranslations: getCoreTranslationAssets(version), }; return { @@ -542,6 +544,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller hasPreReleaseVersion: isPreReleaseVersion(latestVersion), hasReleaseVersion: true, preview: getIsPreview(galleryExtension.flags), + isSigned: !!assets.signature }; } @@ -1031,6 +1034,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi log(new Date().getTime() - startTime); } + async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { + if (!extension.assets.signature) { + return; + } + + this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); + + const context = await this.getAsset(extension.assets.signature); + await this.fileService.writeFile(location, context.stream); + } + async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.readme) { const context = await this.getAsset(extension.assets.readme, {}, token); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 3b4958cba2b..5d6a6d38633 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -178,6 +178,7 @@ export interface IGalleryExtensionAssets { repository: IGalleryExtensionAsset | null; download: IGalleryExtensionAsset; icon: IGalleryExtensionAsset | null; + signature: IGalleryExtensionAsset | null; coreTranslations: [string, IGalleryExtensionAsset][]; } @@ -224,6 +225,7 @@ export interface IGalleryExtension { preview: boolean; hasPreReleaseVersion: boolean; hasReleaseVersion: boolean; + isSigned: boolean; allTargetPlatforms: TargetPlatform[]; assets: IGalleryExtensionAssets; properties: IGalleryExtensionProperties; @@ -335,6 +337,7 @@ export interface IExtensionGalleryService { getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; + downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; getReadme(extension: IGalleryExtension, token: CancellationToken): Promise; getManifest(extension: IGalleryExtension, token: CancellationToken): Promise; @@ -390,6 +393,7 @@ export enum ExtensionManagementErrorCode { CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Internal = 'Internal', + Signature = 'Signature' } export class ExtensionManagementError extends Error { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 2b143f85177..8fbe4ebff24 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -125,6 +125,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "isSigned": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData2}" ] @@ -140,6 +141,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): publisherDisplayName: extension.publisherDisplayName, isPreReleaseVersion: extension.properties.isPreReleaseVersion, dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0), + isSigned: extension.isSigned, ...extension.telemetryData }; } diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 3077d4586f8..1b3665811b5 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -5,7 +5,7 @@ import { Promises } from 'vs/base/common/async'; import { getErrorMessage } from 'vs/base/common/errors'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; @@ -18,12 +18,19 @@ import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/ import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -export class ExtensionsDownloader extends Disposable { +export interface IExtensionsDownloader extends IDisposable { + delete(location: URI): Promise; + downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ extensionLocation: URI; signatureArchiveLocation?: URI }>; +} + +export class ExtensionsDownloader extends Disposable implements IExtensionsDownloader { readonly extensionsDownloadDir: URI; private readonly cache: number; private readonly cleanUpPromise: Promise; + private static readonly SignatureArchiveExtension = '.sigzip'; + constructor( @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService private readonly fileService: IFileService, @@ -32,21 +39,33 @@ export class ExtensionsDownloader extends Disposable { ) { super(); this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath); - this.cache = 20; // Cache 20 downloads + this.cache = 20; // Cache 20 downloaded VSIX files this.cleanUpPromise = this.cleanUp(); } - async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise { + async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ extensionLocation: URI; signatureArchiveLocation?: URI }> { await this.cleanUpPromise; const vsixName = this.getName(extension); - const location = joinPath(this.extensionsDownloadDir, vsixName); + const extensionLocation = joinPath(this.extensionsDownloadDir, vsixName); + let signatureArchiveLocation: URI | undefined; - // Download only if vsix does not exist + await this.downloadFile(extensionLocation, extension, operation, 'vsix', this.extensionGalleryService.download); + + if (extension.assets.signature) { + signatureArchiveLocation = ExtensionsDownloader.getSignatureArchiveLocation(extensionLocation); + + await this.downloadFile(signatureArchiveLocation, extension, operation, 'signature archive', this.extensionGalleryService.downloadSignatureArchive); + } + + return { extensionLocation, signatureArchiveLocation }; + } + + private async downloadFile(location: URI, extension: IGalleryExtension, operation: InstallOperation, fileType: string, download: (extension: IGalleryExtension, location: URI, operation: InstallOperation) => Promise) { if (!await this.fileService.exists(location)) { - // Download to temporary location first only if vsix does not exist + // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); if (!await this.fileService.exists(tempLocation)) { - await this.extensionGalleryService.download(extension, tempLocation, operation); + await download(extension, tempLocation, operation); } try { @@ -57,16 +76,13 @@ export class ExtensionsDownloader extends Disposable { await this.fileService.del(tempLocation); } catch (e) { /* ignore */ } if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id); + this.logService.info(`Rename failed because ${fileType} was downloaded by another source. So ignoring renaming.`, extension.identifier.id); } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path); + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the ${fileType} from downloaded location`, tempLocation.path); throw error; } } - } - - return location; } async delete(location: URI): Promise { @@ -89,15 +105,25 @@ export class ExtensionsDownloader extends Disposable { private async cleanUp(): Promise { try { if (!(await this.fileService.exists(this.extensionsDownloadDir))) { - this.logService.trace('Extension VSIX downlads cache dir does not exist'); + this.logService.trace('Extension VSIX downloads cache dir does not exist'); return; } const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true }); if (folderStat.children) { const toDelete: URI[] = []; const all: [ExtensionKey, IFileStatWithMetadata][] = []; + const signatureArchives = new Set(); + for (const stat of folderStat.children) { - const extension = ExtensionKey.parse(stat.name); + const name = stat.name; + + if (ExtensionsDownloader.isSignatureArchive(name)) { + const extensionPath = ExtensionsDownloader.getExtensionPath(name); + signatureArchives.add(extensionPath); + continue; + } + + const extension = ExtensionKey.parse(name); if (extension) { all.push([extension, stat]); } @@ -111,9 +137,21 @@ export class ExtensionsDownloader extends Disposable { } distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest + await Promises.settled(toDelete.map(resource => { this.logService.trace('Deleting vsix from cache', resource.path); - return this.fileService.del(resource); + + let promise = Promise.resolve(); + + if (signatureArchives.has(resource.fsPath)) { + const signatureArchiveLocation = ExtensionsDownloader.getSignatureArchiveLocation(resource); + + promise = promise.then(() => this.fileService.del(signatureArchiveLocation)); + } + + promise = promise.then(() => this.fileService.del(resource)); + + return promise; })); } } catch (e) { @@ -121,6 +159,18 @@ export class ExtensionsDownloader extends Disposable { } } + private static getExtensionPath(signatureArchivePath: string): string { + return signatureArchivePath.substring(0, signatureArchivePath.length - ExtensionsDownloader.SignatureArchiveExtension.length); + } + + private static getSignatureArchiveLocation(extensionLocation: URI): URI { + return URI.file(extensionLocation.fsPath + ExtensionsDownloader.SignatureArchiveExtension); + } + + private static isSignatureArchive(name: string): boolean { + return name.endsWith(ExtensionsDownloader.SignatureArchiveExtension); + } + private getName(extension: IGalleryExtension): string { return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid(); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e8a8e731abb..7bab877fd03 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,7 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { getErrorMessage } from 'vs/base/common/errors'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -30,7 +30,7 @@ import { import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { ExtensionsDownloader, IExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; @@ -44,9 +44,12 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -interface InstallableExtension { +export interface InstallableExtension { zipPath: string; + signatureArchivePath?: string; key: ExtensionKey; metadata: Metadata; } @@ -79,6 +82,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService ) { super(galleryService, telemetryService, logService, productService, userDataProfilesService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); @@ -203,7 +208,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const key = ExtensionKey.create(extension).toString(); installExtensionTask = this.installGalleryExtensionsTasks.get(key); if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.logService)); + this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.logService, this.configurationService, this.extensionSignatureVerificationService)); installExtensionTask.waitUntilTaskIsFinished().then(() => this.installGalleryExtensionsTasks.delete(key)); } } @@ -247,7 +252,19 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } -class ExtensionsScanner extends Disposable { +export interface IExtensionsScanner extends IDisposable { + extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise; + getUninstalledExtensions(): Promise>; + removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise; + removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise; + scanExtensions(type: ExtensionType | null, profileLocation: URI | undefined): Promise; + scanUserExtensions(excludeOutdated: boolean): Promise; + setInstalled(extensionKey: ExtensionKey): Promise; + setUninstalled(...extensions: ILocalExtension[]): Promise; + updateMetadata(local: ILocalExtension, metadata: Partial): Promise; +} + +class ExtensionsScanner extends Disposable implements IExtensionsScanner { private readonly uninstalledPath: string; private readonly uninstalledFileLimiter: Queue; @@ -463,6 +480,7 @@ class ExtensionsScanner extends Disposable { updated: !!extension.metadata?.updated, }; } + private async removeUninstalledExtensions(): Promise { const uninstalled = await this.getUninstalledExtensions(); const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions @@ -528,6 +546,8 @@ class ExtensionsScanner extends Disposable { abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtension; metadata: Metadata }> implements IInstallExtensionTask { + public wasVerified: boolean = false; + protected _operation = InstallOperation.Install; get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } @@ -535,7 +555,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILoca readonly identifier: IExtensionIdentifier, readonly source: URI | IGalleryExtension, protected readonly options: InstallOptions, - protected readonly extensionsScanner: ExtensionsScanner, + protected readonly extensionsScanner: IExtensionsScanner, protected readonly logService: ILogService, ) { super(); @@ -584,15 +604,17 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILoca } -class InstallGalleryExtensionTask extends InstallExtensionTask { +export class InstallGalleryExtensionTask extends InstallExtensionTask { constructor( private readonly manifest: IExtensionManifest, private readonly gallery: IGalleryExtension, options: InstallOptions, - private readonly extensionsDownloader: ExtensionsDownloader, - extensionsScanner: ExtensionsScanner, + private readonly extensionsDownloader: IExtensionsDownloader, + extensionsScanner: IExtensionsScanner, logService: ILogService, + private readonly configurationService: IConfigurationService, + private readonly extensionVerificationService: IExtensionSignatureVerificationService ) { super(gallery.identifier, gallery, options, extensionsScanner, logService); } @@ -628,33 +650,61 @@ class InstallGalleryExtensionTask extends InstallExtensionTask { return { local, metadata }; } - const zipPath = await this.downloadExtension(this.gallery, this._operation); + const { zipPath, signatureArchivePath } = await this.downloadExtension(this.gallery, this._operation); + + if (this.isSignatureVerificationEnabled() && signatureArchivePath) { + try { + this.wasVerified = await this.extensionVerificationService.verify(zipPath, signatureArchivePath); + } catch (error) { + await this.deleteDownloadedFile(zipPath); + await this.deleteDownloadedFile(signatureArchivePath, 'signature archive'); + + const code: string = (error as ExtensionSignatureVerificationError).code; + + throw new ExtensionManagementError(code, ExtensionManagementErrorCode.Signature); + } + } + try { - const local = await this.installExtension({ zipPath, key: ExtensionKey.create(this.gallery), metadata }, token); + const local = await this.installExtension({ zipPath, signatureArchivePath, key: ExtensionKey.create(this.gallery), metadata }, token); if (existingExtension && !this.options.profileLocation && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) { await this.extensionsScanner.setUninstalled(existingExtension); } return { local, metadata }; } catch (error) { - await this.deleteDownloadedVSIX(zipPath); + await this.deleteDownloadedFile(zipPath); + + if (signatureArchivePath) { + await this.deleteDownloadedFile(signatureArchivePath, 'signature archive'); + } + throw error; } } - private async deleteDownloadedVSIX(vsix: string): Promise { + private async deleteDownloadedFile(filePath: string, fileType: string = 'vsix'): Promise { try { - await this.extensionsDownloader.delete(URI.file(vsix)); + await this.extensionsDownloader.delete(URI.file(filePath)); } catch (error) { /* Ignore */ - this.logService.warn('Error while deleting the downloaded vsix', vsix.toString(), getErrorMessage(error)); + this.logService.warn(`Error while deleting the downloaded ${fileType}`, filePath.toString(), getErrorMessage(error)); } } - private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise { + protected async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ zipPath: string; signatureArchivePath?: string }> { let zipPath: string | undefined; + let signatureArchivePath: string | undefined; + try { this.logService.trace('Started downloading extension:', extension.identifier.id); - zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath; + const uris = (await this.extensionsDownloader.downloadExtension(extension, operation)); + + zipPath = uris.extensionLocation.fsPath; + + if (uris.signatureArchiveLocation) { + signatureArchivePath = uris.signatureArchiveLocation.fsPath; + } + this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); } catch (error) { throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Download); @@ -662,12 +712,17 @@ class InstallGalleryExtensionTask extends InstallExtensionTask { try { await getManifest(zipPath); - return zipPath; + return { zipPath, signatureArchivePath }; } catch (error) { - await this.deleteDownloadedVSIX(zipPath); + await this.deleteDownloadedFile(zipPath); throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid); } } + + private isSignatureVerificationEnabled(): boolean { + return this.configurationService.getValue('extensions.verifySignature') ?? false; + } + } class InstallVSIXTask extends InstallExtensionTask { @@ -677,7 +732,7 @@ class InstallVSIXTask extends InstallExtensionTask { private readonly location: URI, options: InstallOptions, private readonly galleryService: IExtensionGalleryService, - extensionsScanner: ExtensionsScanner, + extensionsScanner: IExtensionsScanner, logService: ILogService, ) { super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, logService); @@ -717,7 +772,7 @@ class InstallVSIXTask extends InstallExtensionTask { } } - const local = await this.installExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, token); + const local = await this.installExtension({ zipPath: path.resolve(this.location.fsPath), signatureArchivePath: undefined, key: extensionKey, metadata }, token); return { local, metadata }; } @@ -784,7 +839,7 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni constructor( readonly extension: ILocalExtension, private readonly options: UninstallOptions, - private readonly extensionsScanner: ExtensionsScanner, + private readonly extensionsScanner: IExtensionsScanner, ) { super(); } diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts new file mode 100644 index 00000000000..c3e4e9ec770 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const EXTENSION_SIGNATURE_VERIFICATION_SERVICE_ID = 'extensionSignatureVerificationService'; +export const IExtensionSignatureVerificationService = createDecorator(EXTENSION_SIGNATURE_VERIFICATION_SERVICE_ID); + +/** + * A service for verifying signed extensions. + */ +export interface IExtensionSignatureVerificationService { + readonly _serviceBrand: undefined; + + /** + * Verifies an extension file (.vsix) against a signature archive file. + * @param { string } vsixFilePath The extension file path. + * @param { string } signatureArchiveFilePath The signature archive file path. + * @returns { Promise } A promise with `true` if the extension is validly signed and trusted; + * otherwise, `false` because verification is not enabled (e.g.: in the OSS version of VS Code). + * @throws { ExtensionVerificationError } An error with a code indicating the validity, integrity, or trust issue + * found during verification or a more fundamental issue (e.g.: a required dependency was not found). + */ + verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise; +} + +declare module vsceSign { + export function verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise; +} + +/** + * An error raised during extension signature verification. + */ +export interface ExtensionSignatureVerificationError extends Error { + code: string; +} + +export class ExtensionSignatureVerificationService implements IExtensionSignatureVerificationService { + declare readonly _serviceBrand: undefined; + + private moduleLoadingPromise: Promise | undefined; + + private vsceSign(): Promise { + if (!this.moduleLoadingPromise) { + this.moduleLoadingPromise = new Promise( + (resolve, reject) => require( + ['vsce-sign'], + async (obj) => { + const instance = obj; + + return resolve(instance); + }, reject)); + } + + return this.moduleLoadingPromise; + } + + public async verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise { + const vsceSign = await this.vsceSign(); + + if (!vsceSign) { + return false; + } + + return vsceSign.verify(vsixFilePath, signatureArchiveFilePath); + } +} diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts new file mode 100644 index 00000000000..dcff888284f --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { tmpdir } from 'os'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { randomPath } from 'vs/base/common/extpath'; +import { join } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { mock } from 'vs/base/test/common/mock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ExtensionManagementErrorCode, IGalleryExtension, ILocalExtension, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { IExtensionsScanner, InstallableExtension, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; + +class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { + + private readonly _manifest: IExtensionManifest; + private readonly _gallery: IGalleryExtension; + private _installExtensionWasCalled = false; + + public get installExtensionWasCalled(): boolean { + return this._installExtensionWasCalled; + } + + constructor( + manifest: IExtensionManifest, + gallery: IGalleryExtension, + options: InstallOptions, + extensionsDownloader: IExtensionsDownloader, + extensionsScanner: IExtensionsScanner, + logService: ILogService, + extensionSignatureVerificationService: IExtensionSignatureVerificationService, + private readonly extensionLocation: URI, + private readonly signatureArchiveLocation: URI, + configurationService: IConfigurationService + ) { + super(manifest, gallery, options, extensionsDownloader, extensionsScanner, logService, configurationService, extensionSignatureVerificationService); + + this._manifest = manifest; + this._gallery = gallery; + } + + protected override downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<{ zipPath: string; signatureArchivePath?: string }> { + const uris = { + zipPath: this.extensionLocation.fsPath, + signatureArchivePath: this.signatureArchiveLocation.fsPath + }; + + return Promise.resolve(uris); + } + + protected override installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise { + this._installExtensionWasCalled = true; + + const extension = { + identifier: this._gallery.identifier, + installedTimestamp: new Date().getDate(), + isApplicationScoped: true, + isBuiltin: false, + isMachineScoped: false, + isPreReleaseVersion: this._gallery.properties.isPreReleaseVersion, + isSigned: !!this.signatureArchiveLocation, + isValid: true, + location: this.extensionLocation, + manifest: this._manifest, + preRelease: this._gallery.properties.isPreReleaseVersion, + publisherDisplayName: this._gallery.publisherDisplayName, + publisherId: this._gallery.publisherId, + targetPlatform: TargetPlatform.UNKNOWN, + type: ExtensionType.User, + updated: true, + validations: [] + }; + + return Promise.resolve(extension); + } + +} + +suite('InstallGalleryExtensionTask Tests', () => { + let extension: IGalleryExtension; + let extensionLocation: URI; + let extensionsDownloader: IExtensionsDownloader; + let extensionsScanner: IExtensionsScanner; + let logService: ILogService; + let manifest: IExtensionManifest; + let options: InstallOptions; + let signatureArchiveLocation: URI; + let testDirectory: string; + + setup(async () => { + logService = new NullLogService(); + extensionsDownloader = new class extends mock() { }; + extensionsScanner = new class extends mock() { + override async scanExtensions(type: ExtensionType | null, profileLocation: URI | undefined): Promise { + return []; + } + }; + + testDirectory = join(tmpdir(), randomPath(), 'galleryinstall'); + extensionLocation = URI.file(join(testDirectory, 'extension.vsix')); + signatureArchiveLocation = URI.file(join(testDirectory, 'extension.sigzip')); + + manifest = { + name: 'name', + version: '1.2.3', + publisher: 'publisher', + engines: { vscode: '^1.66.0' } + }; + + extension = { + name: manifest.name, + version: manifest.version, + identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: generateUuid() }, + displayName: 'displayName', + publisherId: 'publisherId', + publisher: 'publisher', + publisherDisplayName: 'publisherDisplayName', + description: 'description', + installCount: 7, + rating: 4, + ratingCount: 9, + categories: [], + tags: [], + releaseDate: new Date().getDate(), + lastUpdated: new Date().getDate(), + preview: false, + hasPreReleaseVersion: false, + hasReleaseVersion: true, + allTargetPlatforms: [], + assets: { + manifest: { uri: 'uri:manifest', fallbackUri: 'fallback:manifest' }, + readme: { uri: 'uri:readme', fallbackUri: 'fallback:readme' }, + changelog: { uri: 'uri:changelog', fallbackUri: 'fallback:changlog' }, + download: { uri: 'uri:download', fallbackUri: 'fallback:download' }, + icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, + license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, + repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, + coreTranslations: [] + }, + properties: { targetPlatform: TargetPlatform.UNKNOWN, isPreReleaseVersion: false }, + isSigned: true + }; + + options = {}; + }); + + function CreateTask(func: () => Promise, isSignatureVerificationEnabled?: boolean): TestInstallGalleryExtensionTask { + const configuration = isSignatureVerificationEnabled ? { extensions: { verifySignature: isSignatureVerificationEnabled } } : null; + const configurationService = new TestConfigurationService(configuration); + const extensionSignatureVerificationService = new class extends mock() { + override async verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise { + return func(); + } + }; + + return new TestInstallGalleryExtensionTask( + manifest, + extension, + options, + extensionsDownloader, + extensionsScanner, + logService, + extensionSignatureVerificationService, + extensionLocation, + signatureArchiveLocation, + configurationService); + } + + test('if verification is disabled by settings, the task skips verification', async () => { + const task: TestInstallGalleryExtensionTask = CreateTask(() => { + const error = new Error() as ExtensionSignatureVerificationError; + + error.code = 'If this error is thrown, it is a bug. Verification should be skipped.'; + + throw error; + }); + + await task.run(); + + assert.strictEqual(task.wasVerified, false); + assert.strictEqual(task.installExtensionWasCalled, true); + }); + + test('if verification is disabled because the module is not loaded, the task skips verification', async () => { + const task: TestInstallGalleryExtensionTask = CreateTask(() => Promise.resolve(false), true); + + await task.run(); + + assert.strictEqual(task.wasVerified, false); + assert.strictEqual(task.installExtensionWasCalled, true); + }); + + test('if verification fails, the task throws', async () => { + const errorCode = 'IntegrityCheckFailed'; + + const task: TestInstallGalleryExtensionTask = CreateTask(() => { + const error = new Error() as ExtensionSignatureVerificationError; + + error.code = errorCode; + + throw error; + }, true); + + try { + await task.run(); + + assert.fail('It should have thrown.'); + } catch (e) { + assert.ok(e instanceof Error); + + const extensionVerificationError = e as ExtensionSignatureVerificationError; + + assert.ok(extensionVerificationError); + assert.strictEqual(extensionVerificationError.code, ExtensionManagementErrorCode.Signature); + assert.strictEqual(extensionVerificationError.message, errorCode); + } + + assert.strictEqual(task.wasVerified, false); + assert.strictEqual(task.installExtensionWasCalled, false); + }); + + test('if verification succeeds, the task completes', async () => { + const task: TestInstallGalleryExtensionTask = CreateTask(() => Promise.resolve(true), true); + + await task.run(); + + assert.strictEqual(task.wasVerified, true); + assert.strictEqual(task.installExtensionWasCalled, true); + }); +}); diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 086f7dec035..0b4cab3b1cd 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -15,6 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/common/product'; @@ -110,6 +111,7 @@ class CliMain extends Disposable { services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 0dc354e378b..20d4cab1068 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -25,6 +25,7 @@ import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMai import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -167,6 +168,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index b9801ecbecb..7da15b1307b 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -85,6 +85,7 @@ const mockExtensionGallery: IGalleryExtension[] = [ icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }), aGalleryExtension('MockExtension2', { @@ -107,6 +108,7 @@ const mockExtensionGallery: IGalleryExtension[] = [ icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }) ]; @@ -166,6 +168,7 @@ const noAssets: IGalleryExtensionAssets = { manifest: null, readme: null, repository: null, + signature: null, coreTranslations: [] }; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index f862b70fb53..4d6cd0844a6 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -163,6 +163,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }); @@ -313,6 +314,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local1, local2]); @@ -1439,6 +1441,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { manifest: null, readme: null, repository: null, + signature: null, coreTranslations: [] };