# Code signing

> Codesign tooling, framework and native binary copy steps, and signing constraints for macOS and related package pipelines.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `docs/src/app/packaging/signing/layout.tsx`
- `src/tooling/codesign.zig`
- `packages/native-sdk/scripts/copy-framework.js`
- `packages/native-sdk/scripts/copy-native.js`
- `src/tooling/assets.zig`

---

---
title: "Code signing"
description: "Codesign tooling, framework and native binary copy steps, and signing constraints for macOS and related package pipelines."
---

`native package --target macos` builds a `.app` tree, then optionally signs it through `src/tooling/codesign.zig` (`signAdHoc` / `signIdentity`). Signing is macOS-only in the package path. Default mode is `none`. Identity signing enables Hardened Runtime (`--options runtime`). Notarization is a separate host-tool step; the package command does not call `notarytool`.

## Signing modes

| Mode | Flag value | Behavior |
| --- | --- | --- |
| None | `none` (default) | Writes `signing-plan.txt` as unsigned; no `codesign` |
| Ad-hoc | `adhoc` (alias `ad-hoc`) | `codesign --sign - --force --deep` |
| Identity | `identity` | `codesign --sign <identity> --force --deep --options runtime` plus optional `--entitlements` |

```bash
native package --target macos --signing adhoc
native package --target macos --signing identity \
  --identity "Developer ID Application: Your Name" \
  --entitlements assets/native-sdk.entitlements
```

### Package flags

<ParamField body="--signing" type="none \| adhoc \| identity" default="none">
Signing mode. Invalid values fail CLI parse.
</ParamField>

<ParamField body="--identity" type="string">
Required for successful `identity` mode. Missing identity writes an unsigned plan and skips codesign.
</ParamField>

<ParamField body="--entitlements" type="path">
Passed only on identity sign. Example starter: `assets/native-sdk.entitlements`.
</ParamField>

<ParamField body="--team-id" type="string">
Accepted into `SigningConfig` and stored on the package options. Not consumed by `runSigning` or automatic notarization; pass the team id yourself to `notarytool`.
</ParamField>

## macOS package order (seal constraints)

`createMacosApp` finishes every sealed path **before** `runSigning`. Anything written into `Contents/` after a successful codesign breaks the resource seal (`codesign --verify --strict` reports “file added”; Gatekeeper can show “damaged”).

```text
createMacosApp
├── Contents/MacOS/<name>     binary (optional --binary)
├── Contents/Info.plist
├── Contents/Resources/       assets via assets.bundle + icons
├── package-manifest.zon      includes .signing = mode
├── Contents/Frameworks/      CEF only if web_engine=chromium
└── runSigning
    ├── write signing-plan.txt  (before codesign)
    └── codesign --deep …
```

| Stage | Path / owner | Must finish before codesign? |
| --- | --- | --- |
| App binary | `Contents/MacOS/` | Yes |
| Assets | `assets.zig` → `Contents/Resources/` | Yes |
| CEF framework | `copyMacosCefRuntime` → `Contents/Frameworks/Chromium Embedded Framework.framework` | Yes |
| Signing plan | `Contents/Resources/signing-plan.txt` | Written **before** success path; rewritten only on failure |
| Codesign | Host `/usr/bin/codesign` via shell | Last step |

<Warning>
Do not post-process the `.app` after signing (extra files under `Contents/`, re-copy CEF, edit Info.plist). Rebuild and re-sign instead.
</Warning>

### Command shapes (`codesign.zig`)

| API | Emitted command (simplified) |
| --- | --- |
| `signAdHoc` | `codesign --sign - --force --deep <app>` |
| `signIdentity` | `codesign --sign <id> --force --deep --options runtime [--entitlements <path>] <app>` |
| `buildZipCommand` | `ditto -c -k --keepParent <app> <app>.zip` |
| `buildNotarizeSubmitCommand` | `xcrun notarytool submit <zip> --team-id … [--apple-id …] [--password @keychain:…] --wait` |
| `buildStapleCommand` | `xcrun stapler staple <app>` |

`runShell` treats non-zero exits (including missing `codesign`, exit 127) as failure so callers do not claim a signature that does not exist. On package failure, the mode still soft-continues: the plan is rewritten to “unsigned” and packaging does not abort.

### Signing plan contents

| Mode / outcome | `Contents/Resources/signing-plan.txt` |
| --- | --- |
| `none` | `signing=none` / `unsigned local package` |
| `adhoc` success | `signing=adhoc` / `ad-hoc signed` |
| `adhoc` fail | `signing=adhoc` / `codesign --sign - failed; bundle is unsigned` |
| `identity` success | `signing=identity` / `signed with <name>` |
| `identity` missing id | `signing=identity` / `no identity provided; bundle is unsigned` |
| `identity` fail | `signing=identity` / `codesign failed; bundle is unsigned` |

`package-manifest.zon` also records `.signing = none|adhoc|identity` (requested mode, not post-sign verification).

## Ad-hoc vs identity vs Gatekeeper

| Signature | Typical use | Gatekeeper |
| --- | --- | --- |
| Ad-hoc (`-`) | Local / CI seal checks | Valid seal; quarantine downloads still get “cannot check for malicious software”. Local copies without quarantine often open with no dialog. |
| Developer ID + notarize | Distribution | No malware dialog when notarization staples successfully. |
| Broken seal after sign | Defect | “Damaged” / move to Trash — not normal ad-hoc behavior. |

Verification pin:

```bash
zig build test-package-signing
# packages with --signing adhoc, then:
# codesign --verify --strict --deep zig-out/package/native-sdk-signing-verify.app
# greps signing-plan for "ad-hoc signed"
# skips cleanly when codesign is absent (non-macOS hosts)
```

