Native macOS Authenticode signing for Windows artifacts — no Windows machine, no osslsigncode, no jsign, no OpenSSL/JVM. A fully managed .NET 10 engine.
Status: all backends shipped. Signs PE files (
.exe/.dll/.sys, incl. managed assemblies), PowerShell scripts (.ps1), and MSI installers (.msi), using a local PFX certificate, a PKCS#11 token / HSM, or Azure Trusted Signing (a.k.a. Azure Artifact Signing) — for the token and cloud paths the key never leaves the device/cloud — with optional RFC3161 timestamping, and verifies signatures (integrity + chain report). The Azure path is proven offline by a contract test (delegated path == the signtool-proven in-proc path) and has been signed + verified end-to-end against a live Trusted Signing account; re-verify it anytime withscripts/verify-azure.sh(uses youraz login, no stored credentials).
Grab the latest signed + notarized DMG for your Mac from the Releases page — MacSign-<ver>-osx-arm64.dmg for Apple Silicon, -osx-x64.dmg for Intel — open it, and drag MacSign to Applications. It's Developer ID–signed and Apple-notarized, so it opens with no Gatekeeper warning. (Requires macOS 11 Big Sur or later; the app is self-contained, so no .NET runtime is needed to run it.) After that MacSign keeps itself current: it checks for new releases on launch (toggle in Preferences) and via Help → "Check for Updates…", and installs them in one click. Or build from source below.
The cross-platform tools for signing Windows binaries from a Mac are fiddly CLIs. MacSign reimplements Authenticode natively in .NET so signing is a single dependency-clean, notarizable app — and so the format logic is unit-testable and fully under our control.
| Project | What |
|---|---|
src/MacSign.Signing |
The engine. No third-party deps; one Microsoft platform package (System.Security.Cryptography.Pkcs) for the CMS APIs. |
src/MacSign.Signing.Pkcs11 |
Optional PKCS#11/HSM backend, quarantined so Pkcs11Interop stays out of the core. Loaded only by consumers that sign with a token. |
src/MacSign.Signing.Azure |
Optional Azure Trusted Signing backend, quarantined so Azure.Identity stays out of the core. A delegating RSA POSTs each digest to the cloud sign endpoint. |
src/MacSign.Signing.Msi |
Optional MSI backend, quarantined so the OpenMcdf (CFBF) dependency stays out of the core. |
src/MacSign.Cli |
A thin console harness (macsign) — scriptable signing/verifying. |
src/MacSign.App |
The native macOS GUI (.NET 10 + Avalonia) — consumes the engine in-process. Sign / Verify / Sign (Mac) / Profiles / Activity / Preferences, light + dark. |
src/MacSign.Fixture |
A trivial class library whose compiled DLL is the unsigned PE the tests/CI sign. |
tests/MacSign.Signing.Tests |
xUnit: PE digest, CMS framing, sign→verify round-trip, secret hygiene. |
tests/MacSign.App.Tests |
xUnit for the macOS signing (codesign/notarytool) wrapper: exact argv per option, identity allow-listing, .dmg-direct notarize, process injection/cancellation. |
Requires the .NET 10 SDK.
dotnet build -c Release
dotnet test# Make a throwaway self-signed code-signing cert (test/dev only):
PFX_PW=secret dotnet run --project src/MacSign.Cli -- \
gen-test-cert --pfx test.pfx --cer test.cer --password-env PFX_PW
# Sign a PE in place (optionally RFC3161-timestamped; --timestamp-url accepts a
# comma-separated list of TSAs tried in order, so one outage won't fail the sign):
PFX_PW=secret dotnet run --project src/MacSign.Cli -- \
sign --pfx test.pfx --password-env PFX_PW --description "My App" \
--timestamp-url http://timestamp.digicert.com some.dll
# Sign with a PKCS#11 token / HSM instead (key never leaves the device):
PIN=1234 dotnet run --project src/MacSign.Cli -- \
sign --pkcs11-module /path/to/pkcs11.so --password-env PIN some.dll
# Sign with Azure Trusted Signing (key never leaves Azure). With no token flag the
# token is acquired via Azure.Identity (az login, env service principal, or managed
# identity); or pass one explicitly with --trusted-signing-token[-env]:
dotnet run --project src/MacSign.Cli -- \
sign --trusted-signing-endpoint eus.codesigning.azure.net \
--trusted-signing-account my-account \
--trusted-signing-profile my-profile some.dll
# Verify a signature (reports signer, timestamp, integrity, and chain trust):
dotnet run --project src/MacSign.Cli -- verify some.dll
# Remove an existing signature, in place (PE / PowerShell / MSI):
dotnet run --project src/MacSign.Cli -- remove some.dllverify reports signature integrity (file unmodified + signer signature valid) separately from chain trust — on macOS the Microsoft roots aren't in the system store, so chain trust usually can't be established, but integrity can be asserted authoritatively. It lists every signer on a co-signed binary and flags a nested signature, and only surfaces an RFC3161 timestamp it has cryptographically validated (a forged or grafted token is not shown as the signing time).
A native macOS GUI (src/MacSign.App, .NET 10 + Avalonia) consumes the same engine in-process — no shelling out. Six screens: Sign (drag-drop files + a credential/options inspector, ⌘S to sign), Verify (a Windows artifact's integrity vs. chain-trust report, or a Mac .app/.dmg's codesign signature — signer, Team ID, Hardened Runtime, notarization — and Remove signature for a signed Authenticode file, with a two-step confirm), Sign (Mac) (sign, notarize & staple a .app bundle or .dmg with your Developer ID), Profiles (reusable presets — no secrets stored), Activity (run history), and Preferences (⌘, — theme override, signing defaults, data housekeeping, and an Updates section: "Check for updates automatically" toggle + "Check Now"). The sidebar groups these by domain (Windows · macOS · Library · App), and a native menu bar (File · Edit · View · Window · Help, plus an About box) rounds out the shell. Full light + dark, following the macOS appearance. MacSign checks for a newer release on launch — throttled to once per day, on by default — and Help → "Check for Updates…" triggers it on demand; when an update is found you can download, verify, and install in one click: the notarized, Developer ID–signed app inside the downloaded DMG (Team ID Q6LRJQSA42) is the trust anchor, so no separate appcast or signing key is needed. It degrades gracefully if the install directory isn't writable or signature verification fails.
![]() Verify — integrity vs. chain trust, every signer, a validated timestamp |
![]() Sign (Mac) — sign · notarize · staple a .app/.dmg |
![]() Preferences — theme, signing defaults, data housekeeping |
Sign (Mac) is the inverse of the engine's day job: rather than signing Windows artifacts, it signs your Mac apps. It's a thin, injection-safe wrapper over Apple's own
codesign/notarytool/stapler(not a reimplementation) — choose a.appor.dmg, pick a Developer ID identity, and watch sign → verify → notarize → staple stream in a live log. You can also create the keychain notary profile in-app — Notarize → Keychain profile → Set up… runsnotarytool store-credentialsfrom an App Store Connect API key, so you never need Terminal for it (API-key only; the key stays in its.p8). Before submitting to the notary it runs a pre-flight (mounting a.dmgto inspect its contents) and stops — with a "Notarize anyway" override — if anything inside isn't signed/hardened, so you don't burn a multi-minute round-trip on a doomed upload.If the pre-flight finds unsigned
.appbundles inside your.dmg, a "Sign contents & continue" button appears alongside "Notarize anyway". Clicking it signs the problematic apps inside the image — with Hardened Runtime and deep signing, using the Developer ID identity already chosen, and applying your Entitlements.plistif you've set one (recommended for JIT-enabled apps such as .NET apps that needallow-jit) — re-seals the DMG in place, and then proceeds automatically to sign, notarize, and staple. Only the apps that failed the pre-flight checks are re-signed; already-valid signatures are left untouched.
dotnet run --project src/MacSign.App # run from source
# Build a signed + notarized DMG (Developer ID + a notarytool keychain profile):
SIGN_IDENTITY="Developer ID Application: NAME (TEAMID)" \
NOTARY_PROFILE=your-notary-profile ./build-macos.sh
# (omit the env vars for an unsigned local build)Releases are tag-driven: push a v* tag and CI builds, signs, notarizes, and publishes the arm64 + x64 DMGs to a GitHub Release (.github/workflows/release.yml). Setup + required secrets: docs/RELEASE-SIGNING.md.
Prefer supplying the password (or PIN, or Azure token) via an environment variable — --password-env / --trusted-signing-token-env. The plaintext --password / --trusted-signing-token flags still work, but MacSign warns about them: an argv secret lands in your shell history and the process list (ps). Secrets are never persisted, logged, or placed on a child-process command line.
The authoritative check is Windows signtool, run in CI (.github/workflows/ci.yml): the macOS job signs a fixture PE and uploads it; a windows-latest job runs signtool verify /pa against it. That gate proves the cross-platform claim and must stay green.
Locally on macOS you can sanity-check with osslsigncode (chain trust will fail for a self-signed cert — that's expected; supply the cert as a trusted root to get a full pass):
osslsigncode verify some.dll # parses + checks digest/signature
openssl x509 -inform DER -in test.cer -out test.pem
osslsigncode verify -CAfile test.pem -ignore-timestamp some.dll # full passIf MacSign saves you a Windows VM, you can support development here: https://www.buymeacoffee.com/thefinder808
Licensed under the Apache-2.0 License. Release notes live in the CHANGELOG. Contributions are welcome — see CONTRIBUTING.md and the Code of Conduct. To report a security issue, follow the security policy.



