# Apple Code Signing & Notarization

Current status and reference for macOS app signing, notarization, and distribution.

## Signing Identity

| Field            | Value                                                                 |
| ---------------- | --------------------------------------------------------------------- |
| Certificate      | Developer ID Application: Shawn Sloan (25VDK2A4QR)                    |
| SHA-1            | `00D4364338DEADDD2DF37331A79127A00E35ADA7`                            |
| Team ID          | `25VDK2A4QR`                                                          |
| Bundle ID        | `bot.molt.mac`                                                        |
| Keychain Profile | `sophiaclaw-notary` (stored via `xcrun notarytool store-credentials`) |
| Apple ID         | _(see 1Password or internal credentials vault)_                       |

## Sparkle Auto-Update Signing

| Field       | Value                                                                      |
| ----------- | -------------------------------------------------------------------------- |
| Algorithm   | ed25519 (Sparkle 2.x)                                                      |
| Public Key  | `ranDTIAwEoHQ5da0R5loYMoyV1ic1WPKwmSMM+2bi+Q=`                             |
| Private Key | Stored in macOS keychain (generated via `generate_keys`)                   |
| Appcast URL | `https://raw.githubusercontent.com/sophiaclaw/sophiaclaw/main/appcast.xml` |

The Sparkle key pair was rotated on 2026-03-12. The previous public key (`AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=`) is no longer valid. This is safe since there are no external users on the old key.

## Current Status

**As of v0.1.1 (2026-03-12):**

- Signing: Working. App is signed with Developer ID Application certificate and hardened runtime (universal: arm64 + x86_64).
- Notarization: Submitted, pending Apple processing. Submission: `7bc63bb8-eadd-4739-98e0-c1fdfe76801e`.
- Stapling: Blocked on notarization completing. Run `xcrun stapler staple dist/SOPHIAClaw.app` once accepted.
- Gatekeeper: Developer machine works without stapling. Other machines see "cannot verify" until notarization ticket is stapled.
- Sparkle: appcast.xml updated with v0.1.1 entry, signed with new ed25519 key.
- GitHub Release: https://github.com/sophiaclaw/sophiaclaw/releases/tag/v0.1.1

### Previous releases

**v0.1.0 (2026-03-12):**

- Notarization submission: `34ae68d1-44d2-48cd-9629-669c2b91eaa4` (status: In Progress / delayed).
- Used old Sparkle public key and legacy bundle ID `ai.sophiaclaw.mac`.

## Entitlements

The app uses a minimal entitlements file at `apps/macos/SOPHIAClaw.entitlements`:

```xml
com.apple.security.device.audio-input          <!-- Microphone for voice wake -->
com.apple.security.device.camera               <!-- Camera for skills -->
com.apple.security.automation.apple-events     <!-- AppleScript integration -->
com.apple.security.personal-information.location <!-- Location-aware skills -->
com.apple.security.cs.allow-jit                <!-- WebView JIT -->
com.apple.security.cs.allow-unsigned-executable-memory
```

### Restricted Entitlements (NOT included)

These require a provisioning profile and will cause AMFI to SIGKILL the app if included without one:

- `keychain-access-groups` -- Not needed; Data Protection keychain works without it
- `com.apple.security.network.client` -- App Sandbox only; non-sandboxed apps have network access by default
- `com.apple.security.network.server` -- Same as above

## Known Issues Resolved

### 1. App killed on launch (EPOLICY / AMFI error 163)

**Root cause:** Restricted entitlements (`keychain-access-groups`, `network.client/server`) in the entitlements file require an embedded provisioning profile. Without one, AMFI rejects the binary.

**Fix:** Removed restricted entitlements. Keychain works via Data Protection keychain without explicit access groups. Networking works without entitlements for non-sandboxed apps.

**Diagnostic:** Run the app binary directly and check AMFI logs:

```bash
/path/to/SOPHIAClaw.app/Contents/MacOS/SOPHIAClaw &
sleep 2
/usr/bin/log show --last 5s --predicate 'eventMessage CONTAINS "amfi"'
```

### 2. Keychain password prompt loop

**Root cause:** The legacy macOS keychain ties item access to the code signature of the app that created them. When the app is re-signed (every rebuild), macOS prompts for the keychain password to authorize the new signature.

