electron-userland / electron-builder

A complete solution to package and build a ready for distribution Electron app with “auto update” support out of the box
https://www.electron.build
MIT License
13.61k stars 1.74k forks source link

Support for Azure Trusted Signing in Electron Builder #8276

Closed iliakolev closed 5 days ago

iliakolev commented 3 months ago

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/trusted-signing-action@v0.3.20 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 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

@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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 3 weeks ago

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 commented 3 weeks ago

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 commented 3 weeks ago

Okay, nvm, the patch is too large. My best recommendation is cloning electron-builder, pulling this PR https://github.com/electron-userland/electron-builder/pull/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 https://github.com/electron-userland/electron-builder/blob/0d24b78d43ce12f74f5bc073478688c7ad814034/packages/app-builder-lib/src/options/winOptions.ts#L178-L202

MikeJerred commented 2 weeks ago

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 commented 2 weeks ago

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 https://github.com/electron-userland/electron-builder/blob/c081df8e04494645028c4160bcc1376f029cbca5/packages/app-builder-lib/package.json#L76

MikeJerred commented 2 weeks ago

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

https://github.com/electron-userland/electron-builder/blob/c081df8e04494645028c4160bcc1376f029cbca5/packages/app-builder-lib/package.json#L76

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 commented 2 weeks ago

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 commented 2 weeks ago

@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 commented 2 weeks ago

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 commented 2 weeks ago

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 commented 2 weeks ago

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: https://github.com/electron-userland/electron-builder/blob/b3ce7f788cccf87ba841c9189a3b8758cd7c27c2/packages/app-builder-lib/src/options/winOptions.ts#L88-L91 https://github.com/electron-userland/electron-builder/blob/b3ce7f788cccf87ba841c9189a3b8758cd7c27c2/packages/app-builder-lib/src/options/winOptions.ts#L190-L210

MikeJerred commented 2 weeks ago

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 commented 2 weeks ago

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 commented 2 weeks ago

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: app-builder-lib@25.1.1
npm WARN Found: dmg-builder@25.0.5
npm WARN node_modules/dmg-builder
npm WARN   dmg-builder@"^25" from electron-builder@25.1.0        
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 app-builder-lib@25.1.1
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: app-builder-lib@25.1.1
npm WARN Found: peer dmg-builder@"^25.1.025.1.0" from app-builder-lib@25.1.1
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 app-builder-lib@25.1.1
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: app-builder-lib@25.1.1
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 app-builder-lib@25.1.1
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 app-builder-lib@25.1.1
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: app-builder-lib@25.1.1
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 app-builder-lib@25.1.1
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 commented 2 weeks ago

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

MikeJerred commented 2 weeks ago

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 commented 2 weeks ago

@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 commented 2 weeks ago

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 commented 2 weeks ago

Well that's super odd, it's hitting neither this line https://github.com/electron-userland/electron-builder/blob/5e21509a3f40d1a21f6f9ec9bf1d9d72c7149a21/packages/app-builder-lib/src/codeSign/windowsCodeSign.ts#L13 nor https://github.com/electron-userland/electron-builder/blob/5e21509a3f40d1a21f6f9ec9bf1d9d72c7149a21/packages/app-builder-lib/src/codeSign/windowsCodeSign.ts#L17

MikeJerred commented 1 week ago

The code in my node_modules has those lines, it looks like I have the correct packages installed as far as I can see. Am I setting the debug flag correctly to enable the feature?

mmaietta commented 1 week ago

Honestly, I'm confused because those are log.info commands, so it should be showing up without even having the DEBUG env var present. I tested the signtool implementation locally and it worked correctly too (in addition to the correct logging). Not sure why nothing is showing up for your logs, but it does have me worried that the signing refactor broke something for end-users.

Bartel-C8 commented 1 week ago

Also not working here. At least not signing.

I see that this line is not returning valid data: https://github.com/electron-userland/electron-builder/blob/d1cb6bdbf8111156bb16839f501bdd9e6d477338/packages/app-builder-lib/src/winPackager.ts#L131

As when I set forceCodeSigning to true , I hit the error message: 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

Effective config being print:

win:
  signtoolOptions:
    publisherName: "Manufacturer"
  azureSignOptions:
    endpoint: https://weu.codesigning.azure.net/
    certificateProfileName: "profilenamehere"
  forceCodeSigning: true

Also, is it normal that I had to had to run it in a elevated cmd prompt? I got this error message: errorOut=ERROR: Cannot create symbolic link : A required privilege is not held by the client.

Bartel-C8 commented 1 week ago

I don't know how this MemoryLazy thing works. But if I split it up, the signtoolManager resolves fine, but the cscInfo does not. But there is a value in "selected" which looks like to contain some sort of data?

checking value

Screenshot 2024-09-22 at 22 57 38

checking cscInfo

Screenshot 2024-09-22 at 22 58 00

Edit: Isn't is just because of no certificate file is being set? Which is not applicable in the azure case? So there should be a code path when no csc info is being set? (as now that part always returns?: https://github.com/electron-userland/electron-builder/blob/d1cb6bdbf8111156bb16839f501bdd9e6d477338/packages/app-builder-lib/src/winPackager.ts#L140 )

mmaietta commented 1 week ago

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 commented 1 week ago

Okay, I did a bit more refactoring and moved some of the signtool logic that was still in winPackager into the signtool manager class (https://github.com/electron-userland/electron-builder/pull/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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

Merged PR with fixes https://github.com/electron-userland/electron-builder/pull/8524

Deploying the release now. It'll be in 25.1.5

Bartel-C8 commented 1 week ago

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 commented 1 week ago

@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 commented 5 days ago

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.