Skip to content

Commit a5e7d79

Browse files
committed
[ci] add Apple code signing and notarization to release pipeline
1 parent 384f712 commit a5e7d79

File tree

3 files changed

+217
-4
lines changed

3 files changed

+217
-4
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
Guide the user through setting up Apple code signing and notarization for the CLI release pipeline. Walk through each step interactively — do NOT dump all steps at once. After each step, wait for the user to confirm completion before moving to the next.
2+
3+
Use the AskUserQuestion tool with selectable options at decision points and when waiting for user input.
4+
5+
## Step 1: Prerequisites Check
6+
7+
Ask the user to confirm they have:
8+
- An Apple Developer Program **organization** account (not personal)
9+
- Access to https://developer.apple.com/account
10+
- Access to https://appstoreconnect.apple.com
11+
- `openssl` installed locally
12+
- `gh` CLI installed and authenticated to the `apoxy-dev/apoxy` repo
13+
- `rcodesign` installed (`cargo install apple-codesign`)
14+
15+
## Step 2: Generate Private Key
16+
17+
Run this for the user:
18+
```bash
19+
openssl genrsa -out developer_id.key 2048
20+
```
21+
22+
Confirm the file was created.
23+
24+
## Step 3: Generate CSR
25+
26+
Run this for the user:
27+
```bash
28+
openssl req -new -key developer_id.key \
29+
-out developer_id.csr \
30+
-subj "/emailAddress=support@apoxy.dev/CN=Apoxy, Inc/C=US"
31+
```
32+
33+
Confirm the file was created.
34+
35+
## Step 4: Upload CSR to Apple (Manual)
36+
37+
Tell the user:
38+
1. Go to https://developer.apple.com/account/resources/certificates/add
39+
2. Select **"Developer ID Application"** (NOT "Developer ID Installer")
40+
3. Upload the `developer_id.csr` file generated in the previous step
41+
4. Download the resulting `.cer` file
42+
43+
Ask the user to provide the path to the downloaded `.cer` file (usually `~/Downloads/developerID_application.cer`).
44+
45+
## Step 5: Convert to .p12
46+
47+
Generate a random password and convert the cert:
48+
```bash
49+
P12_PASSWORD=$(openssl rand -base64 24)
50+
echo "Generated P12 password: $P12_PASSWORD"
51+
echo "$P12_PASSWORD" > .p12-password.txt
52+
53+
# Convert Apple's DER cert to PEM
54+
openssl x509 -inform der -in <DOWNLOADED_CER_PATH> -out developer_id.pem
55+
56+
# Download Apple's intermediate cert
57+
curl -sO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
58+
openssl x509 -inform der -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem
59+
60+
# Bundle into .p12
61+
openssl pkcs12 -export \
62+
-out developer_id.p12 \
63+
-inkey developer_id.key \
64+
-in developer_id.pem \
65+
-certfile DeveloperIDG2CA.pem \
66+
-password pass:$P12_PASSWORD
67+
```
68+
69+
Replace `<DOWNLOADED_CER_PATH>` with the path the user provided. Confirm the `.p12` was created and is non-empty.
70+
71+
## Step 6: Create App Store Connect API Key (Manual)
72+
73+
Tell the user:
74+
1. Go to https://appstoreconnect.apple.com/access/integrations/api
75+
2. Click "+" to generate a new key
76+
3. Name it something like "CI Notarization"
77+
4. Select **"Developer"** role
78+
5. Download the `.p8` file (only downloadable once!)
79+
6. Note the **Key ID** and **Issuer ID** shown on the page
80+
81+
Ask the user for:
82+
- The **Issuer ID** (UUID format like `2bda9bb5-f36d-48f2-bd80-6493b0b6a051`)
83+
- The **Key ID** (alphanumeric like `WLA58M3W6H`)
84+
- The path to the downloaded `.p8` file (usually `~/Downloads/AuthKey_<KEY_ID>.p8`)
85+
86+
## Step 7: Generate Notary Key JSON
87+
88+
Run this for the user using the values they provided:
89+
```bash
90+
rcodesign encode-app-store-connect-api-key \
91+
<ISSUER_ID> \
92+
<KEY_ID> \
93+
<PATH_TO_P8>
94+
```
95+
96+
Note: these are **positional arguments**, not flags. Capture the JSON output to a file:
97+
```bash
98+
rcodesign encode-app-store-connect-api-key <ISSUER_ID> <KEY_ID> <PATH_TO_P8> > notary-key.json
99+
```
100+
101+
Verify the JSON has `issuer_id`, `key_id`, and `private_key` fields.
102+
103+
## Step 8: Upload GitHub Secrets
104+
105+
Run these for the user:
106+
```bash
107+
# Base64-encode the .p12
108+
P12_B64=$(base64 < developer_id.p12 | tr -d '\n')
109+
110+
gh secret set APPLE_P12_BASE64 --body "$P12_B64" --repo apoxy-dev/apoxy
111+
gh secret set APPLE_P12_PASSWORD --body "$(cat .p12-password.txt)" --repo apoxy-dev/apoxy
112+
gh secret set APPLE_NOTARY_KEY_JSON --body "$(cat notary-key.json)" --repo apoxy-dev/apoxy
113+
```
114+
115+
Confirm each secret was set successfully.
116+
117+
## Step 9: Cleanup Sensitive Files
118+
119+
Ask the user if they want to clean up the local sensitive files. If yes:
120+
```bash
121+
rm -f developer_id.key developer_id.csr developer_id.pem developer_id.p12 \
122+
DeveloperIDG2CA.cer DeveloperIDG2CA.pem .p12-password.txt notary-key.json
123+
```
124+
125+
Remind them to keep the `.p8` file and password backed up securely (e.g., in a password manager) in case they need to rotate secrets later.
126+
127+
## Step 10: Verify
128+
129+
Tell the user to trigger a release to test:
130+
```bash
131+
git tag vX.Y.Z && git push origin vX.Y.Z
132+
```
133+
134+
Then on a Mac after the release completes:
135+
```bash
136+
curl -sL https://github.com/apoxy-dev/apoxy/releases/download/vX.Y.Z/apoxy-darwin-arm64 -o apoxy
137+
chmod +x apoxy
138+
codesign -dv --verbose=4 ./apoxy
139+
spctl --assess --type execute ./apoxy
140+
```
141+
142+
Both commands should pass without errors.

