You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[ci] Sign and notarize macOS releases with rcodesign directly
Sign and notarize by calling rcodesign directly instead of the
indygreg/apple-code-sign-action:
- The sign-publish job already installs rcodesign and calls it for
encode-app-store-connect-api-key; the action only wrapped the sign and
notarize steps, so this consolidates on one way of invoking the tool.
- Drops a third-party action (personal namespace) that handled the PKCS#12
password and notarization API key. This repo SHA-pins third-party actions
from non-vendor namespaces; indygreg/ was the only non-vendor action on a
floating tag (GitHub actions/* and AWS aws-actions/* stay on version tags).
Harden the release job:
- Pin the rcodesign tarball to a repo-held SHA256 (APPLE_CODESIGN_SHA256 in
the workflow, per-arch digests in the builder-image script) rather than
trusting the release's own .sha256 sidecar, which a compromised release
could replace alongside the tarball.
- Restrict the push trigger to release tags (tags: ["v*"]) so a non-release
tag push cannot sign and publish artifacts, matching build-linux-packages.yml.
- Delete each signing secret immediately after its last use (P8 after encoding
the API key, P12 after Apple-signing, GPG key after zip-signing, API JSON
after notarization), so none remain on disk once the AWS credentials action
and upload-artifact run. An ERR trap in the credential-materialization step
removes the temp files if it fails before their paths are cleanup-tracked;
the always() Cleanup remains the backstop for later steps.
- Upload each zip's detached .sig to S3 before the zip itself, so a partial
upload cannot leave a consumable zip without its signature.
- Check out the requested tag on workflow_dispatch (ref:) and refuse to
build/sign a non-tag ref, mirroring build-linux-packages.yml.
Also:
- Note in the workflow env block that the packaging Taskfile greps
APPLE_CODESIGN_PKG_VERSION from this file.
- Comment why executable bits are restored after artifact download, and that
setup_gpg materializes the key at NFPM_SIGNING_KEY.
- macos-zip.md: reword "load-bearing"/"Tier 1/2/3" jargon, fix a 404 notary
link, note DEB now shares the GPG key, and rewrite the action-pinning
section to reflect the direct-rcodesign approach.
The two that run after credentials are materialized (`configure-aws-credentials`
233
+
and `upload-artifact`) run only after every signing secret has already been
234
+
deleted at its last point of use (see *Credential lifecycle*).
235
+
236
+
Supply-chain integrity of the tool itself is handled by pinning the
237
+
`rcodesign`release tarball's SHA256 in this repository, rather than trusting
238
+
the release's own `*.sha256` sidecar (a compromised release could replace the
239
+
tarball and the sidecar together). The CI workflow verifies against
240
+
`APPLE_CODESIGN_SHA256`; the local builder image passes the per-arch digest as
241
+
a Docker build arg (both live next to the pinned version and must be updated
242
+
together on a version bump).
221
243
222
244
### Apple-sign / GPG-sign / notarize order
223
245
224
246
The CI workflow performs the signing operations in this order, and the
225
-
order is load-bearing:
247
+
order matters:
226
248
227
-
1. **Apple-sign each Mach-O binary** (`rcodesign sign` via the action). The
228
-
signature is embedded inside the Mach-O via the `LC_CODE_SIGNATURE` load
229
-
command.
249
+
1. **Apple-sign each Mach-O binary** (`rcodesign sign`). The signature is
250
+
embedded inside the Mach-O via the `LC_CODE_SIGNATURE` load command.
230
251
2. **Zip each binary and detached-GPG-sign the resulting zip**
231
252
(`build-zip-pkg.sh`). The GPG signature is over the zip bytes, which
232
253
include the already-Apple-signed Mach-O.
@@ -261,20 +282,21 @@ This is a deliberate guard against a previously-latent failure mode:
261
282
The precondition replaces "publish unsigned then error" with "fail at the
262
283
import step with a clear message."
263
284
264
-
### Why `secrets.RPM_GPG_PRIVATE_KEY` is shared with RPM signing
285
+
### Why `secrets.RPM_GPG_PRIVATE_KEY` is shared across package formats
265
286
266
-
The macOS zip workflow reuses the same GPG private key as the RPM packaging
267
-
workflow rather than introducing a `MACOS_GPG_PRIVATE_KEY` secret.
287
+
The macOS zip workflow reuses the same GPG private key as the RPM and DEB
288
+
packaging workflows rather than introducing a `MACOS_GPG_PRIVATE_KEY` secret.
268
289
269
290
Rationale: the GPG key identifies *the project*, not the package format. End
270
291
users who want to verify "is this artifact really from ava-labs?" use the
271
-
same key whether they downloaded an RPM or a macOS zip (Linux tarballs are
272
-
currently unsigned; if they ever gain GPG signing, the same key would
273
-
naturally be reused). Maintaining two keys for the same provenance claim
274
-
would create rotation and trust-chain complexity without a corresponding
275
-
security benefit. The `RPM_GPG_PRIVATE_KEY` secret name predates macOS zip
276
-
signing; renaming it would require coordinated changes across multiple
277
-
workflows. The name is historical; the key is project-wide.
292
+
same key whether they downloaded an RPM, a DEB, or a macOS zip (plain Linux
293
+
tarballs are currently unsigned; if they ever gain GPG signing, the same key
294
+
would naturally be reused). Maintaining separate keys for the same provenance
295
+
claim would create rotation and trust-chain complexity without a
296
+
corresponding security benefit. The `RPM_GPG_PRIVATE_KEY` secret name
297
+
predates both DEB and macOS zip signing; renaming it would require
298
+
coordinated changes across multiple workflows. The name is historical; the
299
+
key is project-wide.
278
300
279
301
If the project ever needs per-format signing keys (for example, to support
280
302
different rotation cadences), the migration path is: add new format-specific
@@ -306,8 +328,8 @@ boundary use the sec-team naming convention.
306
328
307
329
### Local validator scope
308
330
309
-
The local validator is intentionally "Tier 1": it exercises the parts of the
310
-
pipeline that are reproducible offline. What it does and does not cover:
331
+
The local validator intentionally covers only the parts of the pipeline that
332
+
are reproducible offline. What it does and does not cover:
311
333
312
334
| Part | Local validator | Why |
313
335
| --- | --- | --- |
@@ -316,12 +338,12 @@ pipeline that are reproducible offline. What it does and does not cover:
316
338
| `rcodesign encode-app-store-connect-api-key` | yes, with `openssl`-generated ECDSA P8 | encode step is purely local, no network |
317
339
| `rcodesign notary-submit` (notarization) | no | hardcoded `https://appstoreconnect.apple.com/notary/v2/submissions` endpoint in `app-store-connect/src/notary_api.rs`; no `--dry-run`, no env-var endpoint override |
318
340
319
-
Reaching Tier 2 (running the indygreg action via `act`) or Tier 3 (mocking
320
-
the notary HTTPS endpoint) would require more infrastructure than the
321
-
current Tier-1 setup. Tier 1 covers ~80% of the value at ~5% of the
322
-
complexity: any divergence between local and CI for the signing/zip/GPG
323
-
path will surface; only Apple's notary submission itself is untested
324
-
locally.
341
+
The one part it cannot cover offline is Apple's notary submission itself
342
+
(`rcodesign notary-submit`), which has to reach Apple's servers. Mocking that
343
+
HTTPS endpoint would take more infrastructure than the rest of the validator
344
+
combined, for the single step a real release exercises anyway. Everything
345
+
else, any divergence between local and CI on the sign / zip / GPG path,
346
+
surfaces locally.
325
347
326
348
### Stub Mach-O binaries in the local validator
327
349
@@ -368,6 +390,12 @@ revisiting the design:
368
390
mounted `OUTPUT_DIR` or the published artifacts
369
391
- `Import GPG key`fails fast on an empty `RPM_GPG_PRIVATE_KEY` rather than
370
392
letting the no-op signing branch produce an unsigned release
393
+
- each signing secret is deleted right after its last consumer, so none
394
+
remain on disk once the AWS credentials action and `upload-artifact` run
395
+
- each zip's `.sig` is uploaded to S3 before the zip itself, so a partial
396
+
upload can never leave a consumable zip without its signature
397
+
- the `rcodesign` tarball is verified against a repo-pinned SHA256, not the
398
+
upstream `*.sha256` sidecar
371
399
- the macOS Mach-O binaries are built natively on `macos-14`; all other
372
400
operations run on `ubuntu-24.04`
373
401
@@ -376,7 +404,7 @@ revisiting the design:
376
404
The current design should be reconsidered if any of these change:
377
405
378
406
- rcodesign gains a `--dry-run` flag or env-var endpoint override for
379
-
notarization (would enable a Tier 3 local mock)
407
+
notarization (would let the local validator cover notary submission too)
380
408
- Apple migrates away from an rcodesign-compatible API (would force a fall-
381
409
back to Apple's `notarytool` or `codesign`, which only run on macOS)
382
410
- the security team rotates to a per-format GPG signing key (would require
@@ -404,7 +432,6 @@ The current design should be reconsidered if any of these change:
- [`rcodesign` documentation](https://gregoryszorc.com/docs/apple-codesign/stable/) - including [certificate management](https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_certificate_management.html) and [signing reference](https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign_signing.html)
410
-
- [Apple notary service overview](https://developer.apple.com/documentation/security/customizing_the_notarization_workflow)
437
+
- [Apple notary service overview](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
# The pinned digests above are only valid for one rcodesign version. Fail loudly
42
+
# if the requested version has drifted, rather than verifying against the wrong
43
+
# digest.
44
+
if [[ "${APPLE_CODESIGN_VERSION}"!="${PINNED_APPLE_CODESIGN_VERSION}" ]];then
45
+
echo"ERROR: no pinned rcodesign digest for version ${APPLE_CODESIGN_VERSION} (pinned: ${PINNED_APPLE_CODESIGN_VERSION}); update the digests in $(basename "$0")">&2
46
+
exit 1
47
+
fi
48
+
35
49
# ── Go checksum ──────────────────────────────────────────────────────
0 commit comments