**Fix:** Switched `DeviceKeychainService` to use `kSecUseDataProtectionKeychain = true` on all operations. The Data Protection keychain doesn't use legacy ACLs tied to code signatures. Existing legacy items are auto-migrated on first access.

### 3. libswiftCompatibilitySpan.dylib signature failure

**Root cause:** The packaging script bundled Apple's `libswiftCompatibilitySpan.dylib` into `Contents/Frameworks/`. This dylib has a designated requirement that it must be signed by Apple. Re-signing it with our Developer ID breaks the requirement, causing `codesign --verify --deep` to fail, and notarization to reject the submission.

**Fix:** Removed the dylib from the bundle. The binary doesn't link to it (`otool -L` confirms), and macOS 14+ ships the Swift runtime in the OS. Updated `scripts/package-mac-app.sh` to skip copying it.

## Build & Sign Workflow

### Development build (ad-hoc signed)

```bash
cd apps/macos
swift build -c debug --product SOPHIAClaw
# Ad-hoc sign with entitlements for keychain/TCC access
codesign --force --sign - --entitlements SOPHIAClaw.entitlements .build/debug/SOPHIAClaw
```

Or use the helper script:

```bash
scripts/build-and-run-mac.sh
```

### Release build (Developer ID signed + notarized)

The `package-mac-dist.sh` script handles build, signing, notarization, and zip/DMG creation in one shot:

```bash
ALLOW_MISSING_TEXTUAL_BUNDLE=1 \
NOTARIZE=1 NOTARYTOOL_PROFILE=sophiaclaw-notary \
BUNDLE_ID=bot.molt.mac \
APP_VERSION=X.Y.Z \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: Shawn Sloan (25VDK2A4QR)" \
scripts/package-mac-dist.sh
```

If the script times out waiting for notarization (Apple can be slow), the app is already built and signed at `dist/SOPHIAClaw.app`. Finish manually:

```bash
# Wait for notarization
xcrun notarytool info <submission-id> --keychain-profile "sophiaclaw-notary"

# Once accepted, staple
xcrun stapler staple dist/SOPHIAClaw.app

# Re-zip with stapled ticket
ditto -c -k --sequesterRsrc --keepParent dist/SOPHIAClaw.app dist/SOPHIAClaw-X.Y.Z.zip

# Upload to release (replace existing zip)
gh release upload vX.Y.Z dist/SOPHIAClaw-X.Y.Z.zip --clobber --repo sophiaclaw/sophiaclaw
```

### Dev build only (no notarization)

```bash
ALLOW_MISSING_TEXTUAL_BUNDLE=1 scripts/package-mac-app.sh
```

This builds, packages into `dist/SOPHIAClaw.app`, and signs with the Developer ID certificate.

### Check notarization status

```bash
xcrun notarytool info <submission-id> --keychain-profile "sophiaclaw-notary"
```

### View notarization failure log

```bash
xcrun notarytool log <submission-id> --keychain-profile "sophiaclaw-notary"
```

## Storing Credentials

If the keychain profile needs to be re-created (new machine, rotated password):

```bash
xcrun notarytool store-credentials sophiaclaw-notary \
  --apple-id <your-apple-id@example.com> \
  --team-id 25VDK2A4QR \
  --password <app-specific-password>
```

Generate the app-specific password at [appleid.apple.com](https://appleid.apple.com/account/manage) > Sign-In and Security > App-Specific Passwords.

## Auto-Poll Script

A polling script is available at `/tmp/notarize-poll.sh` that checks notarization status every 60 seconds and auto-staples + uploads when accepted. Update the submission ID in the script before running:

```bash
bash /tmp/notarize-poll.sh
```

## Verification Commands

```bash
# Check signing details
codesign -dv --verbose=4 dist/SOPHIAClaw.app

# Deep verify (checks nested code)
codesign --verify --deep --strict -v dist/SOPHIAClaw.app

# Gatekeeper assessment
spctl --assess --type exec -vvv dist/SOPHIAClaw.app

# Check what the binary links to
otool -L dist/SOPHIAClaw.app/Contents/MacOS/SOPHIAClaw

# Check AMFI logs after launch failure
/usr/bin/log show --last 10s --predicate 'eventMessage CONTAINS "amfi"'
```
