Skip to content

Commit 9fc9bcf

Browse files
committed
Make IPA installable: drop static EverywhereCore embed, ad-hoc sign
EverywhereCore is a static .a; shipping it under Frameworks/ made iOS reject installation. Remove it (Evcore is already linked into the tunnel), ad-hoc codesign the app/appex, and validate the Payload structure in CI.
1 parent 477f3dd commit 9fc9bcf

6 files changed

Lines changed: 166 additions & 80 deletions

File tree

‎.github/workflows/build-ipa.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,12 @@ jobs:
172172
body: |
173173
## decoder sec. `${{ steps.meta.outputs.version }}`
174174
175-
Two unsigned IPAs in this release:
175+
Two unsigned IPAs in this release (ad-hoc signed for install tools):
176176
177-
1. **`DecoderSec-…-lite-unsigned.ipa`** — UI only (no Packet Tunnel). Resign **once** with any profile for `com.decodersec.app`. Opens, imports configs, no VPN.
178-
2. **`DecoderSec-…-unsigned.ipa`** — full VPN. Resign with Network Extension packet-tunnel for app + `com.decodersec.app.PacketTunnel` (one command: `Scripts/resign_ipa.sh`). No App Group.
177+
1. **`DecoderSec-…-lite-unsigned.ipa`** — UI only (no Packet Tunnel). Resign once for `com.decodersec.app`.
178+
2. **`DecoderSec-…-unsigned.ipa`** — full VPN. Resign with Network Extension for app + PacketTunnel (`Scripts/resign_ipa.sh`).
179+
180+
EverywhereCore is statically linked into the tunnel — it is **not** embedded as a Frameworks binary (that broke install on beta.5).
179181
180182
```bash
181183
./Scripts/resign_ipa.sh --ipa DecoderSec-…-unsigned.ipa \

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ All notable changes to **decoder sec.** are documented here.
99
- `ru` / `en` localization
1010
- Signed TestFlight builds
1111

12+
## [0.1.0-beta.6] — 2026-08-11
13+
14+
### Fixed
15+
- **Install failure:** EverywhereCore is a static `.a` — embedding it as `Frameworks/*.framework` made iOS reject the IPA. Removed from the bundle (Evcore is already linked into `DecoderSecTunnel`)
16+
- Ad-hoc `codesign` on app + appex so Sideloadly / device install tools accept the package
17+
- CI validates: no static-lib framework binary, `Payload/*.app` structure, signature present
18+
1219
## [0.1.0-beta.5] — 2026-08-11
1320

1421
### Fixed

‎PATCHES.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ an Xcode UI edit reasserts them.
6060
Adding the product to a Copy Files phase by `productRef` double-
6161
resolves and fails with `No such file or directory` on the bare
6262
product name.
63+
- **Installable IPA note:** EverywhereCore’s ios slice is a *static*
64+
library (`.a` inside a `.framework` wrapper). Xcode links `Evcore*` into
65+
`DecoderSecTunnel` at build time. `Scripts/ci_export_ipa.sh` **removes**
66+
`Frameworks/EverywhereCore.framework` from the packaged app and applies
67+
an ad-hoc signature — shipping the static `.a` as a framework binary
68+
makes iOS refuse installation.
6369
- `ThirdParty/zashboard` registered as a folder reference (blue folder,
6470
`lastKnownFileType = folder`) on the app target's Resources phase —
6571
zashboard's `index.html` references its bundled assets via relative

‎Scripts/ci_export_ipa.sh‎

Lines changed: 131 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
#!/usr/bin/env bash
2-
# Archive DecoderSec and produce unsigned .ipa file(s) for GitHub Actions / local CI.
2+
# Archive DecoderSec and produce installable .ipa file(s) for GitHub Actions / local CI.
33
#
44
# Modes:
5-
# SIGNING=unsigned — archive with CODE_SIGNING_ALLOWED=NO, pack Payload → .ipa
5+
# SIGNING=unsigned — archive unsigned, then ad-hoc codesign + zip Payload → .ipa
66
# SIGNING=manual — use cert/profile already installed in the keychain
77
# BOTH=1 (default) — emit full VPN IPA + Lite (no PacketTunnel) from one archive
88
# LITE=1 — Lite only (UI / config browsing)
99
# LITE=0 BOTH=0 — full VPN IPA only
1010
#
11+
# Why we strip EverywhereCore.framework from the IPA:
12+
# Upstream EverywhereCore is a *static* .a wrapped as a .framework. Xcode links
13+
# Evcore* into DecoderSecTunnel (and the app) at build time. Embedding that .a
14+
# as Frameworks/EverywhereCore.framework/EverywhereCore makes iOS refuse install
15+
# ("invalid framework binary"). The tiny Xcode "codeless stub" is also useless
16+
# at runtime when everything is already statically linked. So we delete it.
17+
#
1118
# Env:
1219
# CONFIGURATION Debug|Release (default Release)
1320
# DERIVED_DATA path (default ./DerivedData)
@@ -29,9 +36,8 @@ EXPORT_OPTIONS="${EXPORT_OPTIONS:-$ROOT/Config/ExportOptions-ad-hoc.plist}"
2936
LITE="${LITE:-0}"
3037
BOTH="${BOTH:-1}"
3138

32-
# Unsigned archives often embed a ~33KB Xcode "codeless" stub instead of the
33-
# real EverywhereCore binary. Reject anything under this floor.
34-
MIN_CORE_BYTES="${MIN_CORE_BYTES:-1000000}"
39+
APP_ENTITLEMENTS="$ROOT/DecoderSec/DecoderSec.entitlements"
40+
TUNNEL_ENTITLEMENTS="$ROOT/DecoderSecTunnel/DecoderSecTunnel.entitlements"
3541

3642
mkdir -p "$(dirname "$ARCHIVE_PATH")" "$IPA_DIR" "$DERIVED_DATA"
3743
rm -f "$IPA_DIR"/*.ipa
@@ -97,68 +103,140 @@ if [[ -z "$APP_PATH" ]]; then
97103
exit 1
98104
fi
99105

100-
find_real_everywhere_core() {
101-
# Prefer the SPM binary artifact that Xcode downloaded for this build.
102-
local candidates=()
103-
while IFS= read -r -d '' f; do
104-
candidates+=("$f")
105-
done < <(find "$DERIVED_DATA/SourcePackages" \
106-
"$ROOT/DerivedData/SourcePackages" \
107-
"$HOME/Library/Developer/Xcode/DerivedData" \
108-
-type d -path '*/EverywhereCore.xcframework/ios-arm64/EverywhereCore.framework' \
109-
-print0 2>/dev/null || true)
110-
111-
local best="" best_size=0 size
112-
for f in "${candidates[@]+"${candidates[@]}"}"; do
113-
[[ -f "$f/EverywhereCore" ]] || continue
114-
size=$(wc -c < "$f/EverywhereCore" | tr -d ' ')
115-
if (( size > best_size )); then
116-
best_size=$size
117-
best=$f
118-
fi
119-
done
120-
printf '%s' "$best"
106+
is_macho_dylib() {
107+
local bin="$1"
108+
[[ -f "$bin" ]] || return 1
109+
local info
110+
info="$(file -b "$bin" 2>/dev/null || true)"
111+
# Reject static .a wrapped as "framework" binary — iOS will not install it.
112+
if grep -qi 'ar archive' <<<"$info"; then
113+
return 1
114+
fi
115+
grep -qiE 'Mach-O|dynamically linked shared library|dylib' <<<"$info"
121116
}
122117

