Seed Project
Overview
Code signing identifies the publisher and allows the operating system to verify the integrity of signed files. Configure signing before distributing your customized platform. On macOS, also configure notarization for distribution outside the Mac App Store.
The settings for code signing are located under the "codeSign" property of the "win" top-level key for Windows and the "mac" top-level key for macOS in the iocd.cli.config.json seed project configuration file.
Windows
The io.Connect CLI uses signtool.exe (part of the Windows SDK) to sign binaries. It must be available on your system PATH before running a build with code signing enabled.
Ensure that the CI workflow also makes signtool.exe available on PATH.
The "type" property of the "codeSign" object under the "win" top-level key accepts the following values:
| Value | Description |
|---|---|
"custom" |
Use a custom signing script. |
"signtool" |
Use a certificate and a sign tool to sign. The certificate can be retrieved from a PFX certificate file or from the Windows Certificate Store. |
"off" |
Disable code signing. This is the default. |
The generated GitHub Actions workflow doesn't use config/iocd.cli.config.json for Windows signing. It selects the signing method from its windows_sign input via the WIN_CODE_SIGN_TYPE environment variable, and supplies the certificate via WIN_PFX_PATH and WIN_PFX_PASS, or via WIN_CERT_SHA1. Settings in the configuration file take precedence over these variables.
PFX Certificate File
A PFX file contains the certificate and private key used by the signing tool. Supply its password via an environment variable.
To configure signing via a PFX certificate file, provide the path to the file and the PFX password:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"pfxPath": "path/to/certificate.pfx",
"pfxPassword": "${WIN_PFX_PASS}"
}
}
}Certificate from the Windows Certificate Store
Use a certificate available in the Windows Certificate Store. Services such as DigiCert KeyLocker synchronize the certificate into the store while keeping the private key in a hardware security module.
To configure signing via the Windows Certificate Store, provide the certificate SHA-1. Configure exactly one of "pfxPath" or "certificateSha1":
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"certificateSha1": "${WIN_CERT_SHA1}"
}
}
}⚠️ Note that if using DigiCert KeyLocker, you must run
smctl windows certsyncbefore building to sync certificates to the Windows Certificate Store.
Custom Script
For advanced signing scenarios, you can provide a custom signing script:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "custom",
"customCodeSignScriptPath": "path/to/custom-sign.js"
}
}
}Your custom script should export a function that receives the binary path and config:
// custom-sign.js
module.exports = async function(binaryPath, config) {
// Your custom signing logic here.
console.log(`Signing ${binaryPath}`);
// Use any signing tool or API you need.
};Excluding Folders from Signing
Available since @interopio/iocd-cli 0.0.74
You can exclude specific folders from code signing on Windows by using the "excludeFolders" property of the "codeSign" object. This is useful when certain folders contain third-party binaries that are already signed or don't need to be signed with your certificate.
The "excludeFolders" property accepts an array of glob patterns. Paths are matched relative to the io.Connect Desktop component directory using forward slashes. By default, this list is empty. The CLI still skips already signed binaries and selected runtime files, including Microsoft runtime DLLs.
Supported glob syntax:
| Pattern | Description |
|---|---|
** |
Matches any number of path segments (including zero). |
* |
Matches anything within a single path segment. |
? |
Matches a single character. |
! (prefix) |
Negates a pattern (re-includes previously excluded paths). |
Patterns are evaluated in order and the last matching pattern wins (similar to .gitignore).
Example configuration that excludes all binaries under /components and /assets/gw-legacy, but re-includes /components/my-addon:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"pfxPath": "path/to/certificate.pfx",
"pfxPassword": "${WIN_PFX_PASS}",
"excludeFolders": [
"components/**",
"assets/gw-legacy/**",
"!components/my-addon/**"
]
}
}
}Signing Squirrel-Generated Stub Executables
The CLI signs eligible platform binaries before packaging. On Windows, it also signs the final setup executable after Forge creates it. Squirrel generates additional executables inside the installer, including Update.exe and the app launcher. Configure the maker to sign these embedded executables during package creation.
To sign these Squirrel-generated stubs, add a windowsSign object to the @electron-forge/maker-squirrel configuration in config/forge.config.js:
// In `config/forge.config.js`.
const path = require("path");
module.exports = {
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
// ... other Squirrel config options,
windowsSign: {
certificateFile: path.resolve(__dirname, "certs/certificate.pfx"),
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
timestampServer: "http://timestamp.digicert.com",
signToolPath: "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.26100.0\\x64\\signtool.exe",
},
},
},
],
};The windowsSign object has the following properties:
| Property | Type | Description |
|---|---|---|
certificateFile |
string |
Path to the PFX certificate file used for signing the Squirrel-generated stubs. |
certificatePassword |
string |
Password for the PFX certificate. Use an environment variable to avoid hardcoding secrets. |
signToolPath |
string |
Explicit path to signtool.exe. Required only if signtool.exe isn't on your system PATH. |
signWithParams |
string |
Additional arguments passed to signtool.exe (e.g., /sha1 <thumbprint> for selecting a certificate from the Windows Certificate Store). |
timestampServer |
string |
URL of the timestamp server to use for timestamping the signature. |
To sign the stubs with a certificate from the Windows Certificate Store, such as the certificate used by the generated GitHub Actions workflow, pass its SHA-1 thumbprint via signWithParams instead of specifying a certificate file and password:
// In `config/forge.config.js`.
module.exports = {
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
// ... other Squirrel config options,
windowsSign: {
signWithParams: `/sha1 ${process.env.WIN_CERT_SHA1}`,
timestampServer: "http://timestamp.digicert.com"
},
},
},
],
};⚠️ Note that the
signToolPathmay vary depending on the Windows SDK version installed on your system. Verify the path before configuring it.
⚠️ Note that you must never hardcode the
certificatePasswordvalue directly in your configuration file. Always use an environment variable (e.g.,process.env.WINDOWS_CERTIFICATE_PASSWORD) and store the actual password in your CI/CD secrets or a secure credential store.
⚠️ Note that without this configuration, the
Update.exeand<app-name>.exestubs inside the installer will be unsigned. This may trigger Windows SmartScreen warnings or antivirus flags for end users.
macOS
Configure a signing identity or certificate for the app bundle, then configure notarization for the resulting distribution.
The "type" property of the "codeSign" object under the "mac" top-level key accepts the following values:
| Value | Description |
|---|---|
"certificate" |
Use a .p12 certificate file. |
"custom" |
Use a custom signing script. |
"keychain" |
Use a certificate from macOS Keychain. This is the default. |
"off" |
Disable code signing. |
Keychain
This method uses a certificate from macOS Keychain. This is the mode to use if you want to use an Apple Developer ID certificate. You must download the certificate from the Apple Developer Portal, import it into your Keychain, and it will become available for code signing tools.
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "keychain",
"identity": "Developer ID Application: Your Company (TEAM_ID)"
}
}
}You can use the following properties of the "codeSign" object to specify your Developer ID Application identity and Keychain to use:
| Property | Type | Description |
|---|---|---|
"identity" |
string |
Developer ID Application identity or SHA-1 hash (optional, auto-selected if not specified). |
"keychain" |
string |
Keychain name or path (optional, uses the default Keychain if not specified). |
Certificate
This method uses a .p12 certificate file that's imported into a temporary keychain during the build. The temporary keychain is removed after the build.
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "certificate",
"certificatePath": "./certs/mac-cert.p12",
"certificatePassword": "${MAC_CERT_PASSWORD}"
}
}
}Custom Script
For advanced signing scenarios, you can provide a custom signing script:
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "custom",
"customCodeSignScriptPath": "path/to/custom-sign.js"
}
}
}Your custom script should export a function that receives the app bundle path and config:
// custom-sign.js
module.exports = async function(appBundlePath, config) {
// Implement signing with your selected tool or service.
console.log(`Signing ${appBundlePath}`);
};Signing Entitlements
Available since @interopio/iocd-cli 2.0.0
For production builds using mac.codeSign.type: "keychain" or "certificate", the CLI signs the app and its nested code with the bundled default entitlements.
The bundled default entitlement keys and values are:
| Key | Default Value |
|---|---|
com.apple.security.cs.allow-jit |
true |
com.apple.security.cs.allow-unsigned-executable-memory |
true |
com.apple.security.cs.allow-dyld-environment-variables |
true |
com.apple.security.cs.debugger |
true |
com.apple.security.cs.disable-library-validation |
true |
com.apple.security.inherit |
true |
com.apple.security.files.user-selected.read-write |
true |
com.apple.security.files.downloads.read-write |
true |
com.apple.security.scripting-targets |
Dictionary described after the table. |
com.apple.security.device.camera |
true |
com.apple.security.device.microphone |
true |
com.apple.security.device.audio-input |
true |
com.apple.security.automation.apple-events |
true |
The default com.apple.security.scripting-targets dictionary contains these string values:
{
"com.apple.systemevents": "com.apple.systemevents",
"com.apple.finder": "com.apple.finder"
}Use mac.codeSign.entitlements in config/iocd.cli.config.json to add, update, or remove default entitlements:
{
"mac": {
"codeSign": {
"type": "keychain",
"entitlements": {
"set": {
"com.apple.security.device.audio-input": true,
"com.apple.security.device.camera": false,
"com.apple.security.scripting-targets": {
"com.example.target": ["group.example.actions"]
}
},
"remove": ["com.apple.security.cs.debugger"]
}
}
}
}The scripting target in this example is illustrative. Use the bundle identifier and scripting access groups of the target app.
The "entitlements" object has the following properties:
| Property | Type | Description |
|---|---|---|
"remove" |
string[] |
Removes top-level entitlement keys. Removing an absent key has no effect. |
"set" |
object |
Adds missing keys and replaces existing values. Values may be booleans, strings, safe integers, arrays, or nested dictionaries containing the same types. |
Keys are literal entitlement names, including any dots. Setting a dictionary or array replaces the whole value. It doesn't merge nested keys or append array items. Setting a key to false preserves it as a plist Boolean with that value, whereas removing it omits the key entirely. A key can't appear in both set and remove.
Values retain their plist types: objects become dictionaries, arrays remain arrays, and numbers become integers. null, fractional numbers, unsafe integers, non-finite numbers, invalid XML characters, and reserved object property names are rejected before signing. Although general plist files support real numbers, macOS signing entitlements don't. Dates and binary data aren't supported by this JSON overlay.
Each build starts from the defaults bundled with the CLI and applies your overlay. Unmentioned defaults, including new defaults introduced by CLI upgrades, are preserved. The CLI generates a temporary plist for signing and removes it after signing completes or fails. You don't need to edit files under node_modules or place an entitlements file in /modifications.
This overlay applies to the two built-in production signing methods. It doesn't change Info.plist, ad-hoc signing during npm run dev, or disabled signing. With "type": "custom", your signing function receives the overlay as config.entitlements and is responsible for applying it.
The audio-input entitlement doesn't replace the microphone usage description in Info.plist or the microphone permission granted by the user on macOS. Rebuild, sign, and notarize your distribution after changing entitlements, then test recording in that built app.
Notarization
Notarization submits the signed app to Apple for checking. Configure it for distribution outside the Mac App Store so that Gatekeeper can verify the notarization status.
The notarization settings are located under the "notarization" property of the "mac" top-level key:
// In `config/iocd.cli.config.json`.
{
"mac": {
"notarization": {
"type": "notarytool",
"appleId": "${MAC_NOTARIZATION_APPLE_ID}",
"appleIdPassword": "${MAC_NOTARIZATION_APPLE_ID_PASSWORD}",
"appleTeamId": "${MAC_NOTARIZATION_APPLE_TEAM_ID}"
}
}
}Use an app-specific password for the Apple ID, and supply it via an environment variable. You can generate an app-specific password in your Apple account settings.
⚠️ Note that
"notarytool"is the default notarization type, and notarization runs even when code signing is disabled. A macOS build without notarization credentials fails. Set"type"to"off"to skip notarization (e.g., for development builds).
The generated GitHub Actions workflow supplies the Apple credentials to the macOS build step as APPLE_ID, APPLE_PASSWORD, and APPLE_TEAM_ID. Reference these variables in the configuration file (e.g., "appleId": "${APPLE_ID}"), or rename them in the workflow to match the variables in the example above.
Custom Notarization Script
For advanced notarization scenarios, you can provide a custom notarization script:
{
"mac": {
"notarization": {
"type": "custom",
"customNotarizationScriptPath": "path/to/custom-notarize.js"
}
}
}Your custom script should export a function that receives the app bundle path and config:
// custom-notarize.js
module.exports = async function(appBundlePath, config) {
// Your custom notarization logic here.
console.log(`Notarizing ${appBundlePath}`);
// Use any notarization tool or API you need.
};