.github/workflows/release.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,19 @@ jobs:
8181
uses: actions/setup-go@v5
8282
with:
8383
go-version: ">=1.24"
84+
- name: Decode Apple P12 certificate
85+
run: echo "${{ secrets.APPLE_P12_BASE64 }}" | base64 -d > /tmp/apple-developer-id.p12
8486
- name: Publish GitHub release and Helm chart
8587
env:
8688
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8789
APOXY_DOCKERHUB_PASSWORD: ${{ secrets.APOXY_DOCKERHUB_PASSWORD }}
8890
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
91+
APPLE_P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }}
92+
APPLE_NOTARY_KEY_JSON: ${{ secrets.APPLE_NOTARY_KEY_JSON }}
8993
SHUTUP: 1
90-
timeout-minutes: 15
94+
timeout-minutes: 30
9195
run: |
92-
dagger call -v publish-github-release --src=. --tag=$GITHUB_REF_NAME --github-token=env:GITHUB_TOKEN --sha=${GITHUB_SHA::7}
96+
dagger call -v publish-github-release --src=. --tag=$GITHUB_REF_NAME --github-token=env:GITHUB_TOKEN --sha=${GITHUB_SHA::7} --apple-p12=file:/tmp/apple-developer-id.p12 --apple-p12-password=env:APPLE_P12_PASSWORD --apple-notary-key=env:APPLE_NOTARY_KEY_JSON
9397
dagger call -v publish-helm-release --src=./deploy/helm --tag=$GITHUB_REF_NAME --registry-password=env:APOXY_DOCKERHUB_PASSWORD
9498
9599
publish-homebrew-formula:

ci/main.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
)
2929

3030
const ZigVersion = "0.14.1"
31+
const RcodesignVersion = "0.29.0"
3132

3233
type ApoxyCli struct{}
3334

@@ -292,12 +293,71 @@ Commits:
292293
return result, nil
293294
}
294295