123118
repair_app_bundle() {
124119
local app="$1"
125-
local core_src core_dst size
126120

127121
# Drop Xcode source icon bundle — runtime uses Assets.car / PNG icons.
128-
# Some Windows unpackers choke on nested .icon packages.
129122
rm -rf "$app/AppIcon.icon"
130123

131-
core_dst="$app/Frameworks/EverywhereCore.framework"
132-
mkdir -p "$app/Frameworks"
133-
core_src="$(find_real_everywhere_core)"
134-
if [[ -z "$core_src" ]]; then
135-
echo "error: could not locate EverywhereCore.xcframework ios-arm64 slice under DerivedData" >&2
136-
exit 1
124+
# EverywhereCore is a static library. Embedding it (or Xcode's stub) as a
125+
# Frameworks/*.framework binary breaks or confuses installers. Evcore is
126+
# already linked into DecoderSecTunnel; remove the embed.
127+
if [[ -d "$app/Frameworks/EverywhereCore.framework" ]]; then
128+
echo "→ removing static/stub EverywhereCore.framework (linked into binaries at build time)"
129+
rm -rf "$app/Frameworks/EverywhereCore.framework"
137130
fi
138-
size=$(wc -c < "$core_src/EverywhereCore" | tr -d ' ')
139-
echo "→ embedding EverywhereCore from $core_src ($size bytes)"
140-
if (( size < MIN_CORE_BYTES )); then
141-
echo "error: source EverywhereCore is only $size bytes (stub?)" >&2
142-
exit 1
131+
# Clean empty Frameworks dir
132+
if [[ -d "$app/Frameworks" ]] && [[ -z "$(ls -A "$app/Frameworks" 2>/dev/null || true)" ]]; then
133+
rmdir "$app/Frameworks" || true
143134
fi
144-
rm -rf "$core_dst"
145-
ditto "$core_src" "$core_dst"
146-
# Strip extended attributes that break Sideloadly / Windows unzip.
135+
136+
# Fix any remaining framework Info.plist MinimumOSVersion=100.0 (gomobile artifact)
137+
while IFS= read -r -d '' plist; do
138+
if command -v plutil >/dev/null 2>&1; then
139+
local min
140+
min="$(plutil -extract MinimumOSVersion raw "$plist" 2>/dev/null || true)"
141+
if [[ "$min" == "100.0" || "$min" == "100" ]]; then
142+
plutil -replace MinimumOSVersion -string "15.0" "$plist"
143+
echo "→ fixed MinimumOSVersion in $plist"
144+
fi
145+
fi
146+
done < <(find "$app" -name 'Info.plist' -print0)
147+
147148
if command -v xattr >/dev/null 2>&1; then
148149
xattr -cr "$app" 2>/dev/null || true
149150
fi
150-
size=$(wc -c < "$core_dst/EverywhereCore" | tr -d ' ')
151-
if (( size < MIN_CORE_BYTES )); then
152-
echo "error: embedded EverywhereCore is only $size bytes after copy" >&2
151+
}
152+
153+
adhoc_sign_bundle() {
154+
local app="$1"
155+
if ! command -v codesign >/dev/null 2>&1; then
156+
echo "error: codesign missing — cannot produce installable IPA on this host" >&2
157+
exit 1
158+
fi
159+
160+
echo "→ ad-hoc codesign (required for Sideloadly / device install)"
161+
162+
# Nested Mach-O first: dylibs, then frameworks, then appex, then app.
163+
while IFS= read -r -d '' bin; do
164+
codesign --force --sign - --timestamp=none "$bin" 2>/dev/null || true
165+
done < <(find "$app" -type f \( -name '*.dylib' -o -name '*.so' \) -print0 2>/dev/null || true)
166+
167+
while IFS= read -r -d '' fw; do
168+
codesign --force --sign - --timestamp=none "$fw"
169+
done < <(find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' -print0 2>/dev/null || true)
170+
171+
while IFS= read -r -d '' appex; do
172+
if [[ -f "$TUNNEL_ENTITLEMENTS" ]]; then
173+
codesign --force --sign - --timestamp=none --entitlements "$TUNNEL_ENTITLEMENTS" "$appex"
174+
else
175+
codesign --force --sign - --timestamp=none "$appex"
176+
fi
177+
done < <(find "$app/PlugIns" -maxdepth 1 -type d -name '*.appex' -print0 2>/dev/null || true)
178+
179+
if [[ -f "$APP_ENTITLEMENTS" ]]; then
180+
codesign --force --sign - --timestamp=none --entitlements "$APP_ENTITLEMENTS" "$app"
181+
else
182+
codesign --force --sign - --timestamp=none "$app"
183+
fi
184+
185+
if [[ ! -d "$app/_CodeSignature" ]]; then
186+
echo "error: ad-hoc codesign did not produce _CodeSignature" >&2
153187
exit 1
154188
fi
155-
echo "✓ EverywhereCore embedded ($size bytes)"
189+
# --no-strict: ad-hoc + NE entitlements without a profile still verify structurally.
190+
if ! codesign --verify --no-strict "$app" >/dev/null 2>&1; then
191+
echo "warning: codesign --verify reported issues (continuing; Sideloadly will re-sign)" >&2
192+
codesign -dv "$app" 2>&1 | tail -8 || true
193+
fi
194+
echo "✓ ad-hoc signature present"
195+
}
196+
197+
validate_installable_app() {
198+
local app="$1"
199+
local lite="$2"
200+
201+
[[ -f "$app/Info.plist" ]] || { echo "error: missing Info.plist"; return 1; }
202+
[[ -x "$app/DecoderSec" || -f "$app/DecoderSec" ]] || { echo "error: missing DecoderSec binary"; return 1; }
203+
204+
# Must not ship a static .a as a framework binary.
205+
if [[ -f "$app/Frameworks/EverywhereCore.framework/EverywhereCore" ]]; then
206+
if ! is_macho_dylib "$app/Frameworks/EverywhereCore.framework/EverywhereCore"; then
207+
echo "error: EverywhereCore.framework binary is not a dylib (static .a breaks install)" >&2
208+
file "$app/Frameworks/EverywhereCore.framework/EverywhereCore" >&2 || true
209+
return 1
210+
fi
211+
fi
212+
213+
if [[ "$lite" != "1" ]]; then
214+
[[ -d "$app/PlugIns/DecoderSecTunnel.appex" ]] || {
215+
echo "error: full IPA missing DecoderSecTunnel.appex" >&2
216+
return 1
217+
}
218+
[[ -f "$app/PlugIns/DecoderSecTunnel.appex/DecoderSecTunnel" ]] || {
219+
echo "error: missing tunnel binary" >&2
220+
return 1
221+
}
222+
else
223+
if [[ -d "$app/PlugIns" ]]; then
224+
echo "error: lite IPA still has PlugIns/" >&2
225+
return 1
226+
fi
227+
fi
228+
229+
# Ad-hoc signature must exist for install tools.
230+
if [[ ! -d "$app/_CodeSignature" ]]; then
231+
echo "error: app is not codesigned (ad-hoc required for install)" >&2
232+
return 1
233+
fi
156234
}
157235

158236
pack_ipa() {
159237
local lite="$1"
160238
local name="$2"
161-
local stage app_name app_copy
239+
local stage app_name app_copy out check
162240
stage="$(mktemp -d)"
163241
mkdir -p "$stage/Payload"
164242
ditto "$APP_PATH" "$stage/Payload/$(basename "$APP_PATH")"
@@ -171,46 +249,23 @@ pack_ipa() {
171249
rm -rf "$app_copy/PlugIns"
172250
fi
173251

174-
# Sideload-friendly IPA: strip macOS extras (-X). Always keep Payload/ as zip root.
175-
local out="$IPA_DIR/$name"
252+
adhoc_sign_bundle "$app_copy"
253+
validate_installable_app "$app_copy" "$lite"
254+
255+
out="$IPA_DIR/$name"
176256
rm -f "$out"
177-
if [[ ! -f "$app_copy/Info.plist" ]]; then
178-
echo "error: staged app missing Info.plist before zip" >&2
179-
rm -rf "$stage"
180-
exit 1
181-
fi
182257
(
183258
cd "$stage"
184-
# -X strips uid/gid/xattr; -9 deflates large binaries. Do NOT use ditto here:
185-
# ditto -ck without keepParent can omit the Payload/ root and break Sideloadly.
186259
zip -r -X -9 "$out" Payload
187260
)
188261

189-
# Validate: must unpack cleanly and contain Payload/*.app/Info.plist
190-
local check
191262
check="$(mktemp -d)"
192263
unzip -q "$out" -d "$check"
193-
if [[ ! -f "$check/Payload/$app_name/Info.plist" ]]; then
194-
echo "error: packed IPA missing Payload/$app_name/Info.plist" >&2
195-
echo "archive top-level:" >&2
196-
unzip -l "$out" | head -30 >&2
197-
rm -rf "$stage" "$check"
198-
exit 1
199-
fi
200-
local core_size=0
201-
if [[ -f "$check/Payload/$app_name/Frameworks/EverywhereCore.framework/EverywhereCore" ]]; then
202-
core_size=$(wc -c < "$check/Payload/$app_name/Frameworks/EverywhereCore.framework/EverywhereCore" | tr -d ' ')
203-
fi
204-
if (( core_size < MIN_CORE_BYTES )); then
205-
echo "error: IPA EverywhereCore is $core_size bytes — refusing to ship a stub" >&2
206-
rm -rf "$stage" "$check"
207-
exit 1
208-
fi
209-
# zip integrity
264+
validate_installable_app "$check/Payload/$app_name" "$lite"
210265
unzip -t "$out" >/dev/null
211266
rm -rf "$stage" "$check"
212267

213-
echo "✓ unsigned IPA: $out ($(du -h "$out" | awk '{print $1}'), EverywhereCore=${core_size} bytes)"
268+
echo "✓ IPA: $out ($(du -h "$out" | awk '{print $1}'))"
214269
}
215270

216271
if [[ "$IS_BOTH" == "1" ]]; then

‎Scripts/resign_ipa.sh‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,18 @@ unzip -q "$IPA" -d "$WORK/unpacked"
271271
APP_CANDIDATES=("$WORK"/unpacked/Payload/*.app)
272272
[[ -d "${APP_CANDIDATES[0]}" ]] || die "no .app found in IPA Payload"
273273
APP_PATH="${APP_CANDIDATES[0]}"
274+
275+
# Static EverywhereCore .a embedded as a framework breaks device install.
276+
if [[ -f "$APP_PATH/Frameworks/EverywhereCore.framework/EverywhereCore" ]]; then
277+
if file "$APP_PATH/Frameworks/EverywhereCore.framework/EverywhereCore" | grep -qi 'ar archive'; then
278+
echo "==> removing non-installable static EverywhereCore.framework"
279+
rm -rf "$APP_PATH/Frameworks/EverywhereCore.framework"
280+
fi
281+
fi
282+
if [[ -d "$APP_PATH/Frameworks" ]] && [[ -z "$(ls -A "$APP_PATH/Frameworks" 2>/dev/null || true)" ]]; then
283+
rmdir "$APP_PATH/Frameworks" || true
284+
fi
285+
274286
APP_INFO="$APP_PATH/Info.plist"
275287
APP_BUNDLE_ID="$(plist_print "$APP_INFO" "CFBundleIdentifier")"
276288
[[ -n "$APP_BUNDLE_ID" ]] || die "could not read app bundle identifier"

‎docs/RELEASES.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ git tag -a v0.1.0-beta.5 -m "decoder sec. v0.1.0-beta.5"
5151
git push origin v0.1.0-beta.5
5252
```
5353

54-
If an older IPA “cannot unpack” in Sideloadly / 3uTools: use **beta.5+** (packaged with `ditto --norsrc` and a real EverywhereCore binary).
54+
If an older IPA cannot unpack or install: use **beta.6+**.
55+
56+
- beta.4 and earlier: packaging / unpack issues
57+
- beta.5: shipped a static EverywhereCore `.a` as a framework → iOS refuses install
58+
- beta.6: removes that framework embed + ad-hoc codesign
5559
## Manual prerelease
5660

5761
Actions → **Build IPA** → Run workflow with `publish_prerelease: true`.

0 commit comments

Comments
 (0)