diff --git a/.env.example b/.env.example
index 20bb71e..f320ff7 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,10 @@
+# Development signing (./build.sh or ./build.sh dev)
CODESIGN_IDENTITY="Apple Development: Your Name (XXXXXXXXXX)"
BUNDLE_ID="com.yourcompany.plc-touch"
TEAM_ID="XXXXXXXXXX"
+
+# Release signing (./build.sh release)
+# Requires a "Developer ID Application" certificate from developer.apple.com
+DEVELOPER_ID="Developer ID Application: Your Name (XXXXXXXXXX)"
+APPLE_ID="your@email.com"
+NOTARIZE_PASSWORD="xxxx-xxxx-xxxx-xxxx" # App-specific password from appleid.apple.com
diff --git a/.gitignore b/.gitignore
index ff2b536..2ec82ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ Cargo.lock
# Apple signing (contains device UDIDs and developer certificates)
embedded.provisionprofile
+release.provisionprofile
entitlements.plist
# Claude Code local settings
diff --git a/build.sh b/build.sh
index ab817eb..a008325 100755
--- a/build.sh
+++ b/build.sh
@@ -14,10 +14,33 @@ fi
# CODESIGN_IDENTITY="Apple Development: Your Name (XXXXXXXXXX)"
# BUNDLE_ID="com.yourcompany.plc-touch"
# TEAM_ID="XXXXXXXXXX"
-IDENTITY="${CODESIGN_IDENTITY:?Set CODESIGN_IDENTITY in .env or as env var}"
+#
+# For release builds, also set:
+# DEVELOPER_ID="Developer ID Application: Your Name (XXXXXXXXXX)"
+# APPLE_ID="your@email.com"
+# NOTARIZE_PASSWORD="app-specific-password"
BUNDLE_ID="${BUNDLE_ID:-com.example.plc-touch}"
TEAM_ID="${TEAM_ID:-XXXXXXXXXX}"
+MODE="${1:-dev}"
+
+case "$MODE" in
+ dev)
+ IDENTITY="${CODESIGN_IDENTITY:?Set CODESIGN_IDENTITY in .env}"
+ ;;
+ release)
+ IDENTITY="${DEVELOPER_ID:?Set DEVELOPER_ID in .env for release builds}"
+ ;;
+ *)
+ echo "Usage: ./build.sh [dev|release]"
+ echo " dev — Development build with provisioning profile (default)"
+ echo " release — Developer ID build with notarization for distribution"
+ exit 1
+ ;;
+esac
+
+echo "Building plc-touch ($MODE)..."
+
KEYCHAIN_ACCESS_GROUP="${TEAM_ID}.${BUNDLE_ID}" cargo build --release
# Create .app bundle
@@ -26,9 +49,11 @@ mkdir -p "$APP/Contents/MacOS"
cp target/release/plc-touch "$APP/Contents/MacOS/plc-touch"
-# Copy provisioning profile if it exists
-if [ -f embedded.provisionprofile ]; then
+# Embed provisioning profile
+if [ "$MODE" = "dev" ] && [ -f embedded.provisionprofile ]; then
cp embedded.provisionprofile "$APP/Contents/embedded.provisionprofile"
+elif [ "$MODE" = "release" ] && [ -f release.provisionprofile ]; then
+ cp release.provisionprofile "$APP/Contents/embedded.provisionprofile"
fi
cat > "$APP/Contents/Info.plist" << EOF
@@ -54,9 +79,11 @@ cat > "$APP/Contents/Info.plist" << EOF
EOF
-# Generate entitlements from env vars
+# Generate entitlements
ENTITLEMENTS_FILE=$(mktemp)
-cat > "$ENTITLEMENTS_FILE" << EOF
+if [ "$MODE" = "release" ]; then
+ # Developer ID with provisioning profile: includes application-identifier + keychain-access-groups
+ cat > "$ENTITLEMENTS_FILE" << EOF
@@ -70,9 +97,71 @@ cat > "$ENTITLEMENTS_FILE" << EOF
EOF
+else
+ # Dev: needs application-identifier for provisioning profile
+ cat > "$ENTITLEMENTS_FILE" << EOF
+
+
+
+
+ com.apple.application-identifier
+ ${TEAM_ID}.${BUNDLE_ID}
+ keychain-access-groups
+
+ ${TEAM_ID}.*
+
+
+
+EOF
+fi
+
+if [ "$MODE" = "release" ]; then
+ # Release: Developer ID signing with hardened runtime (required for notarization)
+ codesign --force --sign "$IDENTITY" \
+ --options runtime \
+ --timestamp \
+ --entitlements "$ENTITLEMENTS_FILE" \
+ "$APP"
+
+ rm -f "$ENTITLEMENTS_FILE"
+
+ echo "✓ Signed with Developer ID"
+
+ # Create zip for notarization
+ ZIP="target/release/plc-touch.zip"
+ rm -f "$ZIP"
+ ditto -c -k --keepParent "$APP" "$ZIP"
-codesign --force --sign "$IDENTITY" --entitlements "$ENTITLEMENTS_FILE" "$APP"
-rm -f "$ENTITLEMENTS_FILE"
+ echo "Submitting for notarization..."
+ APPLE_ID_ARG="${APPLE_ID:?Set APPLE_ID in .env for notarization}"
+ PASS_ARG="${NOTARIZE_PASSWORD:?Set NOTARIZE_PASSWORD in .env (app-specific password)}"
-echo "✓ Built and signed $APP"
-echo " Run with: $APP/Contents/MacOS/plc-touch"
+ xcrun notarytool submit "$ZIP" \
+ --apple-id "$APPLE_ID_ARG" \
+ --team-id "$TEAM_ID" \
+ --password "$PASS_ARG" \
+ --wait
+
+ # Staple the notarization ticket to the app
+ xcrun stapler staple "$APP"
+
+ # Re-create zip with stapled app
+ rm -f "$ZIP"
+ ditto -c -k --keepParent "$APP" "$ZIP"
+
+ echo ""
+ echo "✓ Built, signed, notarized, and stapled"
+ echo " Distribute: $ZIP"
+ echo " Run with: $APP/Contents/MacOS/plc-touch"
+else
+ # Dev: Apple Development signing
+ codesign --force --sign "$IDENTITY" \
+ --entitlements "$ENTITLEMENTS_FILE" \
+ "$APP"
+
+ rm -f "$ENTITLEMENTS_FILE"
+
+ echo ""
+ echo "✓ Built and signed (dev)"
+ echo " Run with: $APP/Contents/MacOS/plc-touch"
+fi
diff --git a/src/enclave.rs b/src/enclave.rs
index 5a63479..ade7f79 100644
--- a/src/enclave.rs
+++ b/src/enclave.rs
@@ -145,7 +145,7 @@ pub fn generate_key(label: &str, syncable: bool) -> Result {
CFString::wrap_under_get_rule(kSecAttrSynchronizable),
CFBoolean::true_value().as_CFType(),
));
- // Use explicit access group so the key is findable across devices
+ // Explicit access group so the key is findable across devices
attrs_pairs.push((
CFString::wrap_under_get_rule(kSecAttrAccessGroup),
CFString::new(keychain_access_group()).as_CFType(),