296+
// SignDarwinBinary signs and notarizes a macOS binary using rcodesign.
297+
// This runs entirely on Linux using rcodesign (Rust reimplementation of Apple codesign).
298+
func (m *ApoxyCli) SignDarwinBinary(
299+
ctx context.Context,
300+
binary *dagger.File,
301+
appleP12 *dagger.Secret,
302+
appleP12Password *dagger.Secret,
303+
appleNotaryKey *dagger.Secret,
304+
) *dagger.File {
305+
rcodesignURL := fmt.Sprintf(
306+
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%%2F%s/apple-codesign-%s-x86_64-unknown-linux-musl.tar.gz",
307+
RcodesignVersion, RcodesignVersion,
308+
)
309+
310+
return dag.Container().
311+
From("ubuntu:22.04").
312+
WithEnvVariable("DEBIAN_FRONTEND", "noninteractive").
313+
WithExec([]string{"apt-get", "update"}).
314+
WithExec([]string{"apt-get", "install", "-y", "wget", "zip"}).
315+
// Install rcodesign.
316+
WithExec([]string{"sh", "-c", fmt.Sprintf(
317+
"wget -qO- '%s' | tar xzf - -C /usr/local/bin --strip-components=1 --wildcards '*/rcodesign'",
318+
rcodesignURL,
319+
)}).
320+
// Mount secrets.
321+
WithMountedSecret("/secrets/developer-id.p12", appleP12).
322+
WithMountedSecret("/secrets/p12-password", appleP12Password).
323+
WithMountedSecret("/secrets/notary-key.json", appleNotaryKey).
324+
// Copy unsigned binary.
325+
WithFile("/work/apoxy", binary).
326+
// Sign with hardened runtime.
327+
WithExec([]string{
328+
"rcodesign", "sign",
329+
"--p12-file", "/secrets/developer-id.p12",
330+
"--p12-password-file", "/secrets/p12-password",
331+
"--code-signature-flags", "runtime",
332+
"/work/apoxy",
333+
}).
334+
// Verify signature.
335+
WithExec([]string{"rcodesign", "verify", "/work/apoxy"}).
336+
// Notarize: wrap in ZIP (required by Apple), submit, and wait.
337+
WithExec([]string{"sh", "-c", "cd /work && zip apoxy.zip apoxy"}).
338+
WithExec([]string{
339+
"rcodesign", "notary-submit",
340+
"--api-key-path", "/secrets/notary-key.json",
341+
"--max-wait-seconds", "900",
342+
"--wait",
343+
"/work/apoxy.zip",
344+
}).
345+
// Return the signed binary (not the ZIP).
346+
File("/work/apoxy")
347+
}
348+
295349
// PublishGithubRelease publishes a CLI binary to GitHub releases.
296350
func (m *ApoxyCli) PublishGithubRelease(
297351
ctx context.Context,
298352
src *dagger.Directory,
299353
githubToken *dagger.Secret,
300354
tag, sha string,
355+
// +optional
356+
appleP12 *dagger.Secret,
357+
// +optional
358+
appleP12Password *dagger.Secret,
359+
// +optional
360+
appleNotaryKey *dagger.Secret,
301361
) *dagger.Container {
302362
cliCtrLinuxAmd64 := m.BuildCLI(ctx, src, "linux/amd64", tag, sha)
303363
cliCtrLinuxArm64 := m.BuildCLI(ctx, src, "linux/arm64", tag, sha)
@@ -329,6 +389,13 @@ func (m *ApoxyCli) PublishGithubRelease(
329389
}
330390
}
331391

392+
darwinAmd64Binary := cliCtrMacosAmd64.File("/apoxy")
393+
darwinArm64Binary := cliCtrMacosArm64.File("/apoxy")
394+
if appleP12 != nil && appleP12Password != nil && appleNotaryKey != nil {
395+
darwinAmd64Binary = m.SignDarwinBinary(ctx, darwinAmd64Binary, appleP12, appleP12Password, appleNotaryKey)
396+
darwinArm64Binary = m.SignDarwinBinary(ctx, darwinArm64Binary, appleP12, appleP12Password, appleNotaryKey)
397+
}
398+
332399
return dag.Container().
333400
From("ubuntu:22.04").
334401
WithEnvVariable("DEBIAN_FRONTEND", "noninteractive").
@@ -341,8 +408,8 @@ func (m *ApoxyCli) PublishGithubRelease(
341408
WithSecretVariable("GITHUB_TOKEN", githubToken).
342409
WithFile("/apoxy-linux-amd64", cliCtrLinuxAmd64.File("/apoxy")).
343410
WithFile("/apoxy-linux-arm64", cliCtrLinuxArm64.File("/apoxy")).
344-
WithFile("/apoxy-darwin-amd64", cliCtrMacosAmd64.File("/apoxy")).
345-
WithFile("/apoxy-darwin-arm64", cliCtrMacosArm64.File("/apoxy")).
411+
WithFile("/apoxy-darwin-amd64", darwinAmd64Binary).
412+
WithFile("/apoxy-darwin-arm64", darwinArm64Binary).
346413
// Create tarballs for each platform
347414
WithExec([]string{"sh", "-c", "cd /tmp && cp /apoxy-linux-amd64 apoxy && tar czf /apoxy_Linux_x86_64.tar.gz apoxy && rm apoxy"}).
348415
WithExec([]string{"sh", "-c", "cd /tmp && cp /apoxy-linux-arm64 apoxy && tar czf /apoxy_Linux_arm64.tar.gz apoxy && rm apoxy"}).

0 commit comments

Comments
 (0)