Skip to content
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

Support for Azure Trusted Signing in Electron Builder #8276

Closed
iliakolev opened this issue Jun 26, 2024 · 55 comments
Closed

Support for Azure Trusted Signing in Electron Builder #8276

iliakolev opened this issue Jun 26, 2024 · 55 comments

Comments

@iliakolev
Copy link

iliakolev commented Jun 26, 2024

  • Electron-Builder Version: 24.13.3
  • Node Version: 18.16.0
  • Electron Version: 31.0.2
  • Electron Type (current, beta, nightly): electron-updater: 6.2.1
  • Target: Windows

I'm inquiring about the possibility of integrating Azure Trusted Signing within the Electron Builder workflow. Currently, my build process involves:

  1. Signing the executable using the azure/[email protected] GitHub Action.
  2. Packaging and releasing the signed executable, ensuring auto-update functionality with s3 provider.

If you could provide guidance on how to integrate Azure Trusted Signing more seamlessly with Electron Builder or if there are plans to support this in the future, it would be greatly appreciated.

Thank you for your time and assistance.

@mmaietta
Copy link
Collaborator

mmaietta commented Jul 6, 2024

I guess a lot of those powershell commands could be copy-pasted/translated into electron-builder's signing workflow, but if any updates happen to that github action, they won't be propagated into electron-builder without someone opening an issue here. Would that be a problem?

@jmeinke
Copy link
Contributor

jmeinke commented Jul 16, 2024

We're also interested in this. There is a nice guide for implementation of trusted EV code signing using Azure here: https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/

Basically, we'd need to call the action multiple times:

  1. on the executable files after they have been built
  2. on the installer executable after everything has been packaged in

Currently, it seems that steps 1 and 2 are done in one action and therefore it's not possible to execute code signing after each of the steps using the GitHub action, right?

@MikeJerred
Copy link
Contributor

From the github action code it looks like a powershell script that installs the TrustedSigning module and then invokes it with appropriate params is all that is required:

Install-Module -Name TrustedSigning -RequiredVersion 0.4.1 -Force -Repository PSGallery
Invoke-TrustedSigning @params

This should be possible to add to electron-builder with some extra env vars?

@mmaietta
Copy link
Collaborator

I quickly put together a basic copy of the action code for an afterSign hook, which maybe you'd be able to test with.

Basically, we skip the signing stage for the regular signing process. Instead, we leverage the afterSign for running the powershell commands.

Would you be willing to do some investigating from there? Not sure if the params need to be passed in as a var or as a literal string. I don't have any azure account to test this myself

// electron-builder-config.ts
import { AfterPackContext, Configuration } from "app-builder-lib"
import { CustomWindowsSignTaskConfiguration } from "app-builder-lib/out/codeSign/windowsCodeSign"
import { WinPackager } from "app-builder-lib/out/winPackager"
import { execFile } from "child_process"

// Configuration object
export default {
     // base configuration
     win: {
        // block signing
        sign: (_configuration: CustomWindowsSignTaskConfiguration, _packager: WinPackager | undefined) => {
            return Promise.resolve()
        }
    },
    afterSign: async (context: AfterPackContext) => {
        await new Promise((resolve, reject) => execFile(
            `chcp 65001 >NUL & powershell.exe`,
            ["-Name", "TrustedSigning", "-RequiredVersion", "0.4.1", "-Force", "-Repository", "PSGallery",],
            {
                shell: true,
                timeout: 60 * 1000,
            },
            (error, stdout, stderr) => {
                if (error || stderr) {
                    reject(error?.message || stderr)
                }
                resolve(stdout)
            }
        ))

        // requires the following env vars also set:
        // AZURE_TENANT_ID
        // AZURE_CLIENT_ID
        // AZURE_CLIENT_SECRET
        // AZURE_CLIENT_CERTIFICATE_PATH
        // AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
        // AZURE_USERNAME
        // AZURE_PASSWORD

        const params: any = {}
        // Variables you can define
        const trustedSigningAccountName = undefined
        const certificateProfileName = undefined
        const files = undefined
        const filesFolder = undefined
        const filesFolderFilter = undefined
        const filesFolderRecurse = false // boolean
        const filesFolderDepth = undefined // integer
        const filesCatalog = undefined
        const fileDigest = undefined
        const timestampRfc3161 = undefined
        const timestampDigest = undefined
        const appendSignature = false // boolean
        const description = undefined
        const descriptionUrl = undefined
        const generateDigestPath = undefined
        const generateDigestXml = false // boolean
        const ingestDigestPath = undefined
        const signDigest = false // boolean
        const generatePageHashes = false // boolean
        const suppressPageHashes = false // boolean
        const generatePkcs7 = false // boolean
        const pkcs7Options = undefined
        const pkcs7Oid = undefined
        const enhancedKeyUsage = undefined
        const excludeEnvironmentCredential = false // boolean
        const excludeWorkloadIdentityCredential = false // boolean
        const excludeManagedIdentityCredential = false // boolean
        const excludeSharedTokenCacheCredential = false // boolean
        const excludeVisualStudioCredential = false // boolean
        const excludeVisualStudioCodeCredential = false // boolean
        const excludeAzureCliCredential = false // boolean
        const excludeAzurePowerShellCredential = false // boolean
        const excludeAzureDeveloperCliCredential = false // boolean
        const excludeInteractiveBrowserCredential = false // boolean
        const timeout = undefined // integer
        const batchSize = undefined // integer

        const notNullOrEmptyString = (value: any) => {
            if (typeof value === 'string') {
                return value.trim().length !== 0
            }
            return value !== undefined || value !== null
        }
        if (notNullOrEmptyString(trustedSigningAccountName)) {
            params.CodeSigningAccountName = trustedSigningAccountName
        }
        if (notNullOrEmptyString(certificateProfileName)) {
            params.CertificateProfileName = certificateProfileName
        }
        if (notNullOrEmptyString(files)) {
            params.Files = files
        }
        if (notNullOrEmptyString(filesFolder)) {
            params.FilesFolder = filesFolder
        }
        if (notNullOrEmptyString(filesFolderFilter)) {
            params.FilesFolderFilter = filesFolderFilter
        }
        if (notNullOrEmptyString(filesFolderRecurse)) {
            params.FilesFolderRecurse = filesFolderRecurse // boolean
        }
        if (notNullOrEmptyString(filesFolderDepth)) {
            params.FilesFolderDepth = filesFolderDepth // integer
        }
        if (notNullOrEmptyString(filesCatalog)) {
            params.FilesCatalog = filesCatalog
        }
        if (notNullOrEmptyString(fileDigest)) {
            params.FileDigest = fileDigest
        }
        if (notNullOrEmptyString(timestampRfc3161)) {
            params.TimestampRfc3161 = timestampRfc3161
        }
        if (notNullOrEmptyString(timestampDigest)) {
            params.TimestampDigest = timestampDigest
        }
        if (notNullOrEmptyString(appendSignature)) {
            params.AppendSignature = appendSignature // boolean
        }
        if (notNullOrEmptyString(description)) {
            params.Description = description
        }
        if (notNullOrEmptyString(descriptionUrl)) {
            params.DescriptionUrl = descriptionUrl
        }
        if (notNullOrEmptyString(generateDigestPath)) {
            params.GenerateDigestPath = generateDigestPath
        }
        if (notNullOrEmptyString(generateDigestXml)) {
            params.GenerateDigestXml = generateDigestXml // boolean
        }
        if (notNullOrEmptyString(ingestDigestPath)) {
            params.IngestDigestPath = ingestDigestPath
        }
        if (notNullOrEmptyString(signDigest)) {
            params.SignDigest = signDigest // boolean
        }
        if (notNullOrEmptyString(generatePageHashes)) {
            params.GeneratePageHashes = generatePageHashes // boolean
        }
        if (notNullOrEmptyString(suppressPageHashes)) {
            params.SuppressPageHashes = suppressPageHashes // boolean
        }
        if (notNullOrEmptyString(generatePkcs7)) {
            params.GeneratePkcs7 = generatePkcs7 // boolean
        }
        if (notNullOrEmptyString(pkcs7Options)) {
            params.Pkcs7Options = pkcs7Options
        }
        if (notNullOrEmptyString(pkcs7Oid)) {
            params.Pkcs7Oid = pkcs7Oid
        }
        if (notNullOrEmptyString(enhancedKeyUsage)) {
            params.EnhancedKeyUsage = enhancedKeyUsage
        }
        if (notNullOrEmptyString(excludeEnvironmentCredential)) {
            params.ExcludeEnvironmentCredential = excludeEnvironmentCredential // boolean
        }
        if (notNullOrEmptyString(excludeWorkloadIdentityCredential)) {
            params.ExcludeWorkloadIdentityCredential = excludeWorkloadIdentityCredential // boolean
        }
        if (notNullOrEmptyString(excludeManagedIdentityCredential)) {
            params.ExcludeManagedIdentityCredential = excludeManagedIdentityCredential // boolean
        }
        if (notNullOrEmptyString(excludeSharedTokenCacheCredential)) {
            params.ExcludeSharedTokenCacheCredential = excludeSharedTokenCacheCredential // boolean
        }
        if (notNullOrEmptyString(excludeVisualStudioCredential)) {
            params.ExcludeVisualStudioCredential = excludeVisualStudioCredential // boolean
        }
        if (notNullOrEmptyString(excludeVisualStudioCodeCredential)) {
            params.ExcludeVisualStudioCodeCredential = excludeVisualStudioCodeCredential // boolean
        }
        if (notNullOrEmptyString(excludeAzureCliCredential)) {
            params.ExcludeAzureCliCredential = excludeAzureCliCredential // boolean
        }
        if (notNullOrEmptyString(excludeAzurePowerShellCredential)) {
            params.ExcludeAzurePowerShellCredential = excludeAzurePowerShellCredential // boolean
        }
        if (notNullOrEmptyString(excludeAzureDeveloperCliCredential)) {
            params.ExcludeAzureDeveloperCliCredential = excludeAzureDeveloperCliCredential // boolean
        }
        if (notNullOrEmptyString(excludeInteractiveBrowserCredential)) {
            params.ExcludeInteractiveBrowserCredential = excludeInteractiveBrowserCredential // boolean
        }
        if (notNullOrEmptyString(timeout)) {
            params.Timeout = timeout // integer
        }
        if (notNullOrEmptyString(batchSize)) {
            params.BatchSize = batchSize // integer
        }
        await new Promise((resolve, reject) => execFile(
            `chcp 65001 >NUL & powershell.exe`,
            ["Invoke-TrustedSigning", params],
            {
                shell: true,
                timeout: 60 * 1000,
            },
            (error, stdout, stderr) => {
                if (error || stderr) {
                    reject(error?.message || stderr)
                }
                resolve(stdout)
            }
        ))
    },
}

