Code Signing
tynd build signs Windows .exe / NSIS / MSI via signtool and macOS binaries / .app via codesign (+ optional notarization) for you when a bundle.sign block is present in tynd.config.ts.
Why sign? Unsigned binaries trigger SmartScreen warnings on Windows, are quarantined by Gatekeeper on macOS, and are flagged by Chrome/Edge on download. Signing + notarizing eliminates those prompts.
Built-in signing
Declare certs once in tynd.config.ts; every tynd build signs automatically.
export default {
runtime: "lite",
backend: "backend/main.ts",
frontendDir: "dist",
bundle: {
identifier: "com.example.myapp",
sign: {
windows: {
certificate: "./cert.pfx", // or "cert:subject=My Publisher"
password: "env:WIN_CERT_PASSWORD", // reads from env var
timestampUrl: "http://timestamp.digicert.com",
},
macos: {
identity: "Developer ID Application: Name (TEAMID)",
entitlements: "./entitlements.plist", // optional
notarize: {
appleId: "env:APPLE_ID",
password: "env:APPLE_APP_PASSWORD", // app-specific password
teamId: "env:APPLE_TEAM_ID",
},
},
},
},
} satisfies TyndConfig;env:NAMEresolves fromprocess.env.NAMEat build time and throws if missing — no secrets in source control.- The raw binary is signed first, then each installer artifact on top. Both layers need valid signatures for Gatekeeper / SmartScreen to trust the final download.
notarizeis opt-in. When present, the signed.appis zipped and submitted withxcrun notarytool submit --wait, then stapled so offline launches work.- On Windows,
signtool.exeis auto-discovered:SIGNTOOLenv var → Windows SDK → PATH.
Omit bundle.sign and builds stay unsigned — fine for dev and internal CI smoke tests.
Windows
Get a certificate
| Type | Cost/yr | SmartScreen |
|---|---|---|
| EV code-signing (hardware token) | ~$300-600 | removed immediately |
| Standard code-signing (OV) | ~$100-250 | removed after reputation builds (1-2 months) |
| Free for open source: SignPath.io (Foundation tier) | $0 | OV-level |
EV certs ship on a USB token (YubiKey or SafeNet); the key can’t be exported. Standard certs come as a .pfx.
Built-in signing
Point certificate at a .pfx file or a certificate store lookup:
sign: {
windows: {
certificate: "./cert.pfx", // file path
// or
certificate: "cert:subject=My Publisher Inc", // Windows cert store by subject
// or
certificate: "cert:sha1=AAAAAAAA...FFFFFFFF", // Windows cert store by thumbprint
password: "env:WIN_CERT_PASSWORD",
timestampUrl: "http://timestamp.digicert.com",
}
}Manual signtool (advanced)
signtool sign `
/fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
/f cert.pfx /p $env:CERT_PASSWORD `
release\my-app.exeAlways include /tr <timestamp> — without it, the signature expires when the cert does.
Verify:
signtool verify /pa /v release\my-app.exemacOS
Get a Developer ID
- Enroll in the Apple Developer Program ($99/yr).
- Create a Developer ID Application certificate in Xcode or the web console.
- For
.dmg/.pkginstallers, also create a Developer ID Installer certificate. - Export both as
.p12(password-protected) for CI.
Built-in signing
sign: {
macos: {
identity: "Developer ID Application: Your Name (TEAMID)",
entitlements: "./entitlements.plist", // optional
notarize: {
appleId: "env:APPLE_ID",
password: "env:APPLE_APP_PASSWORD",
teamId: "env:APPLE_TEAM_ID",
},
}
}Entitlements
WebKit needs JIT — minimal entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key> <true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/>
<key>com.apple.security.cs.disable-library-validation</key> <true/>
<key>com.apple.security.network.client</key> <true/>
</dict>
</plist>Manual codesign + notarytool
codesign --deep --force --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Your Name (TEAMID)" \
release/MyApp.app
codesign --force --sign "Developer ID Application: Your Name (TEAMID)" \
release/MyApp-1.0.0.dmg
xcrun notarytool submit release/MyApp-1.0.0.dmg \
--apple-id "you@example.com" --team-id "TEAMID" --password "app-specific-password" \
--wait
xcrun stapler staple release/MyApp-1.0.0.dmg
xcrun stapler staple release/MyApp.appGenerate an app-specific password at appleid.apple.com → Sign-in and security → App-specific passwords.
Verify
spctl --assess -vv release/MyApp.app
# Expected: "source=Notarized Developer ID"
codesign --verify --deep --strict --verbose=2 release/MyApp.appLinux
Linux has no centralized signature-verification model. Two practical paths:
Detached .sig (manual verification)
gpg --armor --detach-sign release/my-app-1.0.0.AppImage
gpg --armor --detach-sign release/my-app-1.0.0.deb
# Users verify with:
gpg --verify my-app-1.0.0.AppImage.asc my-app-1.0.0.AppImagePublish the public key alongside the downloads.
Signed .deb / .rpm repos
- Debian/APT —
dpkg-sig --sign builder file.deb+ sign theReleasefile withgpg --clearsign. Users drop the key in/etc/apt/trusted.gpg.d/. - RPM/dnf —
rpm --addsign file.rpm(needs%_gpg_namein~/.rpmmacros). Users import viarpm --import.
AppImage embedded signature
./appimagetool --sign --sign-key <KEYID> release/MyApp.AppImageVerification is then done by the AppImage runtime itself.
For most desktop apps shipping via GitHub Releases, detached .sig is enough — serious distros will package you themselves anyway.
CI — GitHub Actions example
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bunx tynd build --bundle
env:
# Windows
WIN_CERT_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }}
# macOS
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- uses: actions/upload-artifact@v4
with:
name: signed-${{ matrix.os }}
path: release/For macOS, import the cert into the runner’s keychain before the build step:
- name: Import macOS certs
if: runner.os == 'macOS'
env:
CERT_BASE64: ${{ secrets.MACOS_DEV_ID_P12 }}
CERT_PASSWORD: ${{ secrets.MACOS_DEV_ID_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "$CERT_BASE64" | base64 --decode > cert.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import cert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychainFor Windows, write the PFX from a base64-encoded secret before the build:
- name: Stage Windows cert
if: runner.os == 'Windows'
env:
CERT_BASE64: ${{ secrets.WINDOWS_PFX_BASE64 }}
run: |
[IO.File]::WriteAllBytes("cert.pfx", [Convert]::FromBase64String($env:CERT_BASE64))EV certs + CI
EV certs ship on hardware tokens and don’t work in most CI runners. Use a cloud-signing service instead — DigiCert KeyLocker, SSL.com eSigner, Azure Trusted Signing. They expose a signtool-compatible interface over an authenticated API.