-
Notifications
You must be signed in to change notification settings - Fork 66
Add optional Artifact Signing for Windows builds #2995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
c3de091
5be42af
bfd68f1
a465408
939c147
f0cf79a
0700241
08dc82c
ec67144
82cf136
6b940af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { isErrnoException } from '@studio/common/lib/is-errno-exception'; | |
| import { exec } from 'child_process'; | ||
| import { exec as pkgExec } from '@yao-pkg/pkg'; | ||
| import type { ForgeConfig } from '@electron-forge/shared-types'; | ||
| import { windowsSign } from './windowsSign'; | ||
|
|
||
| const repoRoot = path.resolve( __dirname, '../..' ); | ||
|
|
||
|
|
@@ -22,6 +23,7 @@ const config: ForgeConfig = { | |
| ], | ||
| executableName: process.platform === 'linux' ? 'studio' : undefined, | ||
| icon: path.join( __dirname, 'assets', 'studio-app-icon' ), | ||
| windowsSign, | ||
| osxSign: { | ||
| optionsForFile: ( filePath ) => { | ||
| // The bundled Node binary requires specific entitlements for V8 JIT compilation. | ||
|
|
@@ -95,9 +97,16 @@ const config: ForgeConfig = { | |
|
|
||
| setupExe: 'studio-setup.exe', | ||
|
|
||
| // CI code-signing setup writes certificate.pfx at the repository root. | ||
| certificateFile: path.join( repoRoot, 'certificate.pfx' ), | ||
| certificatePassword: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, | ||
| // Azure mode: use the custom signing hook that calls signtool | ||
| // with Azure Trusted Signing parameters. | ||
| // PFX mode: use the local certificate file and password. | ||
| ...( windowsSign | ||
| ? { windowsSign } | ||
| : { | ||
| certificateFile: path.join( repoRoot, 'certificate.pfx' ), | ||
| certificatePassword: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, | ||
| } | ||
| ), | ||
|
Comment on lines
+100
to
+109
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once we'll have established that the new code signing is good, we'll be able to merge the checks done inside As it stands, this code looks a bit ugly, but it's a temporary compromise. |
||
| }, | ||
| [ 'win32' ] | ||
| ), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||
| import path from 'path'; | ||||||||||||||||||
| import type { WindowsSignOptions } from '@electron/packager'; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Azure Trusted Signing configuration for Windows code signing. | ||||||||||||||||||
| // | ||||||||||||||||||
| // Uses a custom hook module because the default @electron/windows-sign | ||||||||||||||||||
| // dual-signs (SHA1 + SHA256), but Azure only supports SHA256. | ||||||||||||||||||
| // The hook calls signtool directly with SHA256-only parameters. | ||||||||||||||||||
| // | ||||||||||||||||||
| // Controlled by the USE_AZURE_TRUSTED_SIGNING env var: | ||||||||||||||||||
| // - Unset/falsy: returns undefined, letting Forge use PFX certificate signing. | ||||||||||||||||||
| // - Set: returns the Azure signing hook config, or throws if the | ||||||||||||||||||
| // required Azure env vars are missing. | ||||||||||||||||||
| function getWindowsSign(): WindowsSignOptions | undefined { | ||||||||||||||||||
| if ( ! process.env.USE_AZURE_TRUSTED_SIGNING ) { | ||||||||||||||||||
| return undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if ( ! process.env.AZURE_CODE_SIGNING_DLIB || ! process.env.AZURE_METADATA_JSON ) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| 'USE_AZURE_TRUSTED_SIGNING is set but Azure signing env vars ' + | ||||||||||||||||||
| '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON) are missing. ' + | ||||||||||||||||||
|
Comment on lines
+19
to
+22
|
||||||||||||||||||
| if ( ! process.env.AZURE_CODE_SIGNING_DLIB || ! process.env.AZURE_METADATA_JSON ) { | |
| throw new Error( | |
| 'USE_AZURE_TRUSTED_SIGNING is set but Azure signing env vars ' + | |
| '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON) are missing. ' + | |
| if ( ! process.env.AZURE_CODE_SIGNING_DLIB || ! process.env.AZURE_METADATA_JSON || ! process.env.SIGNTOOL_PATH ) { | |
| throw new Error( | |
| 'USE_AZURE_TRUSTED_SIGNING is set but Azure signing env vars ' + | |
| '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON, SIGNTOOL_PATH) are missing. ' + |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // Custom signing hook for @electron/windows-sign. | ||
| // Azure Trusted Signing only supports SHA256; the default dual-sign | ||
| // (sha1 + sha256) fails because there's no local cert for sha1. | ||
| // This hook calls signtool directly with SHA256-only parameters. | ||
|
|
||
| /* eslint-disable @typescript-eslint/no-require-imports */ | ||
|
|
||
| const { execFileSync } = require( 'child_process' ); | ||
| const path = require( 'path' ); | ||
| const { | ||
| assertAzureSigningEnv, | ||
| getAzureSignArgs, | ||
| getAzureSigningConfig, | ||
| } = require( './azure-signing.cjs' ); | ||
|
|
||
| // @electron/windows-sign includes .ps1 shim scripts from node_modules/.bin | ||
| // in its file list. These are text files that don't need code signing. | ||
| const SKIP_EXTENSIONS = new Set( [ '.ps1', '.vbs', '.wsf' ] ); | ||
|
|
||
| module.exports = function ( fileToSign ) { | ||
| if ( SKIP_EXTENSIONS.has( path.extname( fileToSign ).toLowerCase() ) ) { | ||
| return; | ||
| } | ||
| assertAzureSigningEnv(); | ||
| const { signtoolPath } = getAzureSigningConfig(); | ||
|
|
||
| execFileSync( signtoolPath, getAzureSignArgs( fileToSign ), { stdio: 'inherit' } ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| const REQUIRED_ENV_VARS = [ | ||
| 'AZURE_CODE_SIGNING_DLIB', | ||
| 'AZURE_METADATA_JSON', | ||
| 'SIGNTOOL_PATH' | ||
| ]; | ||
|
|
||
| const DEFAULTS = { | ||
| fileDigestAlgorithm: 'SHA256', | ||
| timestampDigestAlgorithm: 'SHA256', | ||
| timestampServer: 'http://timestamp.acs.microsoft.com', | ||
| }; | ||
|
|
||
| function getAzureSigningConfig() { | ||
| return { | ||
| signtoolPath: process.env.SIGNTOOL_PATH, | ||
| dlib: process.env.AZURE_CODE_SIGNING_DLIB, | ||
| metadata: process.env.AZURE_METADATA_JSON, | ||
| fileDigestAlgorithm: process.env.AZURE_FILE_DIGEST || DEFAULTS.fileDigestAlgorithm, | ||
| timestampDigestAlgorithm: | ||
| process.env.AZURE_TIMESTAMP_DIGEST || DEFAULTS.timestampDigestAlgorithm, | ||
| timestampServer: process.env.AZURE_TIMESTAMP_SERVER || DEFAULTS.timestampServer, | ||
| }; | ||
| } | ||
|
|
||
| function assertAzureSigningEnv() { | ||
| for ( const envVar of REQUIRED_ENV_VARS ) { | ||
| if ( ! process.env[ envVar ] ) { | ||
| throw new Error( `Required env var ${ envVar } is not set!` ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function getAzureSignArgs( fileToSign, extraArgs = [] ) { | ||
| const { dlib, metadata, fileDigestAlgorithm, timestampDigestAlgorithm, timestampServer } = | ||
| getAzureSigningConfig(); | ||
|
|
||
| return [ | ||
| 'sign', | ||
| '/v', | ||
| ...extraArgs, | ||
| '/fd', | ||
| fileDigestAlgorithm, | ||
| '/tr', | ||
| timestampServer, | ||
| '/td', | ||
| timestampDigestAlgorithm, | ||
| '/dlib', | ||
| dlib, | ||
| '/dmdf', | ||
| metadata, | ||
| fileToSign, | ||
| ]; | ||
| } | ||
|
|
||
| module.exports = { | ||
| assertAzureSigningEnv, | ||
| getAzureSignArgs, | ||
| getAzureSigningConfig, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,32 @@ | ||
| import { execFileSync } from 'child_process'; | ||
| import fs from 'fs/promises'; | ||
| import path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| import convertToWindowsStore from 'electron2appx'; | ||
| import packageJson from '../apps/studio/package.json' with { type: 'json' }; | ||
|
|
||
| const useAzureSigning = [ '1', 'true' ].includes( | ||
| process.env.USE_AZURE_TRUSTED_SIGNING?.trim().toLowerCase() | ||
| ); | ||
|
|
||
| console.log( '--- :electron: Packaging AppX' ); | ||
|
|
||
| console.log( '~~~ Verifying WINDOWS_CODE_SIGNING_CERT_PASSWORD env var...' ); | ||
| if ( ! process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD ) { | ||
| console.error( 'Required env var WINDOWS_CODE_SIGNING_CERT_PASSWORD is not set!' ); | ||
| process.exit( 1 ); | ||
| if ( useAzureSigning ) { | ||
| const azureSigning = await import( './azure-signing.cjs' ); | ||
| const { assertAzureSigningEnv } = azureSigning.default; | ||
| console.log( '~~~ Verifying Azure Trusted Signing env vars...' ); | ||
| try { | ||
| assertAzureSigningEnv(); | ||
| } catch ( error ) { | ||
| console.error( error instanceof Error ? error.message : error ); | ||
| process.exit( 1 ); | ||
| } | ||
| } else { | ||
| console.log( '~~~ Verifying WINDOWS_CODE_SIGNING_CERT_PASSWORD env var...' ); | ||
| if ( ! process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD ) { | ||
| console.error( 'Required env var WINDOWS_CODE_SIGNING_CERT_PASSWORD is not set!' ); | ||
| process.exit( 1 ); | ||
| } | ||
| } | ||
|
|
||
| const __dirname = path.dirname( fileURLToPath( import.meta.url ) ); | ||
|
|
@@ -167,14 +184,58 @@ await convertToWindowsStore( { | |
| outputDirectory: appxOutputPathUnsigned, | ||
| } ); | ||
|
|
||
| // Create signed AppX | ||
| // Create signed AppX (used for local testing via sideloading) | ||
| const appxOutputPathSigned = path.resolve( outPath, `${ appxName }-${ architecture }-signed` ); | ||
| console.log( `~~~ Creating signed .appx for local testing at ${ appxOutputPathSigned }...` ); | ||
|
|
||
| await convertToWindowsStore( { | ||
| ...sharedOptions, | ||
| publisher: 'CN="Automattic, Inc.", O="Automattic, Inc.", S=California, C=US', | ||
| devCert: 'certificate.pfx', | ||
| certPass: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, | ||
| outputDirectory: appxOutputPathSigned, | ||
| } ); | ||
| if ( useAzureSigning ) { | ||
| const sideloadPublisher = | ||
| 'CN=Automattic Inc., O=Automattic Inc., L=San Francisco, S=California, C=US'; | ||
|
|
||
| // Build unsigned, then sign with Azure Trusted Signing via signtool. | ||
| await convertToWindowsStore( { | ||
| ...sharedOptions, | ||
| publisher: sideloadPublisher, | ||
| devCert: 'nil', | ||
| outputDirectory: appxOutputPathSigned, | ||
| } ); | ||
|
|
||
| console.log( '~~~ Signing sideload .appx with Azure Trusted Signing...' ); | ||
| const azureSigning = await import( './azure-signing.cjs' ); | ||
| const { getAzureSignArgs, getAzureSigningConfig } = azureSigning.default; | ||
| const appxFiles = ( await fs.readdir( appxOutputPathSigned ) ).filter( ( f ) => | ||
| f.endsWith( '.appx' ) | ||
| ); | ||
| if ( appxFiles.length === 0 ) { | ||
| console.error( 'No .appx file found to sign!' ); | ||
| process.exit( 1 ); | ||
| } | ||
|
|
||
| for ( const appxFile of appxFiles ) { | ||
| const appxPath = path.join( appxOutputPathSigned, appxFile ); | ||
| console.log( `Signing ${ appxPath }...` ); | ||
| const { signtoolPath } = getAzureSigningConfig(); | ||
| execFileSync( signtoolPath, getAzureSignArgs( appxPath, [ '/debug' ] ), { | ||
| stdio: 'inherit', | ||
| } ); | ||
| console.log( `Signed ${ appxFile } successfully.` ); | ||
|
|
||
| // Rename to remove misleading "unsigned" from the filename | ||
| const renamedFile = appxFile.replace( ' unsigned', '' ); | ||
| if ( renamedFile !== appxFile ) { | ||
| const renamedPath = path.join( appxOutputPathSigned, renamedFile ); | ||
| await fs.rename( appxPath, renamedPath ); | ||
| console.log( `Renamed to ${ renamedFile }` ); | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } else { | ||
| // PFX certificate signing | ||
| await convertToWindowsStore( { | ||
| ...sharedOptions, | ||
| publisher: | ||
| 'CN="Automattic, Inc.", O="Automattic, Inc.", S=California, C=US', | ||
| devCert: 'certificate.pfx', | ||
| certPass: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, | ||
| outputDirectory: appxOutputPathSigned, | ||
| } ); | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Waiting for confirmation on the tooling from a few more clients before shipping a new version of the plugin. A commit sha doesn't look as neat as a tag, but the pinning result is the same.