@MikeJerred
Copy link
Contributor

Can confirm that this approach works! Here is what I ended up using:

electron-builder.yml:

...
afterSign: ./scripts/after-sign.js

win:
  sign: ./scripts/nop.js

scripts/after-sign.js:

const { spawnSync } = require('node:child_process');

exports.default = async function sign(context) {
  spawnSync(
    'powershell.exe',
    ['Install-Module', '-Name', 'TrustedSigning', '-RequiredVersion', '0.4.1', '-Force', '-Repository', 'PSGallery'],
    { shell: true, stdio: 'inherit' },
  );

  const params = {
    Endpoint: 'https://eus.codesigning.azure.net/',
    CodeSigningAccountName: '<code signing account name>',
    CertificateProfileName: '<certificate profile name>',
    FilesFolder: context.appOutDir,
    FilesFolderFilter: 'exe,dll',
    FileDigest: 'SHA256',
    TimestampRfc3161: 'http://timestamp.acs.microsoft.com',
    TimestampDigest: 'SHA256',
  };
  spawnSync('powershell.exe', ['Invoke-TrustedSigning', params], { shell: true, stdio: 'inherit' });
};

scripts/nop.js:

exports.default = async function nop() {};

@jmeinke
Copy link
Contributor

jmeinke commented Jul 31, 2024

@MikeJerred Wouldn't your proposed solution result in only the packaged executables being signed, not the installer (e.g. NSIS executable file) that results from the packaging process? It seems that the after-sign.js is not executed after the installer has been compiled.

Another problem I've spotted: When testing your script, I received the following output + Invoke-TrustedSigning [object Object] plus an error about missing mandatory parameters. It seems to me that passing a compiled string might work better (but still have to test it myself):

    const paramsString = Object.keys(params).map(key => ` -${key} ${params[key]}`).join('');
    spawnSync('powershell.exe', ['Invoke-TrustedSigning', paramsString], { shell: true, stdio: 'inherit' });

@OrganicChem
Copy link

I just received confirmation from MS that I have approval for the trusted signing cert. Based on some of the above hacks, I may have to skip Windows signing and use a signtool manually since installers as well as app needs to be signed.

@jmeinke
Copy link
Contributor

jmeinke commented Aug 12, 2024

Signing the installer after the electron-builder packing process during and extra build step results in wrong sha512 hashes in the resulting update YAML files.

@OrganicChem
Copy link

I've managed to get this signed with my own script during the build...all executables checkout out nicely. Gone are the days of EV certs.

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 9, 2024