Doctor (`native doctor` on macOS) probes `/usr/bin/codesign`, `xcrun notarytool`, and `/usr/bin/hdiutil`.

## Notarization

Library helpers exist (`codesign.notarize`: zip with `ditto` → `notarytool submit --wait` → `stapler staple`). The `package` CLI does **not** call them.

`zig build notarize` runs host CLI `package` with `--signing identity` (and current web-engine/CEF flags). It does **not** invoke `notarytool` itself. After a signed package exists:

```bash
native package --target macos --signing identity \
  --identity "Developer ID Application: Your Name" \
  --entitlements assets/native-sdk.entitlements

# optional zip (or use ditto yourself)
ditto -c -k --keepParent zig-out/package/your-app.app zig-out/package/your-app.app.zip

xcrun notarytool submit zig-out/package/your-app.app.zip \
  --apple-id "you@example.com" \
  --team-id "TEAMID" \
  --password "@keychain:AC_PASSWORD" \
  --wait

xcrun stapler staple zig-out/package/your-app.app
```

DMG after a signed app:

```bash
zig build dmg   # framework repo helper; needs zig-out/package/… .app first
# or
hdiutil create -volname "Your App" -srcfolder zig-out/package/your-app.app \
  -ov -format UDZO zig-out/package/your-app.dmg
```

## Chromium / CEF packages

When `--web-engine chromium` (or equivalent), packaging copies `Chromium Embedded Framework.framework` into `Contents/Frameworks` **before** codesign. Deep sign is intended to cover nested Mach-O in that tree.

```bash
native cef install --version <pinned-version>
zig build
native package --target macos --signing identity \
  --identity "Developer ID Application: Your Name" \
  --web-engine chromium --cef-dir third_party/cef/macos
```

If Gatekeeper rejects a Chromium app: confirm the framework is under `Contents/Frameworks`, rebuild after any CEF version change, and re-sign the final tree (do not patch Frameworks after signing).

## Entitlements

Starter file `assets/native-sdk.entitlements`:

- `com.apple.security.cs.allow-unsigned-executable-memory` = true  
- `com.apple.security.network.client` = true  

Customize per app needs and pass `--entitlements` on identity packages. Separate tooling (for example `tools/guest-mac`) may ship its own entitlements and ad-hoc sign at build time for Virtualization; that path is independent of `native package`.

## Other platforms

| Target | Signing in Native tooling |
| --- | --- |
| macOS app | `none` / `adhoc` / `identity` via package |
| Windows / Linux desktop artifacts | No codesign step in `package.zig` |
| iOS package | Complete Xcode project; code signing manual (`DEVELOPMENT_TEAM`, `CODE_SIGN_IDENTITY`, or Xcode). `CODE_SIGNING_ALLOWED=NO` for unsigned verification archives. |
| iOS sim dev | Ad-hoc: `codesign --force --sign -` on the sim bundle |
| Android package | Debug APK via `apksigner` + debug keystore when SDK tools present; **store** keys remain manual |

## SDK npm copy pipeline (related packaging)

App codesign is separate from how `@native-sdk/cli` stages its own payload for publish. These scripts matter when you maintain release artifacts:

| Script | When | What |
| --- | --- | --- |
| `packages/native-sdk/scripts/copy-framework.js` | `npm prepack` | Mirrors repo `src/`, `build/`, `skills/`, `skill-data/`, plus `build.zig`, `build.zig.zon`, `app.zon`, `LICENSE` into the package so offline `native init` / `native dev` work |
| `packages/native-sdk/scripts/copy-native.js` | After `zig build` / `build:native` | Copies `zig-out/bin/native` → `packages/native-sdk/bin/native-sdk-<os>-<arch>[.exe]` (linux musl vs gnu detection) |
| `packages/native-sdk/scripts/build-binaries.sh` | Multi-target release | Cross-builds CLI to `packages/native-sdk/npm/<platform>/bin/` and `zig-out/release/` with `CHECKSUMS.txt` — **no codesign step** on published CLI binaries |

`check-framework-sync.js` fails the package scripts check if the mirrored tree drifts from the repo root.

## Failure modes and checks

| Symptom | Likely cause | Check |
| --- | --- | --- |
| Plan says unsigned after `adhoc`/`identity` | `codesign` missing, identity wrong, or non-zero exit | `native doctor`; run `codesign` manually; inspect stderr from package |
| Gatekeeper “damaged” | File written under `Contents/` after sign | `codesign --verify --strict --deep app.app`; rebuild package |
| Identity package unsigned | Omitted `--identity` | Plan text: `no identity provided` |
| Notarization not run by package | By design | Run `notarytool` / `stapler` yourself |
| Chromium app fails distribution checks | CEF missing or signed without framework | Confirm Frameworks layout; re-package then re-sign |

```bash
codesign --verify --strict --deep zig-out/package/your-app.app
codesign -dv --verbose=4 zig-out/package/your-app.app
cat zig-out/package/your-app.app/Contents/Resources/signing-plan.txt
```

## Related pages

<CardGroup cols={2}>
  <Card title="Packaging" href="/packaging">
    Bundle layout, assets, multi-target package outputs, and DMG expectations.
  </Card>
  <Card title="Updates and package distribution" href="/updates-distribution">
    npm multi-platform packages, version sync, and binary distribution for @native-sdk.
  </Card>
  <Card title="Web engines" href="/web-engines">
    CEF layout and when Chromium ships inside the signed .app.
  </Card>
  <Card title="Platform support and security" href="/platform-security">
    Host capability matrix and which platforms own signing.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `native package` flags including signing options.
  </Card>
  <Card title="Testing in CI" href="/testing-ci">
    Gate scripts and package signing verification steps.
  </Card>
</CardGroup>
