signing.adoc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # Release signing
  2. OliveTin signs release binaries on two platforms:
  3. * **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional only when `MACOS_SIGN_P12` is unset).
  4. * **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (signed zip/MSI are uploaded after the release is published).
  5. ## macOS release signing
  6. Release builds can sign and notarize the `darwin` binaries using [quill](https://github.com/anchore/quill) via GoReleaser. This runs on the existing Linux CI runner; no macOS runner or Xcode is required.
  7. Signing is **optional** only when `MACOS_SIGN_P12` is unset — GoReleaser then skips macOS signing and publishes unsigned binaries. When `MACOS_SIGN_P12` is set, the companion macOS secrets below are required or the release fails.
  8. ### Prerequisites
  9. - An active [Apple Developer Program](https://developer.apple.com/programs/) membership.
  10. - A **Developer ID Application** certificate (not "Apple Development" or "Mac App Distribution").
  11. - An [App Store Connect API key](https://appstoreconnect.apple.com/access/integrations/api) with at least **Developer** access.
  12. ### One-time setup
  13. #### 1. Create the signing certificate (OpenSSL, no Mac/Xcode)
  14. Work in a private directory. Keep the private key offline and never commit it.
  15. ```sh
  16. mkdir -p ~/apple-signing && cd ~/apple-signing
  17. chmod 700 .
  18. openssl genrsa -out developer_id_app.key 2048
  19. openssl req -new -key developer_id_app.key -out developer_id_app.csr \
  20. -subj "/emailAddress=you@example.com/CN=Your Name/C=GB"
  21. ```
  22. 1. Open [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list).
  23. 2. Create a certificate of type **Developer ID Application**. Prefer **G2 Sub-CA** if the portal asks.
  24. 3. Upload `developer_id_app.csr` and download the resulting `.cer` (often named `developerID_application.cer`).
  25. Build a `.p12` that includes the **full** chain: leaf + Developer ID G2 intermediate + Apple Root CA.
  26. The Apple Root **must** be present. With only leaf + G2, quill embeds a designated requirement of the form `certificate root[field.1.2.840.113635.100.6.2.6]`. On macOS that resolves to Apple Root CA (which does not have that OID), so AMFI SIGKILLs the binary with `does not satisfy its designated Requirement` even though notarization still succeeds. A correct chain produces `certificate 1[...]` instead.
  27. ```sh
  28. curl -fsSLO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
  29. curl -fsSLO https://www.apple.com/appleca/AppleIncRootCertificate.cer
  30. openssl x509 -inform DER -in developerID_application.cer -out developerID_application.pem
  31. openssl x509 -inform DER -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem
  32. openssl x509 -inform DER -in AppleIncRootCertificate.cer -out AppleRootCA.pem
  33. # Chain file: intermediate then root (leaf is passed separately via -in).
  34. cat DeveloperIDG2CA.pem AppleRootCA.pem > chain.pem
  35. # Export password becomes MACOS_SIGN_PASSWORD.
  36. # On OpenSSL 3 (e.g. Fedora), -legacy improves compatibility with some tooling:
  37. openssl pkcs12 -export -legacy \
  38. -inkey developer_id_app.key \
  39. -in developerID_application.pem \
  40. -certfile chain.pem \
  41. -out Certificates.p12
  42. ```
  43. Confirm the `.p12` has three certificates before base64-encoding:
  44. ```sh
  45. openssl pkcs12 -in Certificates.p12 -nodes -passin pass:"$MACOS_SIGN_PASSWORD" 2>/dev/null \
  46. | grep -c "BEGIN CERTIFICATE"
  47. # expect: 3
  48. ```
  49. If you already have a Mac with the certificate in Keychain Access, you can export a `.p12` from there instead — include the full chain when exporting.
  50. #### 2. Create the notarization API key
  51. 1. Open [App Store Connect → Users and Access → Integrations → App Store Connect API](https://appstoreconnect.apple.com/access/integrations/api).
  52. 2. Create a key with **Developer** role (or Admin).
  53. 3. Download the `.p8` file once (it cannot be downloaded again). Note the **Key ID** shown in the portal and the **Issuer ID** at the top of the API keys page.
  54. #### 3. Base64-encode the key files
  55. Run on a machine that has the files (Linux or macOS):
  56. ```sh
  57. base64 -w0 < ./Certificates.p12 # MACOS_SIGN_P12
  58. base64 -w0 < ./AuthKey_XXXXXX.p8 # MACOS_NOTARY_KEY
  59. ```
  60. On macOS without GNU coreutils, use `base64 -i file | tr -d '\n'`.
  61. #### 4. Add GitHub repository secrets
  62. In **Settings → Secrets and variables → Actions**, create:
  63. | Secret | Value |
  64. |--------|-------|
  65. | `MACOS_SIGN_P12` | Base64 contents of the `.p12` file |
  66. | `MACOS_SIGN_PASSWORD` | Password used when exporting the `.p12` |
  67. | `MACOS_NOTARY_KEY` | Base64 contents of the `.p8` file |
  68. | `MACOS_NOTARY_KEY_ID` | Key ID from App Store Connect (e.g. `ABC123DEF4`) |
  69. | `MACOS_NOTARY_ISSUER_ID` | Issuer UUID from App Store Connect |
  70. All five must be present for signing to run. GoReleaser enables the step when `MACOS_SIGN_P12` is set; missing companion secrets will fail that release.
  71. ### Renewal
  72. | Item | Typical lifetime | What to do |
  73. |------|------------------|------------|
  74. | Developer ID Application certificate | ~5 years | Create a new certificate in the Apple portal, export a new **full-chain** `.p12` (leaf + G2 intermediate + Apple Root CA), update `MACOS_SIGN_P12` and `MACOS_SIGN_PASSWORD`. |
  75. | App Store Connect API key | Does not expire, but can be revoked | Create a new key if compromised or lost; update `MACOS_NOTARY_KEY`, `MACOS_NOTARY_KEY_ID`, and optionally `MACOS_NOTARY_ISSUER_ID`. |
  76. | Apple Developer Program | Annual subscription | Renew membership before it lapses; existing certificates stop working if the account is inactive. |
  77. After updating secrets, the next release on `main` (via semantic-release) will use the new credentials automatically.
  78. ### Verifying a signed release
  79. From any platform (no Mac required), check that quill did **not** emit the broken `certificate root[...]` designated requirement:
  80. ```sh
  81. go install github.com/anchore/quill/cmd/quill@latest
  82. quill describe OliveTin-darwin-arm64/OliveTin
  83. ```
  84. The requirements line must contain `certificate 1[field.1.2.840.113635.100.6.2.6]`. If it says `certificate root[field.1.2.840.113635.100.6.2.6]`, the `.p12` is missing Apple Root CA — rebuild it and update `MACOS_SIGN_P12`.
  85. On a Mac, also run:
  86. ```sh
  87. tar -xzf OliveTin-darwin-arm64.tar.gz
  88. codesign --verify --strict -vvvv OliveTin-darwin-arm64/OliveTin
  89. spctl -a -vv -t execute OliveTin-darwin-arm64/OliveTin
  90. ```
  91. `codesign` should report both `valid on disk` and `satisfies its Designated Requirement`. `spctl` should report `accepted` with `source=Notarized Developer ID`.
  92. ### Configuration reference
  93. - GoReleaser: `notarize.macos` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
  94. - CI secrets: link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`] (`release` step)
  95. - CI preflight (3-cert P12): link:https://github.com/OliveTin/OliveTin/blob/main/var/macos/verify-macos-sign-p12.sh[`var/macos/verify-macos-sign-p12.sh`]
  96. - CI post-sign check (designated requirement): link:https://github.com/OliveTin/OliveTin/blob/main/var/macos/verify-signed-darwin.sh[`var/macos/verify-signed-darwin.sh`]
  97. - link:https://goreleaser.com/customization/notarize/[GoReleaser notarization docs]
  98. ## Windows release signing (SignPath)
  99. Windows Authenticode signing uses [SignPath Foundation](https://signpath.org/) (free for qualifying open-source projects). It does **not** use GoReleaser Pro.
  100. GoReleaser publishes a GitHub release **without** Windows zip/MSI assets. A separate `sign-windows` job submits the unsigned Windows files (as workflow artifacts) to SignPath, then uploads the signed zip/MSI and updates `checksums.txt` on the already-published release.
  101. Signed artifacts:
  102. * `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
  103. * nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing)
  104. If SignPath secrets/vars are missing or signing fails, the release still publishes; Windows assets are simply missing until a successful `sign-windows` run. Install URLs for the Windows zip may 404 until signing finishes.
  105. ### Prerequisites
  106. - Approval for the [SignPath Foundation open-source program](https://signpath.io/product/open-source).
  107. - The SignPath GitHub App installed on the OliveTin organization/repository.
  108. - A SignPath project linked to this repository, with a release signing policy.
  109. ### One-time setup
  110. #### 1. Apply for SignPath Foundation
  111. 1. Open https://signpath.io/product/open-source and apply with the OliveTin GitHub repository URL.
  112. 2. After approval, create (or confirm) the organization and project in the SignPath portal.
  113. #### 2. Install the SignPath GitHub App
  114. 1. Install the SignPath GitHub App and grant access to the OliveTin repository.
  115. 2. Link the Trusted Build System **GitHub.com** to the SignPath project (required so SignPath can verify the workflow artifact origin).
  116. #### 3. Create artifact configurations
  117. In the SignPath project, create two artifact configurations with these slugs (must match CI). Paste the XML from the reference copies in this repo (SignPath does **not** load them automatically):
  118. * slug `windows-zip` ← link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`]
  119. * slug `windows-msi` ← link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`]
  120. Use **Custom** XML in the SignPath UI and paste the file contents. Do **not** use **Upload an artifact sample** on these `.xml` files — SignPath will treat them as XML documents to sign (`xml-file`), which is unavailable on the Foundation/Open Source plan. See link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/README.md[`signpath/README.md`].
  121. #### 4. Add GitHub secrets and variables
  122. In **Settings → Secrets and variables → Actions**:
  123. | Kind | Name | Value |
  124. |------|------|-------|
  125. | Secret | `SIGNPATH_API_TOKEN` | CI submitter API token from SignPath |
  126. | Variable | `SIGNPATH_ORGANIZATION_ID` | SignPath organization ID |
  127. | Variable | `SIGNPATH_PROJECT_SLUG` | SignPath project slug (e.g. `olivetin`) |
  128. | Variable | `SIGNPATH_SIGNING_POLICY_SLUG` | Signing policy slug (e.g. `release-signing`) |
  129. #### 5. Pipeline behaviour
  130. On a new semantic-release from `main`:
  131. 1. GoReleaser publishes container images and a GitHub release **without** Windows zip/MSI assets (those are built locally for SignPath only).
  132. 2. The build job uploads the unsigned Windows files as GitHub Actions artifacts.
  133. 3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to upload the signed files and refresh `checksums.txt`.
  134. All jobs in this chain use GitHub-hosted runners (required by SignPath for OSS projects).
  135. ### Verifying a signed release
  136. On Windows, download `OliveTin-windows-amd64.msi` or extract `OliveTin.exe` from the zip, then either:
  137. * Right-click → **Properties** → **Digital Signatures**, or
  138. * Run:
  139. ```bat
  140. signtool verify /pa OliveTin.exe
  141. signtool verify /pa OliveTin-windows-amd64.msi
  142. ```
  143. ### Configuration reference
  144. - Release publish (no Windows assets initially): `release.draft: false` and `release.ids` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
  145. - CI job: `sign-windows` in link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`]
  146. - Publish helper: link:https://github.com/OliveTin/OliveTin/blob/main/var/windows/signpath-publish-signed.sh[`var/windows/signpath-publish-signed.sh`]
  147. - SignPath artifact configs (reference only): link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`], link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`]
  148. - link:https://docs.signpath.io/trusted-build-systems/github[SignPath GitHub Actions docs]
  149. - link:https://docs.signpath.io/artifact-configuration/examples[SignPath artifact configuration examples]