I'm looking into implementing this in electron-builder, but won't have a way to test (as I don't have any Azure account). So if anyone is willing, I'd be happy to supply a patch-package patch for testing out my implementation.

What are the required params for Invoke-TrustedSigning?
Just these?

  const params = {
    Endpoint: 'https://eus.codesigning.azure.net/',
    CodeSigningAccountName: '<code signing account name>',
    CertificateProfileName: '<certificate profile name>',
    FilesFolder: context.appOutDir,
    FilesFolderFilter: 'exe,dll',
    FileDigest: 'SHA256',
    TimestampRfc3161: 'http://timestamp.acs.microsoft.com',
    TimestampDigest: 'SHA256',
  };

I also see this example configuration here: https://learn.microsoft.com/en-us/azure/trusted-signing/how-to-signing-integrations

  "Endpoint": "<Trusted Signing account endpoint>",
  "CodeSigningAccountName": "<Trusted Signing account name>",
  "CertificateProfileName": "<Certificate profile name>",

Reason I ask is to see if there are any default values I can apply or using enums (for things like TimestampDigest) where it probably doesn't have to be a basic string property.

@MikeJerred
Copy link
Contributor

I'm looking into implementing this in electron-builder, but won't have a way to test (as I don't have any Azure account). So if anyone is willing, I'd be happy to supply a patch-package patch for testing out my implementation.

What are the required params for Invoke-TrustedSigning? Just these?

  const params = {
    Endpoint: 'https://eus.codesigning.azure.net/',
    CodeSigningAccountName: '<code signing account name>',
    CertificateProfileName: '<certificate profile name>',
    FilesFolder: context.appOutDir,
    FilesFolderFilter: 'exe,dll',
    FileDigest: 'SHA256',
    TimestampRfc3161: 'http://timestamp.acs.microsoft.com',
    TimestampDigest: 'SHA256',
  };

I also see this example configuration here: https://learn.microsoft.com/en-us/azure/trusted-signing/how-to-signing-integrations

  "Endpoint": "<Trusted Signing account endpoint>",
  "CodeSigningAccountName": "<Trusted Signing account name>",
  "CertificateProfileName": "<Certificate profile name>",

Reason I ask is to see if there are any default values I can apply or using enums (for things like TimestampDigest) where it probably doesn't have to be a basic string property.

I am happy to help with testing. Those params (plus the azure auth env vars) were enough for the signing to complete without errors when I tried it.

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 9, 2024

Okay, nvm, the patch is too large. My best recommendation is cloning electron-builder, pulling this PR #8458 via gh pr checkout 8458 or checkout branch azure-signing, compile with pnpm compile, and copy the compiled files into your project directly.
Example setup: https://github.com/electron-userland/electron-builder/blob/master/CONTRIBUTING.md#to-setup-a-local-dev-environment

From there, the configuration is within win.azureOptions (other name suggestions are welcome). I took the minimal required fields I could find in the Azure docs, then left it open with [k: string]: string for custom usage scenarios

export interface WindowsAzureSigningConfiguration {
/**
* The Trusted Signing Account endpoint. The URI value must have a URI that aligns to the
* region your Trusted Signing Account and Certificate Profile you are specifying were created
* in during the setup of these resources.
*
* Requires the following environment variables to be set:
* AZURE_TENANT_ID
* AZURE_CLIENT_ID
* AZURE_CLIENT_SECRET
* AZURE_CLIENT_CERTIFICATE_PATH
* AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
* AZURE_USERNAME
* AZURE_PASSWORD
*/
readonly Endpoint: string
/**
* The Certificate Profile name.
*/
readonly CertificateProfileName: string
/**
* Allow other CLI parameters (verbatim) to `Invoke-TrustedSigning`
*/
[k: string]: string
}

@MikeJerred
Copy link
Contributor

Not sure if my local dev setup is correct because I get an error when doing this:

$ npx electron-builder --dir
Error: Cannot find module 'resedit'
Require stack:
- D:\dev\projects\glint\electron\.yalc\app-builder-lib\out\electron\electronWin.js
- D:\dev\projects\glint\electron\.yalc\app-builder-lib\out\electron\ElectronFramework.js
- D:\dev\projects\glint\electron\.yalc\app-builder-lib\out\packager.js
- D:\dev\projects\glint\electron\.yalc\app-builder-lib\out\index.js
- D:\dev\projects\glint\electron\.yalc\electron-builder\out\builder.js
- D:\dev\projects\glint\electron\.yalc\electron-builder\out\cli\cli.js
- D:\dev\projects\glint\electron\.yalc\electron-builder\cli.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1145:15)
    at Function.Module._load (node:internal/modules/cjs/loader:986:27)
    at Module.require (node:internal/modules/cjs/loader:1233:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (D:\dev\projects\glint\electron\.yalc\app-builder-lib\src\electron\electronWin.ts:3:1)
    at Module._compile (node:internal/modules/cjs/loader:1358:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
    at Module.load (node:internal/modules/cjs/loader:1208:32)
    at Function.Module._load (node:internal/modules/cjs/loader:1024:12)
    at Module.require (node:internal/modules/cjs/loader:1233:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (D:\dev\projects\glint\electron\.yalc\app-builder-lib\src\electron\ElectronFramework.ts:13:1)
    at Module._compile (node:internal/modules/cjs/loader:1358:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
    at Module.load (node:internal/modules/cjs/loader:1208:32)
    at Function.Module._load (node:internal/modules/cjs/loader:1024:12)

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 12, 2024

You'll either need to start with the base v25.0.5 installed in your package.json (as that includes most recent dependencies to be resolved during install) before using yalc or you can temporarily add "resedit": "^1.7.0", to your devDependencies

@MikeJerred
Copy link
Contributor

You'll either need to start with the base v25.0.5 installed in your package.json (as that includes most recent dependencies to be resolved during install) before using yalc or you can temporarily add "resedit": "^1.7.0", to your devDependencies

That has fixed that issue, however it still doesn't work:

$ npx electron-builder --dir
  • electron-builder  version=25.0.6 os=10.0.19045
  • loaded configuration  file=D:\dev\projects\glint\electron\electron-builder.yml
  ⨯ Invalid configuration object. electron-builder 25.0.6 has been initialized using a configuration object that does not match the API schema.
 - configuration.win has an unknown property 'azureOptions'. These properties are valid:

@mmaietta
Copy link
Collaborator

Looks like yalc isn't copying over the newly updated scheme.json.
https://github.com/electron-userland/electron-builder/blob/52df0604c998f38324640a9d93f884824edd7691/packages/app-builder-lib/scheme.json

You can copy paste it manually in your node modules path or take the hard-copy approach (instead of using yalc) that I typically use instead using rsync. cp also would work, but I just prefer the logging/update-only/include args that rsync provides

rsync -upaRv --include='*.js' --include='*.d.ts' --include='*.nsi' --include='*.json' --include='*/' --include='*.py*' --include='*.tiff' --exclude='*'  ~/Development/electron-builder/packages/./* node_modules/

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 13, 2024

@MikeJerred I'm thinking of releasing the refactored signing code as part of 25.0.7 with logging that azure signing is in beta.
Previous signing configurations will still work, but logging has been added to note deprecated fields and where they've been moved to (probably within signtoolOptions)

Once released, I would like additional volunteers to test it though with DEBUG=electron-builder env var for console logs to make sure everything is kosher and can un-tag it as a beta feature. CC @OrganicChem @jmeinke @iliakolev 🙃

@Bartel-C8
Copy link

You could also possibly release it on the beta release channel, if the changes are too impactful?
But as only warnings will show, this shouldn't be a problem?

Anyway, also switching here to Azure Trusted Signing, as our previous certificate was expired.
As soon as everything is in place on Azure (verifying company etc) I also will test this out!

@mmaietta
Copy link
Collaborator

Excellent! Thank you

Previous logic is all in place for using signtool.exe, however, the new config has been moved to within a dedicated property signtoolOptions so a bit of refactoring also took place to keep the implementation clean (and avoid installing azure signing provider+modules on every signing request)

It'll be default released to next tag (as opposed to latest).

@mmaietta
Copy link
Collaborator

Alrighty. Beta signing implementation has been released in ^25.1, please give it a shot with DEBUG=electron-builder and report back!

I'm expecting bug reports, so I also request patience as I get this implementation fully functional. 🙃 Also, not sure if the cmd line debug logs will need any info redacted before posting them here since it's a verbatim log of the powershell Invoke-TrustedSigning command (double check any password/tokens provided aren't present)

From my local testing, I got this working up until the point it does Invoke-TrustedSigning as then the parallels VM prompts for Endpoint (since I didn't pass it in as an argument for test purposes), as I don't have an Azure account to test with. Requires NuGet package provider to be installed and TrustedSigning module, but both also required "-Scope", "CurrentUser" since the cmd prompt that is automatically executed within a parallels VM is not elevated to admin.

Logs below with DEBUG=electron-builder

  • signing         file=dist/win-unpacked/electron-quick-start-typescript.exe certificateFile=Foo Bar.pfx
  • signing with Azure Trusted Signing  path=/Users/dev/Development/electron-builder-test-2/dist/win-unpacked/electron-quick-start-typescript.exe
  • executing       file=prlctl args=list -i -s name
  • executing       file=prlctl args=exec {6db0fa46-4f04-432a-a546-f8584beac98f} --current-user powershell.exe -NoProfile -NonInteractive -Command Get-Command pwsh.exe
  • ensure that 'Share folders' is set to 'All Disks', see https://goo.gl/E6XphP
  • unable to find pwsh.exe, falling back to powershell.exe
  • executing       file=prlctl args=exec {6db0fa46-4f04-432a-a546-f8584beac98f} --current-user powershell.exe Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser
  • executed        file=prlctl stdout=
Name                           Version          Source           Summary                                               
----                           -------          ------           -------                                               
nuget                          2.8.5.208        https://onege... NuGet provider for the OneGet meta-package manager    
                      
  • executing       file=prlctl args=exec {6db0fa46-4f04-432a-a546-f8584beac98f} --current-user powershell.exe Install-Module -Name TrustedSigning -RequiredVersion 0.4.1 -Force -Repository PSGallery -Scope CurrentUser
  • executed        file=prlctl
  • executing       file=prlctl args=exec {6db0fa46-4f04-432a-a546-f8584beac98f} --current-user powershell.exe Invoke-TrustedSigning -Files /Users/dev/Development/electron-builder-test-2/dist/win-unpacked/electron-quick-start-typescript.exe

Implementation details: https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/windowsSignAzureManager.ts

Configuration details:

/**
* Options for usage of Azure Trusted Signing (beta)
*/
readonly azureSignOptions?: WindowsAzureSigningConfiguration | null

// https://learn.microsoft.com/en-us/azure/trusted-signing/how-to-signing-integrations
export interface WindowsAzureSigningConfiguration {
/**
* The Trusted Signing Account endpoint. The URI value must have a URI that aligns to the
* region your Trusted Signing Account and Certificate Profile you are specifying were created
* in during the setup of these resources.
*
* Translates to field: Endpoint
*
* Requires one of environment variable configurations for authenticating to Microsoft Entra ID per [Microsoft's documentation](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet#definition)
*/
readonly endpoint: string
/**
* The Certificate Profile name. Translates to field: CertificateProfileName
*/
readonly certificateProfileName: string
/**
* Allow other CLI parameters (verbatim case-sensitive) to `Invoke-TrustedSigning`
*/
[k: string]: string
}

@MikeJerred
Copy link
Contributor

MikeJerred commented Sep 14, 2024

I installed 25.1.0 but doing DEBUG=electron-builder npx electron-builder --dir gives an error:

Error: Cannot find module 'app-builder-lib/out/util/config/load'
Require stack:
- D:\dev\projects\glint\electron\node_modules\electron-builder\out\cli\cli.js
- D:\dev\projects\glint\electron\node_modules\electron-builder\cli.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1144:15)
    at Function.Module._load (node:internal/modules/cjs/loader:985:27)

The app-builder-lib in node_modules is v25.0.5, and it doesn't have a config folder under out/util

@mmaietta
Copy link
Collaborator

Thanks for checking.

Hmmm, it sounds like it desynced the release versioning during the CI/CD. It's been acting finicky lately. Can you try force installing app-builder-lib: 25.1.1 in your package.json?
For some reason a 25.1.0 version wasn't published, but a 25.1.1 was https://www.npmjs.com/package/app-builder-lib?activeTab=versions

I'll look into the dependency resolution issue

@MikeJerred
Copy link
Contributor

I added "app-builder-lib": "25.1.1" to my package.json but I get this error on npm install:

npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: [email protected]
npm WARN node_modules/dmg-builder
npm WARN   dmg-builder@"^25" from [email protected]        
npm WARN   node_modules/electron-builder
npm WARN     dev electron-builder@"^25.1.0" from the root project
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer dmg-builder@"^25.1.025.1.0" from [email protected]
npm WARN node_modules/app-builder-lib
npm WARN   dev app-builder-lib@"25.1.1" from the root project        
npm WARN   1 more (electron-builder)
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: peer dmg-builder@"^25.1.025.1.0" from [email protected]
npm WARN node_modules/app-builder-lib
npm WARN   dev app-builder-lib@"25.1.1" from the root project
npm WARN   1 more (electron-builder)
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer dmg-builder@"^25.1.025.1.0" from [email protected]
npm WARN node_modules/app-builder-lib
npm WARN   dev app-builder-lib@"25.1.1" from the root project
npm WARN   1 more (electron-builder)
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: electron-builder-squirrel-windows@undefined
npm WARN node_modules/electron-builder-squirrel-windows
npm WARN   peer electron-builder-squirrel-windows@"^25.1.025.1.0" from [email protected]
npm WARN   node_modules/app-builder-lib
npm WARN     dev app-builder-lib@"25.1.1" from the root project
npm WARN     1 more (electron-builder)
npm WARN
npm WARN Could not resolve dependency:
npm WARN peer electron-builder-squirrel-windows@"^25.1.025.1.0" from [email protected]
npm WARN node_modules/app-builder-lib
npm WARN   dev app-builder-lib@"25.1.1" from the root project
npm WARN   1 more (electron-builder)
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: electron-builder-squirrel-windows@undefined
npm WARN node_modules/electron-builder-squirrel-windows
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer electron-builder-squirrel-windows@"^25.1.025.1.0" from [email protected]
npm WARN node_modules/app-builder-lib
npm WARN   dev app-builder-lib@"25.1.1" from the root project
npm WARN   1 more (electron-builder)
npm ERR! code ETARGET
npm ERR! notarget No matching version found for dmg-builder@^25.1.025.1.0.
npm ERR! notarget In most cases you or one of your dependencies are requesting
npm ERR! notarget a package version that doesn't exist.

@mmaietta
Copy link
Collaborator

Kk. I've redeployed the monorepo to resync all the workspace versions. Please try 25.1.2

@MikeJerred
Copy link
Contributor

Kk. I've redeployed the monorepo to resync all the workspace versions. Please try 25.1.2

OK this version installs properly. It should also be noted that the docs are stating to use win.azureOptions but actually it should be win.azureSignOptions, regardless I am not seeing any error but the executable is not being signed.

This is my electron-builder.yml:

win:
  publisherName: Logic Over Snacks Ltd.
  azureSignOptions:
    endpoint: https://eus.codesigning.azure.net/
    certificateProfileName: ...
    codeSigningAccountName: ...

I also have set the 3 env vars AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET.
I run this command: DEBUG=electron-builder npx electron-builder --dir which completes without errors, but the executable created does not have a digital signature.

@mmaietta
Copy link
Collaborator

@MikeJerred Can you please upload/send the logs for the azure signing steps?
Should start after the line

  • signing with Azure Trusted Signing  path=/Users/dev/Development/electron-builder-test-2/dist/win-unpacked/electron-quick-start-typescript.exe

Please make sure to redact any sensitive info from the logs if present. Also am happy to discuss further via discord (@onegoldfish) to streamline debugging/implementing this feature.

@MikeJerred
Copy link
Contributor

This is the log, I redacted some long bits that I don't think are relevant:

$ DEBUG=electron-builder npx electron-builder --dir
  • electron-builder  version=25.1.2 os=10.0.19045
  • loaded configuration  file=D:\dev\projects\glint\electron\electron-builder.yml
  • effective config  config=directories
...
<contents of electron-builder.yml>
...
  • writing effective config  file=packaged\builder-effective-config.yaml
  • skipped dependencies rebuild  reason=npmRebuild is set to false
  • packaging       platform=win32 arch=x64 electron=29.1.4 appOutDir=packaged\win-unpacked
  • spawning        command=D:\dev\projects\glint\electron\node_modules\app-builder-bin\win\x64\app-builder.exe unpack-electron --configuration [{"platform":"win32","arch":"x64","version":"29.1.4"}] --output D:\dev\projects\glint\electron\packaged\win-unpacked --distMacOsAppName Electron.app
  • map async       taskCount=2
  • map async       taskCount=1
  • map async       taskCount=73
  • exited          command=app-builder.exe code=0 pid=31416
  • asar usage is disabled — this is strongly not recommended  solution=enable asar and use asarUnpack to unpack files that must be externally available
  • spawning        command=D:\dev\projects\glint\electron\node_modules\app-builder-bin\win\x64\app-builder.exe node-dep-tree --dir D:\dev\projects\glint\electron --flatten
  • unresolved deps  unresolved=lowercase-keys nodeModuleDir=D:\dev\projects\glint\node_modules round=0
  • unresolved deps  unresolved=lowercase-keysresponselike nodeModuleDir=D:\dev\projects\glint\node_modules round=0
...
<a lot of "unresolved deps">
...
  • exited          command=app-builder.exe code=0 pid=6080 out=[{...<lots of packages>...}]
  • asar usage is disabled — this is strongly not recommended  solution=enable asar and use asarUnpack to unpack files that must be externally available
  • spawning        command=D:\dev\projects\glint\electron\node_modules\app-builder-bin\win\x64\app-builder.exe icon --format ico --root D:\dev\projects\glint\electron\build --root D:\dev\projects\glint\electron --out D:\dev\projects\glint\electron\packaged\.icon-ico
  • path resolved   path=D:\dev\projects\glint\electron\build\icon.ico outputFormat=ico
  • exited          command=app-builder.exe code=0 pid=35164 out={"icons":[{"file":"D:\\dev\\projects\\glint\\electron\\build\\icon.ico","size":256}],"isFallback":false}
  • spawning        command=D:\dev\projects\glint\electron\node_modules\app-builder-bin\win\x64\app-builder.exe rcedit --args ["D:\\dev\\projects\\glint\\electron\\packaged\\win-unpacked\\Glint.exe","--set-version-string","FileDescription","An interface tool for git","--set-version-string","ProductName","Glint","--set-version-string","LegalCopyright","Copyright © 2024 Logic Over Snacks Ltd.","--set-file-version","1.8.9","--set-product-version","1.8.9.0","--set-version-string","InternalName","Glint","--set-version-string","OriginalFilename","","--set-version-string","CompanyName","Logic Over Snacks Ltd.","--set-icon","D:\\dev\\projects\\glint\\electron\\build\\icon.ico"]
  • found existing  path=C:\Users\mjerr\AppData\Local\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0
  • execute command  command='C:\Users\mjerr\AppData\Local\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0\rcedit-x64.exe' 'D:\dev\projects\glint\electron\packaged\win-unpacked\Glint.exe' --set-version-string FileDescription 'An interface tool for git' --set-version-string ProductName Glint --set-version-string LegalCopyright 'Copyright © 2024 Logic Over Snacks Ltd.' --set-file-version 1.8.9 --set-product-version 1.8.9.0 --set-version-string InternalName Glint --set-version-string OriginalFilename '' --set-version-string CompanyName 'Logic Over Snacks Ltd.' --set-icon 'D:\dev\projects\glint\electron\build\icon.ico'                     workingDirectory=
  • command executed  executable=C:\Users\mjerr\AppData\Local\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0\rcedit-x64.exe
  • exited          command=app-builder.exe code=0 pid=30740
wine&sign: 0s 390ms

@mmaietta
Copy link
Collaborator

Great investigate work! Thank you :)
Checking this asap. Will post results when I have more info

FWIW, I'm honestly shocked that this was not caught in the code signing unit tests. I'm also struggling to reproduce this locally in my test project with config

win: {
        target: [{
            target: 'zip',
            arch: 'x64'
        }],
        signtoolOptions: {
            certificateFile: 'Foo Bar.pfx',
            publisherName: "Foo Bar",
        },
        forceCodeSigning: true
},

Logs of successful build using signtool:

  • building        target=nsis file=dist/electron-quick-start-typescript Setup 1.0.4.exe archs=arm64 oneClick=true perMachine=true
  • signing         file=dist/win-arm64-unpacked/resources/elevate.exe certificateFile=CN=Foo Bar, O=Foo Bar.pfx
  • signing with signtool.exe  path=dist/win-arm64-unpacked/resources/elevate.exe
  • signing NSIS uninstaller  file=dist/__uninstaller-nsis-electron-quick-start-typescript.exe certificateFile=CN=Foo Bar, O=Foo Bar.pfx
  • signing with signtool.exe  path=dist/__uninstaller-nsis-electron-quick-start-typescript.exe
  • signing         file=dist/electron-quick-start-typescript Setup 1.0.4.exe certificateFile=CN=Foo Bar, O=Foo Bar.pfx
  • signing with signtool.exe  path=dist/electron-quick-start-typescript Setup 1.0.4.exe
  • building block map  blockMapFile=dist/electron-quick-start-typescript Setup 1.0.4.exe.blockmap

I'll see if I can mock a way to do the azure signing method without calling Invoke-TrustedSigning since I can't get an azure test account for free AFAIK, but at least it could test the initial logic in that electron-builder flow? I'll see what I can do

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 23, 2024

Okay, I did a bit more refactoring and moved some of the signtool logic that was still in winPackager into the signtool manager class (#8524)
If you're willing to test this @Bartel-C8 @MikeJerred, I have a patch-package file you can leverage on top of 25.1.4. Please let me know if it resolves the issue for you 🙂

patches/app-builder-lib+25.1.4.patch

[EDIT], patch didn't work, removing from comment to reduce verbosity of this thread/GH issue

@Bartel-C8
Copy link

Bartel-C8 commented Sep 23, 2024

Thanks for your changes @mmaietta , will try them out tonight.

FWIW, I'm honestly shocked that this was not caught in the code signing unit tests. I'm also struggling to reproduce this locally in my test project with config

But, there was probably no problem with normal signing.
I am only testing with a azure signing config entry...

The problem is that electron-builder expects a code-signing file, as in your example as well, certificateFile in the config.
For Azure signing there is no certificate file... So the signing code-flow also should happen when no certificate file is present in the config?

Anyway, I will test the patch provided.
But it would be best to create a unit-test only containing some (dummy) Azure config, and see if the code signing path is hit (probably with errors, as no valid credentials are given)?

@mmaietta
Copy link
Collaborator

Great callout. I've added a unit test in the PR that throws Invalid Configuration when none of the required env var combinations are detected. I made the check occur after installing the nuget package provider and trusted signing module so that it is also covered in the CI tests. Good thing too, as GH runners use pwsh.exe, which differs from my VM of powershell.exe. There's some -Command differences between the two usages that I'm trying to iron out
Currently stuck on this which seems unique to GH runners even when I try locally with pwsh.exe, which is proving to make my iterative debugging quite slow

"Exit code: 1. Command failed: pwsh.exe -NoProfile -NonInteractive -Command Get-PackageProvider | where name -eq 'nuget' | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser
Install-PackageProvider: No match was found for the specified search criteria for the provider 'NuGet'. The package provider requires 'PackageManagement' and 'Provider' tags. Please check if the specified package has the tags.

@MikeJerred
Copy link
Contributor

OK with that patch I get this:

$ npx electron-builder --dir
  • electron-builder  version=25.1.4 os=10.0.19045
  • loaded configuration  file=D:\dev\projects\glint\electron\electron-builder.yml
  • writing effective config  file=packaged\builder-effective-config.yaml
  • skipped dependencies rebuild  reason=npmRebuild is set to false
  • packaging       platform=win32 arch=x64 electron=29.1.4 appOutDir=packaged\win-unpacked
  • asar usage is disabled — this is strongly not recommended  solution=enable asar and use asarUnpack to unpack files that must be externally available
  • signing with Azure Trusted Signing (beta)  path=packaged\win-unpacked\resources\app\node_modules\@git-glint\nodegit\vendor\pageant_sha1.exe
  • signing with Azure Trusted Signing (beta)  path=packaged\win-unpacked\resources\app\node_modules\@git-glint\nodegit\vendor\pageant.exe
  • installing required package provider (NuGet) and module (TrustedSigning) with scope CurrentUser
  • Above command failed, retrying 3 more times
  • Above command failed, retrying 3 more times
  ⨯ Cannot cleanup: 

Error #1 --------------------------------------------------------------------------------
Error: Exit code: 64. Command failed: pwsh.exe -NoProfile -NonInteractive -Command Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser
The argument '-Command Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser' is not recognized as the name of a script file. Check the spelling of the name, or if a path was 
included, verify that the path is correct and try again.


Usage: pwsh[.exe] [-Login] [[-File] <filePath> [args]]
                  [-Command { - | <script-block> [-args <arg-array>]
                                | <string> [<CommandParameters>] } ]
                  [-CommandWithArgs <string> [<CommandParameters>]
                  [-ConfigurationName <string>] [-ConfigurationFile <filePath>]
                  [-CustomPipeName <string>] [-EncodedCommand <Base64EncodedCommand>]
                  [-ExecutionPolicy <ExecutionPolicy>] [-InputFormat {Text | XML}]
                  [-Interactive] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
                  [-NoProfileLoadTime] [-OutputFormat {Text | XML}] 
                  [-SettingsFile <filePath>] [-SSHServerMode] [-STA] 
                  [-Version] [-WindowStyle <style>] 
                  [-WorkingDirectory <directoryPath>]

       pwsh[.exe] -h | -Help | -? | /?

PowerShell Online Help https://aka.ms/powershell-docs

All parameters are case-insensitive.

@mmaietta
Copy link
Collaborator

Yep, I discovered that as well. Got it working out locally, but haven't posted a patch for the updated code yet. Thanks for checking folks!

@mmaietta
Copy link
Collaborator

Alright, got tests passing on GH runners covering invalid configuration + module installation in https://github.com/electron-userland/electron-builder/actions/runs/10998788786/job/30537499953?pr=8524
Patch file app-builder-lib+25.1.4.patch

diff --git a/node_modules/app-builder-lib/out/codeSign/windowsSignAzureManager.js b/node_modules/app-builder-lib/out/codeSign/windowsSignAzureManager.js
index 760e8ea..ee30ff2 100644
--- a/node_modules/app-builder-lib/out/codeSign/windowsSignAzureManager.js
+++ b/node_modules/app-builder-lib/out/codeSign/windowsSignAzureManager.js
@@ -10,9 +10,16 @@ class WindowsSignAzureManager {
     async initializeProviderModules() {
         const vm = await this.packager.vm.value;
         const ps = await (0, windowsCodeSign_1.getPSCmd)(vm);
-        builder_util_1.log.debug(null, "installing required package provider (NuGet) and module (TrustedSigning) with scope CurrentUser");
-        await vm.exec(ps, ["Install-PackageProvider", "-Name", "NuGet", "-MinimumVersion", "2.8.5.201", "-Force", "-Scope", "CurrentUser"]);
-        await vm.exec(ps, ["Install-Module", "-Name", "TrustedSigning", "-RequiredVersion", "0.4.1", "-Force", "-Repository", "PSGallery", "-Scope", "CurrentUser"]);
+        builder_util_1.log.info(null, "installing required module (TrustedSigning) with scope CurrentUser");
+        try {
+            await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", "Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser"]);
+        }
+        catch (error) {
+            // Might not be needed, seems GH runners already have NuGet set up.
+            // Logging to debug just in case users run into this. If NuGet isn't present, Install-Module -Name TrustedSigning will fail, so we'll get the logs at that point
+            builder_util_1.log.debug({ message: error.message || error.stack }, "unable to install PackageProvider Nuget. Might be a false alarm though as some systems already have it installed");
+        }
+        await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", "Install-Module -Name TrustedSigning -RequiredVersion 0.4.1 -Force -Repository PSGallery -Scope CurrentUser"]);
         // Preemptively check env vars once during initialization
         // Options: https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet#definition
         builder_util_1.log.info(null, "verifying env vars for authenticating to Microsoft Entra ID");
@@ -70,10 +77,12 @@ class WindowsSignAzureManager {
             CertificateProfileName: certificateProfileName,
             Files: options.path,
         };
-        const paramsString = Object.entries(params).reduce((res, [field, value]) => {
+        const paramsString = Object.entries(params)
+            .reduce((res, [field, value]) => {
             return [...res, `-${field}`, value];
-        }, []);
-        await vm.exec(ps, ["Invoke-TrustedSigning", ...paramsString]);
+        }, [])
+            .join(" ");
+        await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", `Invoke-TrustedSigning ${paramsString}`]);
         return true;
     }
 }
diff --git a/node_modules/app-builder-lib/out/codeSign/windowsSignToolManager.js b/node_modules/app-builder-lib/out/codeSign/windowsSignToolManager.js
index 54e5074..9773295 100644
--- a/node_modules/app-builder-lib/out/codeSign/windowsSignToolManager.js
+++ b/node_modules/app-builder-lib/out/codeSign/windowsSignToolManager.js
@@ -120,10 +120,35 @@ class WindowsSignToolManager {
         else {
             hashes = Array.isArray(hashes) ? hashes : [hashes];
         }
-        const cscInfo = await this.cscInfo.value;
         const name = this.packager.appInfo.productName;
         const site = await this.packager.appInfo.computePackageUrl();
         const customSign = await (0, resolve_1.resolveFunction)(this.packager.appInfo.type, (0, platformPackager_1.chooseNotNull)((_b = options.options.signtoolOptions) === null || _b === void 0 ? void 0 : _b.sign, options.options.sign), "sign");
+        const cscInfo = await this.cscInfo.value;
+        if (cscInfo) {
+            let logInfo = {
+                file: builder_util_1.log.filePath(options.path),
+            };
+            if ("file" in cscInfo) {
+                logInfo = {
+                    ...logInfo,
+                    certificateFile: cscInfo.file,
+                };
+            }
+            else {
+                logInfo = {
+                    ...logInfo,
+                    subject: cscInfo.subject,
+                    thumbprint: cscInfo.thumbprint,
+                    store: cscInfo.store,
+                    user: cscInfo.isLocalMachineStore ? "local machine" : "current user",
+                };
+            }
+            builder_util_1.log.info(logInfo, "signing");
+        }
+        else if (!customSign) {
+            builder_util_1.log.error({ signHook: customSign, cscInfo }, "no signing info identified, signing is skipped");
+            return false;
+        }
         const executor = customSign || ((config, packager) => this.doSign(config, packager));
         let isNest = false;
         for (const hash of hashes) {
diff --git a/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js b/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
index 5f59773..81368cc 100644
--- a/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
+++ b/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
@@ -347,7 +347,7 @@ class NsisTarget extends core_1.Target {
         else {
             await (0, wine_1.execWine)(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });
         }
-        await packager.sign(uninstallerPath, "signing NSIS uninstaller");
+        await packager.sign(uninstallerPath);
         delete defines.BUILD_UNINSTALLER;
         // platform-specific path, not wine
         defines.UNINSTALLER_OUT_FILE = uninstallerPath;
diff --git a/node_modules/app-builder-lib/out/winPackager.js b/node_modules/app-builder-lib/out/winPackager.js
index a7f21dc..8a0bc5c 100644
--- a/node_modules/app-builder-lib/out/winPackager.js
+++ b/node_modules/app-builder-lib/out/winPackager.js
@@ -96,47 +96,16 @@ class WinPackager extends platformPackager_1.PlatformPackager {
         var _a;
         return (0, platformPackager_1.chooseNotNull)((0, platformPackager_1.chooseNotNull)((0, platformPackager_1.chooseNotNull)((_a = this.platformSpecificBuildOptions.signtoolOptions) === null || _a === void 0 ? void 0 : _a.certificatePassword, this.platformSpecificBuildOptions.certificatePassword), process.env.WIN_CSC_KEY_PASSWORD), super.doGetCscPassword());
     }
-    async sign(file, logMessagePrefix) {
-        var _a;
+    async sign(file) {
         const signOptions = {
             path: file,
             options: this.platformSpecificBuildOptions,
         };
-        const cscInfo = await (await this.signtoolManager.value).cscInfo.value;
-        if (cscInfo == null) {
-            if ((0, platformPackager_1.chooseNotNull)((_a = this.platformSpecificBuildOptions.signtoolOptions) === null || _a === void 0 ? void 0 : _a.sign, this.platformSpecificBuildOptions.sign) != null) {
-                return (0, windowsCodeSign_1.signWindows)(signOptions, this);
-            }
-            else if (this.forceCodeSigning) {
-                throw new builder_util_1.InvalidConfigurationError(`App is not signed and "forceCodeSigning" is set to true, please ensure that code signing configuration is correct, please see https://electron.build/code-signing`);
-            }
-            return false;
+        const didSignSuccessfully = await this.doSign(signOptions);
+        if (!didSignSuccessfully && this.forceCodeSigning) {
+            throw new builder_util_1.InvalidConfigurationError(`App is not signed and "forceCodeSigning" is set to true, please ensure that code signing configuration is correct, please see https://electron.build/code-signing`);
         }
-        if (logMessagePrefix == null) {
-            logMessagePrefix = "signing";
-        }
-        if ("file" in cscInfo) {
-            builder_util_1.log.info({
-                file: builder_util_1.log.filePath(file),
-                certificateFile: cscInfo.file,
-            }, logMessagePrefix);
-        }
-        else {
-            const info = cscInfo;
-            builder_util_1.log.info({
-                file: builder_util_1.log.filePath(file),
-                subject: info.subject,
-                thumbprint: info.thumbprint,
-                store: info.store,
-                user: info.isLocalMachineStore ? "local machine" : "current user",
-            }, logMessagePrefix);
-        }
-        return this.doSign({
-            ...signOptions,
-            options: {
-                ...this.platformSpecificBuildOptions,
-            },
-        });
+        return didSignSuccessfully;
     }
     async doSign(options) {
         return (0, builder_util_1.retry)(() => (0, windowsCodeSign_1.signWindows)(options, this), 3, 500, 500, 0, (e) => {

@MikeJerred
Copy link
Contributor

OK amazing, that is working perfectly now!
It should be noted that I needed to include the "FileDigest" option, so maybe have that one as mandatory in the config or provide a default value (I used fileDigest: 'SHA256')

@mmaietta
Copy link
Collaborator

Thanks for testing @MikeJerred ! Re: fileDigest, is that an enum I can use in the schema (as opposed to generic string?) or should we just hardcode it as default but overridable in the code. i.e.

    const params = {
      FileDigest: "SHA256",
      ...extraSigningArgs, // allows overriding FileDigest if provided in config
      Endpoint: endpoint,
      CertificateProfileName: certificateProfileName,
      Files: options.path,
    }

@MikeJerred
Copy link
Contributor

Thanks for testing @MikeJerred ! Re: fileDigest, is that an enum I can use in the schema (as opposed to generic string?) or should we just hardcode it as default but overridable in the code. i.e.

    const params = {
      FileDigest: "SHA256",
      ...extraSigningArgs, // allows overriding FileDigest if provided in config
      Endpoint: endpoint,
      CertificateProfileName: certificateProfileName,
      Files: options.path,
    }

Not sure what options you can put, or why someone would want to change it, so I would say put it as defaulted to SHA256 allowing overrides - as in your comment there.

@mmaietta
Copy link
Collaborator

mmaietta commented Sep 23, 2024

Merged PR with fixes #8524

Deploying the release now. It'll be in 25.1.5

@mmaietta mmaietta pinned this issue Sep 23, 2024
@Bartel-C8
Copy link

Bartel-C8 commented Sep 24, 2024

Already a step further in my case, it starts attempting signing now. But still get an error.
Will also check/investigate why:

  • exited          command=app-builder.exe code=0 pid=10304
  • signing with Azure Trusted Signing (beta)  path=dist\win-unpacked\KLSTR.ctrl.exe
  • executing       file=powershell.exe args=-NoProfile -NonInteractive -Command Get-Command pwsh.exe
  • unable to find pwsh.exe, falling back to powershell.exe
  • installing required module (TrustedSigning) with scope CurrentUser
  • executing       file=powershell.exe args=-NoProfile -NonInteractive -Command Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser
  • executed        file=powershell.exe stdout=
Name                           Version          Source           Summary
----                           -------          ------           -------
nuget                          2.8.5.208        https://onege... NuGet provider for the OneGet meta-package manager

  • executing       file=powershell.exe args=-NoProfile -NonInteractive -Command Install-Module -Name TrustedSigning -RequiredVersion 0.4.1 -Force -Repository PSGallery -Scope CurrentUser
  • executed        file=powershell.exe
  • verifying env vars for authenticating to Microsoft Entra ID
  • executing       file=powershell.exe args=-NoProfile -NonInteractive -Command Get-Command pwsh.exe
  • unable to find pwsh.exe, falling back to powershell.exe
  • executing       file=powershell.exe args=-NoProfile -NonInteractive -Command Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesigning.azure.net/ -CertificateProfileName <profileName> -Files C:\Users\bartel\git\klstr-ctrl\dist\win-unpacked\KLSTR.ctrl.exe
  • Above command failed, retrying 3 more times
  ⨯ Exit code: 1. Command failed: powershell.exe -NoProfile -NonInteractive -Command Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesigning.azure.net/ -CertificateProfileName <profileName> -Files C:\Users\bartel\git\klstr-ctrl\dist\win-unpacked\KLSTR.ctrl.exe
Invoke-TrustedSigning : The 'Invoke-TrustedSigning' command was found in the module 'TrustedSigning', but the module could not be loaded. For more information, run 'Import-Module
TrustedSigning'.
At line:1 char:1
+ Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesi ...
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Invoke-TrustedSigning:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CouldNotAutoloadMatchingModule


Invoke-TrustedSigning : The 'Invoke-TrustedSigning' command was found in the module 'TrustedSigning', but the module could not be loaded. For more information, run 'Import-Module
TrustedSigning'.
At line:1 char:1
+ Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesi ...
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Invoke-TrustedSigning:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CouldNotAutoloadMatchingModule

  failedTask=build stackTrace=Error: Exit code: 1. Command failed: powershell.exe -NoProfile -NonInteractive -Command Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesigning.azure.net/ -CertificateProfileName <profileName> -Files C:\Users\bartel\git\klstr-ctrl\dist\win-unpacked\KLSTR.ctrl.exe
Invoke-TrustedSigning : The 'Invoke-TrustedSigning' command was found in the module 'TrustedSigning', but the module could not be loaded. For more information, run 'Import-Module
TrustedSigning'.
At line:1 char:1
+ Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesi ...
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Invoke-TrustedSigning:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CouldNotAutoloadMatchingModule


Invoke-TrustedSigning : The 'Invoke-TrustedSigning' command was found in the module 'TrustedSigning', but the module could not be loaded. For more information, run 'Import-Module
TrustedSigning'.
At line:1 char:1
+ Invoke-TrustedSigning -FileDigest SHA256 -Endpoint https://weu.codesi ...
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Invoke-TrustedSigning:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CouldNotAutoloadMatchingModule


    at C:\Users\bartel\git\klstr-ctrl\node_modules\builder-util\src\util.ts:138:18
    at ChildProcess.exithandler (node:child_process:429:5)
    at ChildProcess.emit (node:events:519:28)
    at maybeClose (node:internal/child_process:1104:16)
    at Process.ChildProcess._handle.onexit (node:internal/child_process:304:5)
From previous event:
    at processImmediate (node:internal/timers:491:21)
From previous event:
    at WinPackager.signApp (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\winPackager.ts:270:27)
    at WinPackager.doSignAfterPack (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\platformPackager.ts:346:32)
    at WinPackager.doPack (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\platformPackager.ts:331:7)
    at WinPackager.pack (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\platformPackager.ts:138:5)
    at Packager.doBuild (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\packager.ts:459:9)
    at executeFinally (C:\Users\bartel\git\klstr-ctrl\node_modules\builder-util\src\promise.ts:12:14)
    at Packager.build (C:\Users\bartel\git\klstr-ctrl\node_modules\app-builder-lib\src\packager.ts:393:31)
    at executeFinally (C:\Users\bartel\git\klstr-ctrl\node_modules\builder-util\src\promise.ts:12:14)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Must note I am trying this on a Windows on ARM (on a Mac with Parallels). So could be it influences some things...

Update:

C:\Users\bartel\git\klstr-ctrl>powershell.exe Import-Module TrustedSigning
Import-Module : File \\Mac\Home\Documents\WindowsPowerShell\Modules\TrustedSigning\0.4.1\FileFormat\FileFormat.psm1 cannot be loaded because
running scripts is disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ Import-Module TrustedSigning
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [Import-Module], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess,Microsoft.PowerShell.Commands.ImportModuleCommand

Powershell environment:

PS C:\Users\bartel\git\klstr-ctrl> powershell.exe Get-Host
Name             : ConsoleHost
Version          : 5.1.22621.2506

PS C:\Users\bartel\git\klstr-ctrl> powershell.exe Get-ExecutionPolicy
Restricted

Update2:
A step further with changing the execution policy, but is this the end-user's responsibility? If yes, I think it it's best to mention it in the docs:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

But then I get the message:
Invoke-TrustedSigning : Cannot process command because of one or more missing mandatory parameters: CodeSigningAccountName.
So I will set the configuration entry codeSigningAccountName as well, but if it's required, I think it's best to add it in the type interface as well?

Update3: After adding codeSigningAccountName (thus which is required), I get successfully to the signtool.exe execution.
But currently fails due to using Windows ARM... I get an exit code 3. Most likely because https://www.nuget.org/packages/Microsoft.Trusted.Signing.Client does not exist for ARM... Will retest on a non-ARM machine.

Also, might be very useful to add this resource to the documentation for Azure, as it is gold: https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/

@mmaietta
Copy link
Collaborator

@Bartel-C8 thank you for the deep debugging and report! Is this on local PC or a GH runner (or other platform)? I ask because my unit tests are not able to replicate the initialization setup error you're receiving on GH runner or on my local Parallels VM. (That being said, Parallels VM only needs to be set up once so there is the chance it already had the correct setup, but I don't recall ever setting ExecutionPolicy myself.)

Re: CodeSigningAccountName. Looks like it indeed is required per https://learn.microsoft.com/en-us/azure/trusted-signing/how-to-signing-integrations#create-a-json-file. I recall seeing elsewhere that it wasn't present in a sample config json, but I can't find the resource atm. I'll get a PR set up for CodeSigningAccountName shortly. I couldn't test the final Invoke-TrustedSigning step specifically due to no free azure accounts for open-source projects, so thank you again for reporting back with your findings.

@mmaietta
Copy link
Collaborator

Released v25.1.6 with CodeSigningAccountName property requirement.

I'm going to close this issue since Azure Trusted Signing is now supported in electron-builder per the OP request and this thread is super long.

If you encounter any issues from here, please open a new GH Issue and I'll be happy to take a look.

@mmaietta
Copy link
Collaborator

Note, additional fix needed to be added in v26.0.0-alpha.4 (or you can add the values as custom to your current config in 25.1.8)

Based off: https://github.com/Azure/trusted-signing-action?tab=readme-ov-file#timestamping-1

The files must be signed with timestamping enabled in order for the signatures to be valid for longer than 3 days. It is recommended to use the Trusted Signing timestamp server:

I missed this requirement during initial implementation. It wasn't ever specified in the Trusted Signing Docs, only as an optional param in the GH Action readme ¯_(ツ)_/¯. Sorry about that folks, just wanted to post a follow-up here to notify all to add these params to your config or update their electron-builder version (which just enforces via config that those are added)

TimestampRfc3161: 'http://timestamp.acs.microsoft.com'
TimestampDigest: 'SHA256'

@mmaietta
Copy link
Collaborator

Related note, has anyone tested out the auto-update flow?
(have to also ping here to reach the broader audience as opposed to creating a new issue)

The auto-update signature verification is based on the publisher name in the signing certificate, but since Azure Trusted Signing has no local certificate, the publisherName in the app-update.yml shouldn't be present (unless both a certificate is provided in electron-builder's config WITH an additional azure signing config. AFAICT, these shouldn't be used together, but need confirmation from anyone's update flow)

@mmaietta
Copy link
Collaborator

mmaietta commented Oct 31, 2024

Quick update on ☝️: I've added a required publisherName field to azureSignOptions in #8650 for signature verification to occur during auto-update.

Summary: Since azure trusted signing occurs after the app-update.yml is created during afterPack handler, there's no way to automatically extract the publisher name since there's no local certificate to work with. As such, if using azure signing, you need to provide your publisher name in the config in order for it to be properly provided into the app-update.yml file. If that entry in that file does not exist, then no signature verification occurs, so I've made it a required property.

I felt it was required because why wouldn't you want signing verification on auto-update if you're using the latest secure way to sign windows applications. ¯\(ツ)/¯ If it does not feel like it should be a required property, please comment here, open to the community's thoughts.


Ideally, there'd be an automatic way to fetch the Publisher Name via some Azure Trusted service/command before signing a file, but I haven't been able to identify a way. Suggestions are welcome!


Current workaround is to set publisherName in your build configuration and that way it'll be copied into the app-update.yml file. You can verify this by going into win-unpacked/resources/app-update.yml to double check though 🙂

@divdevhu
Copy link

divdevhu commented Nov 10, 2024

Hey folks! First of all, thank you for getting this feature out there, you are awesome!

I've been trying to get it working for hours now, no luck, so I'm all ears. The package I'm using is 25.1.8.

Here's a pastebin with the logs I'm getting.
I have also set the AZURE_TENANT, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_USERNAME and AZURE_PASSWORD env variables - hopefully with the correct values.

Maybe I'm plumbing things the wrong way (even in Azure, lol). I'd love to have a conversation about this issue with someone, if there's anybody willing to lend a hand to an indie dev. Thank you.

EDIT: eventually, I figured it out. I'll leave this here, maybe someone will need it in the future. I forgot to set up an IAM on the Azure side... Anyway this document is a good read that helped me solve the problem.

@divdevhu
Copy link

I'm not sure where should I put the publisherName property?
What I tried is this:

"win": { "icon": "build/multires.ico", "target": ["nsis"], "azureSignOptions": { "TimestampRfc3161": "http://timestamp.acs.microsoft.com", "TimestampDigest": "SHA256", "publisherName": "Árvíztűrő Tükörfúrógép", "endpoint": "https://neu.codesigning.azure.net/", "certificateProfileName": "szarnyseged", "codeSigningAccountName": "szarnyseged" } }

Well, my real name contains accented characters in the publisherName just like this example. Throws an error:

Exit code: 1. Command failed: pwsh.exe -NoProfile -NonInteractive -Command Invoke-TrustedSigning -FileDigest SHA256 -TimestampRfc3161 http://timestamp.acs.microsoft.com -TimestampDigest SHA256 -publisherName Árvíztűrő Tükörfúrógép -Endpoint https://neu.codesigning.azure.net/ -CertificateProfileName szarnyseged -CodeSigningAccountName szarnyseged -Files C:\Projects\application\release\win-unpacked\Szarnyseged.exe Invoke-TrustedSigning: A parameter cannot be found that matches parameter name 'publisherName'.

@Bartel-C8
Copy link

Bartel-C8 commented Nov 13, 2024

In my case the publisherName is a level higher:

    "win": {
      "signtoolOptions": {
        "publisherName": "<publisherHere>"
      },
      "azureSignOptions": {
        "endpoint": "<urlHere>",
        "certificateProfileName": "<profileNameHere>",
        "codeSigningAccountName": "<accountNameHere>"
      }

A good read for me was: https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/

But it could be that special characters are not handled that well?
Is it even supported as name in your Azure as validated organization name, under "Identity validation" ?

@divdevhu
Copy link

Yeah, the CN and O values are the same, with all that special character stuff. Maybe I just messed up my config JSON. Anyway, the last thing I want is to break my update mechanism. My app is out there already with paying customers. Anyway, having my installer signed is still better than having nothing. Validating sig on update would be a cherry on top but obviously I want to be 100% it won't break a thing.

@mmaietta
Copy link
Collaborator

Yeah this is definitely still a Beta feature since I'm unable to test the full flow myself locally w/o paying a subscription with MS Azure as it'd also run in our CI flows

@michalzaq12
Copy link

@divdevhu

I'm not sure where should I put the publisherName property? What I tried is this:

"win": { "icon": "build/multires.ico", "target": ["nsis"], "azureSignOptions": { "TimestampRfc3161": "http://timestamp.acs.microsoft.com", "TimestampDigest": "SHA256", "publisherName": "Árvíztűrő Tükörfúrógép", "endpoint": "https://neu.codesigning.azure.net/", "certificateProfileName": "szarnyseged", "codeSigningAccountName": "szarnyseged" } }

Well, my real name contains accented characters in the publisherName just like this example. Throws an error:

Exit code: 1. Command failed: pwsh.exe -NoProfile -NonInteractive -Command Invoke-TrustedSigning -FileDigest SHA256 -TimestampRfc3161 http://timestamp.acs.microsoft.com -TimestampDigest SHA256 -publisherName Árvíztűrő Tükörfúrógép -Endpoint https://neu.codesigning.azure.net/ -CertificateProfileName szarnyseged -CodeSigningAccountName szarnyseged -Files C:\Projects\application\release\win-unpacked\Szarnyseged.exe Invoke-TrustedSigning: A parameter cannot be found that matches parameter name 'publisherName'.

This bug is present in version 25.1.8, support for publisherName field (in azureSignOptions) was added in version 26.0.0-alpha.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants