-
Notifications
You must be signed in to change notification settings - Fork 5
feat(release): sign macOS/Windows release binaries #233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
oleg-flamingocx
merged 4 commits into
main
from
feature/sign-openframe-cli-for-macos-windows
Jul 17, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
68ac9a8
feat(release): sign macOS/Windows release binaries
oleg-flamingocx d1400bc
test(release): verify code signatures at unit and post-publish level
oleg-flamingocx e14aed8
Merge branch 'main' into feature/sign-openframe-cli-for-macos-windows
oleg-flamingocx 775625d
fix(release): pin signer identities and yank release on failed verifi…
oleg-flamingocx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| name: Setup macOS code signing | ||
| description: >- | ||
| Import the Apple Developer ID certificate into a throwaway keychain and | ||
| export KEYCHAIN_PATH / SIGNING_IDENTITY for later steps. Mirrors the setup | ||
| portion of .github/steps/sign-macos-package in openframe-oss-tenant; the | ||
| actual signing happens in scripts/sign-binary.sh, driven by GoReleaser | ||
| build hooks. Delete the keychain afterwards with an `if: always()` step. | ||
| inputs: | ||
| apple_certificate_p12: | ||
| description: 'Base64 encoded Apple Developer certificate (.p12)' | ||
| required: true | ||
| apple_certificate_password: | ||
| description: 'Password for the .p12 certificate' | ||
| required: true | ||
|
|
||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: Import certificate into throwaway keychain | ||
| shell: bash | ||
| env: | ||
| APPLE_CERTIFICATE_P12: ${{ inputs.apple_certificate_p12 }} | ||
| APPLE_CERTIFICATE_PASSWORD: ${{ inputs.apple_certificate_password }} | ||
| run: | | ||
| CERTIFICATE_PATH="$RUNNER_TEMP/certificate.p12" | ||
| KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" | ||
|
|
||
| # Decode certificate and create keychain | ||
| echo "$APPLE_CERTIFICATE_P12" | base64 --decode > "$CERTIFICATE_PATH" | ||
| security create-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" | ||
| security default-keychain -s "$KEYCHAIN_PATH" | ||
| security unlock-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" | ||
|
|
||
| # Import certificate and let codesign use the key non-interactively | ||
| security import "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | ||
| security set-key-partition-list -S apple-tool:,apple: -s -k "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" | ||
|
|
||
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | ||
| rm "$CERTIFICATE_PATH" | ||
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | ||
|
|
||
| SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}') | ||
| if [[ -z "$SIGNING_IDENTITY" ]]; then | ||
| echo "::error::No Developer ID Application identity found in keychain" | ||
| exit 1 | ||
| fi | ||
| echo "Found signing identity: $SIGNING_IDENTITY" | ||
| echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Release Signing | ||
|
|
||
| Release binaries are code-signed during `goreleaser release` (see | ||
| [release.yml](../../.github/workflows/release.yml)), **before archiving** — so | ||
| the published archives, `checksums.txt`, and the cosign bundle all cover the | ||
| signed binaries. | ||
|
|
||
| | Platform | Mechanism | | ||
| |----------|-----------| | ||
| | macOS | `codesign` (Developer ID Application, hardened runtime, timestamp) + `notarytool` notarization | | ||
| | Windows | Authenticode via Azure Trusted Signing (SHA-256, RFC3161 timestamp from `timestamp.acs.microsoft.com`) | | ||
| | Linux | Unsigned; integrity via `checksums.txt` + cosign bundle | | ||
|
|
||
| The flow mirrors the `sign-macos-package` / `sign-windows-package` composite | ||
| steps in [openframe-oss-tenant](https://github.com/flamingo-stack/openframe-oss-tenant) | ||
| and uses the same certificates and secrets. One deviation: the release job runs | ||
| on a single macOS runner (signing must happen before GoReleaser packs the | ||
| archives), so Windows signing uses [jsign](https://ebourg.github.io/jsign/) | ||
| instead of `azure/trusted-signing-action` (signtool is Windows-only) — same | ||
| Azure Trusted Signing account, endpoint and certificate profile. | ||
|
|
||
| ## How it's wired | ||
|
|
||
| - A GoReleaser build post-hook calls `scripts/sign-binary.sh <os> <arch> <path>` | ||
| for every built binary. `linux` is a pass-through. | ||
| - The script is a no-op unless `OPENFRAME_SIGN=1`, which only the release | ||
| workflow sets — local builds and CI compile checks never attempt to sign. | ||
| - [.github/steps/setup-macos-signing](../../.github/steps/setup-macos-signing/action.yml) | ||
| imports the Developer ID certificate into a throwaway keychain and exports | ||
| `KEYCHAIN_PATH` / `SIGNING_IDENTITY`; the keychain is deleted in an | ||
| `if: always()` cleanup step. | ||
| - The workflow downloads a version-pinned, checksum-verified jsign jar and | ||
| exports `JSIGN_JAR`. The script fetches a fresh AAD client-credentials token | ||
| per Windows binary (notarization waits can outlive a token fetched up-front). | ||
|
|
||
| ## Required secrets | ||
|
|
||
| Same names as `openframe-oss-tenant`, so org-level secrets cover both repos. | ||
|
|
||
| | Secret | Used for | | ||
| |--------|----------| | ||
| | `APPLE_CERTIFICATE_P12` | Base64-encoded Developer ID Application certificate (.p12) | | ||
| | `APPLE_CERTIFICATE_PASSWORD` | Password for the .p12 | | ||
| | `APPLE_ID_USERNAME` / `APPLE_ID_PASSWORD` | Notarization (app-specific password) | | ||
| | `APPLE_TEAM_ID` | Apple Developer Team ID | | ||
| | `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` | AAD token for Trusted Signing | | ||
| | `AZURE_SIGNING_ENDPOINT` | e.g. `https://eus.codesigning.azure.net` | | ||
| | `AZURE_CODE_SIGNING_ACCOUNT_NAME` | Trusted Signing account | | ||
| | `AZURE_CERTIFICATE_PROFILE_NAME` | Certificate profile | | ||
|
|
||
| ## Testing | ||
|
|
||
| Two layers, neither needing certificates locally: | ||
|
|
||
| - **Unit tests** — `tests/scripts/sign_binary_test.go` (part of | ||
| `make test-unit`) runs `scripts/sign-binary.sh` with PATH stubs for | ||
| `codesign`/`xcrun`/`java`/`curl`/`jq` that record their argv. They pin the | ||
| `OPENFRAME_SIGN` gate, the per-OS dispatch, fail-fast on missing env, and the | ||
| exact flags passed to codesign/notarytool/jsign (identity, hardened runtime, | ||
| endpoint scheme-stripping, alias, timestamp URL, call ordering). | ||
| - **Post-publish verification** — the `verify-windows-signature` / | ||
| `verify-macos-signature` jobs in the release workflow download the published | ||
| assets on real Windows/macOS runners and verify them against the OS trust | ||
| stores (`Get-AuthenticodeSignature` incl. timestamp; `codesign --verify` + | ||
| Developer ID authority check, best-effort `spctl` notarization assessment). | ||
| Both checks pin the signer identity — Authenticode subject | ||
| `Flamingo AI, Inc.`, Apple `TeamIdentifier=F7LDSU8JPJ` — so a binary signed | ||
| by *some* trusted-but-wrong publisher still fails. On verification failure | ||
| the `cleanup-on-failed-verification` job yanks the release and tag. | ||
|
|
||
| ## Verifying a released binary | ||
|
|
||
| macOS: | ||
|
|
||
| ```bash | ||
| codesign --verify --strict --verbose=2 openframe | ||
| spctl --assess --type open --context context:primary-signature -v openframe | ||
| ``` | ||
|
|
||
| Windows (PowerShell): | ||
|
|
||
| ```powershell | ||
| Get-AuthenticodeSignature .\openframe.exe | ||
| ``` | ||
|
|
||
| Any platform — release provenance (covers Linux too), see the `signs` block in | ||
| [.goreleaser.yml](../../.goreleaser.yml): | ||
|
|
||
| ```bash | ||
| cosign verify-blob --bundle checksums.txt.bundle \ | ||
| --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | ||
| --certificate-identity-regexp '^https://github.com/flamingo-stack/openframe-cli/\.github/workflows/release\.yml@.*$' \ | ||
| checksums.txt | ||
| ``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.