diff --git a/.gitignore b/.gitignore index 5bc9e1b1..8620db90 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,8 @@ ShadowsocksX-NG2/DerivedData/ #### # NG2 generated build files (project.yml is the source of truth; run `xcodegen generate`) ShadowsocksX-NG2/*.xcodeproj +# 本机签名身份(DEVELOPMENT_TEAM),构建期由 Configs/Signing.xcconfig 可选包含 +ShadowsocksX-NG2/Configs/Signing.local.xcconfig ShadowsocksX-NG2/App/Resources/Info.plist ShadowsocksX-NG2/App/Resources/ShadowsocksX-NG2.entitlements ShadowsocksX-NG2/Tests/Info.plist diff --git a/ShadowsocksX-NG2/Configs/Signing-Debug.xcconfig b/ShadowsocksX-NG2/Configs/Signing-Debug.xcconfig new file mode 100644 index 00000000..2de281c7 --- /dev/null +++ b/ShadowsocksX-NG2/Configs/Signing-Debug.xcconfig @@ -0,0 +1,17 @@ +// Debug 签名基线:无证书即可构建(贡献者与 CI)。 +// +// 基线是 ad-hoc(`-`)。arm64 上代码至少要有一层有效签名才会被加载,所以「免证书」 +// 不能靠不签名,只能靠 ad-hoc。ad-hoc 产物可以构建、可以跑 app、可以跑全部单测, +// 但过不了 Scripts/packaging-gate.sh —— 那是有证书的发布门槛。 +// +// 有证书的人(含维护者)在 Configs/Signing.local.xcconfig 里显式升级,例如: +// +// DEVELOPMENT_TEAM = <10 位 Apple Developer Team ID> +// CODE_SIGN_IDENTITY = Developer ID Application +// +// 该本机文件在 .gitignore 中,不入库;CI 不需要它。 +// +// `#include?` 必须留在最后一行:xcconfig 中后出现的赋值优先,本机文件才能盖掉 +// 上面的 ad-hoc 基线。 +CODE_SIGN_IDENTITY = - +#include? "Signing.local.xcconfig" diff --git a/ShadowsocksX-NG2/Configs/Signing-Release.xcconfig b/ShadowsocksX-NG2/Configs/Signing-Release.xcconfig new file mode 100644 index 00000000..8638d43e --- /dev/null +++ b/ShadowsocksX-NG2/Configs/Signing-Release.xcconfig @@ -0,0 +1,12 @@ +// Release 签名基线:发布产物必须是 Developer ID 真签名(Hardened Runtime、公证与 +// Gatekeeper 的前提),因此这里不给免证书路径。 +// +// Team 只能来自本机注入的 Configs/Signing.local.xcconfig(gitignore)。缺失时 +// DEVELOPMENT_TEAM 为空,Xcode 以「Signing for ... requires selecting either a +// development team or a provisioning profile」使构建明确失败,不会静默降级成 +// ad-hoc。 +// +// `#include?` 放在 Developer ID 之前:本机文件可以补 Team(另一个键),但不能把 +// 发布身份改成 ad-hoc 或开发证书——Release 基线在这里是硬约束,后赋值者胜。 +#include? "Signing.local.xcconfig" +CODE_SIGN_IDENTITY = Developer ID Application diff --git a/ShadowsocksX-NG2/README.md b/ShadowsocksX-NG2/README.md index 761429ea..57617300 100644 --- a/ShadowsocksX-NG2/README.md +++ b/ShadowsocksX-NG2/README.md @@ -59,8 +59,22 @@ ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配 ## 签名与运行形态基线 - bundle id `com.qiuyuzhou.ShadowsocksX-NG2`,与 Legacy(`com.qiuyuzhou.ShadowsocksX-NG`)区分;运行时数据落 `~/Library/Application Support/ShadowsocksX-NG2/`。macOS 15+ / arm64。 -- Hardened Runtime + Developer ID 签名(`DEVELOPMENT_TEAM` / 证书见 `project.yml`);不启用 App Sandbox,entitlements 为空 dict。 -- 测试 target 用 Apple Development 证书签名:宿主 app 开启 Hardened Runtime 后 library validation 要求被注入的 xctest 包同 Team 签名。 +- Hardened Runtime(DevID 发布路径;免证书的 ad-hoc Debug 例外,见下);不启用 App Sandbox,entitlements 为空 dict;签名样式固定 `Manual`。 +- **证书与 Team 都不入库**(开源仓库):`project.yml` 不含 `CODE_SIGN_IDENTITY` / `DEVELOPMENT_TEAM`,两个键由 project xcconfig 按 config 在构建期注入: + - `Configs/Signing-Debug.xcconfig`(入库)基线是 **ad-hoc(`-`)**:新 clone 与 CI 不需要任何证书就能 `task build` / `task test`。arm64 要求代码至少有 ad-hoc 签名才会被加载,所以「免证书」不是「不签名」。ad-hoc 产物不带 Hardened Runtime(Xcode 对 ad-hoc 签名不传 `-o runtime`,实测;`Scripts/sign-embedded-helpers.sh` 的 ad-hoc 分支同样不加),Hardened Runtime 由 DevID 发布路径与打包门槛断言。 + - `Configs/Signing-Release.xcconfig`(入库)基线是 **Developer ID Application**:发布产物必须真签名,这里不给免证书路径。 + - 两者都可选包含 `Configs/Signing.local.xcconfig`(gitignore)。有证书的人(含维护者)用它升级本机身份,例如让 Debug 也走 Developer ID: + + ```bash + cat > Configs/Signing.local.xcconfig <<'EOF' + DEVELOPMENT_TEAM = YOUR_TEAM_ID + CODE_SIGN_IDENTITY = Developer ID Application + EOF + ``` + + - 该文件缺失时 Debug 构建为 ad-hoc(可跑单测),Release 构建以 Xcode 的「Signing for … requires selecting either a development team or a provisioning profile」明确失败,不静默降级。 + - 测试 target(单测与冒烟)身份随同一基线:去宿主化(ADR-0021)后 xctest 包不再注入宿主 app,ad-hoc 也能加载执行(实测),因此测试无需证书。 + - 注意 `project.yml` 一旦重新设置这两个键,pbxproj 构建设置会盖掉 xcconfig(`Tests/Architecture/SigningConfigurationTests.swift` 守卫该约束)。 - Release 配置关闭 `CODE_SIGN_INJECT_BASE_ENTITLEMENTS`,保证发布产物 entitlements 只来自项目文件。 ## 开发、构建与测试 @@ -86,6 +100,8 @@ Scripts/fetch-external-binaries.sh task gen:prj ``` +无证书也能构建与跑单测:Debug 基线是 ad-hoc,不需要 `Configs/Signing.local.xcconfig`。只有要做 Developer ID 签名(发布,或让本机 Debug 产物过打包门槛)时才写该文件,内容见上节「签名与运行形态基线」。 + `Tests/Smoke/` 是真实进程冒烟(经 wrapper 拉起真实 sslocal 验证启动、握手、 ACL 路由与插件监管),不在默认 `task test` 内,由独立 scheme 按需运行。 @@ -98,6 +114,8 @@ Scripts/packaging-gate.sh \ .build/derivedData/Build/Products/Debug/ShadowsocksX-NG2.app ``` +打包门槛只接受 Developer ID 签名的产物:Team 期望值取自 `Configs/Signing.local.xcconfig`(可用 `EXPECTED_TEAM_ID=` 显式覆盖),两者都没有时该断言直接失败,不静默跳过。无证书的 ad-hoc Debug 产物会在此明确失败(`gate: hint` 会说明原因),这是预期行为。 + ## 代码风格工具 - `swift format` 随 Xcode/Swift 工具链提供,无需单独安装(基线:Apple Swift 6.4);SwiftLint 需 `brew install swiftlint`(基线 0.65.0)。工具缺失时,含本目录 Swift 文件的提交会被 pre-commit 钩子拒绝而不是放行。 diff --git a/ShadowsocksX-NG2/Scripts/packaging-gate.sh b/ShadowsocksX-NG2/Scripts/packaging-gate.sh index 7f490707..76bea276 100755 --- a/ShadowsocksX-NG2/Scripts/packaging-gate.sh +++ b/ShadowsocksX-NG2/Scripts/packaging-gate.sh @@ -12,7 +12,7 @@ # 4. The manifest set exactly covers every Mach-O under Contents/Helpers. # # Usage: Scripts/packaging-gate.sh path/to/ShadowsocksX-NG2.app -# EXPECTED_TEAM_ID overrides the pinned Team ID check. +# EXPECTED_TEAM_ID overrides the Team ID read from the local signing config. set -u @@ -21,8 +21,20 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) NG2_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) VENDOR_DIR="$NG2_ROOT/Vendor" -# Mirrors DEVELOPMENT_TEAM in project.yml. -EXPECTED_TEAM_ID=${EXPECTED_TEAM_ID:-S878RH3PA8} + +# Expected Team ID is injected, not pinned in the repo (open source). The same +# file the build reads supplies it here: Configs/Signing.local.xcconfig +# (gitignored). An explicit EXPECTED_TEAM_ID wins. Knowing no expected team is a +# hard failure: an unsigned or wrong-team bundle must not pass just because +# there is nothing to compare against. +LOCAL_SIGNING_CONFIG="$NG2_ROOT/Configs/Signing.local.xcconfig" +EXPECTED_TEAM_ID=${EXPECTED_TEAM_ID:-} +if [ -z "$EXPECTED_TEAM_ID" ] && [ -f "$LOCAL_SIGNING_CONFIG" ]; then + EXPECTED_TEAM_ID=$( + sed -n 's/^[[:space:]]*DEVELOPMENT_TEAM[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' \ + "$LOCAL_SIGNING_CONFIG" | tail -1 + ) +fi APP=${1:-} [ -n "$APP" ] || { @@ -81,9 +93,19 @@ app_team=$(dv_value "$app_dv" TeamIdentifier) app_authority=$(dv_value "$app_dv" Authority) app_flags=$(dv_value "$app_dv" flags) -[ "$app_team" = "$EXPECTED_TEAM_ID" ] || - fail "main app TeamIdentifier is $EXPECTED_TEAM_ID (got: ${app_team:-none})" +if [ -z "$EXPECTED_TEAM_ID" ]; then + fail "expected Team ID is known (set EXPECTED_TEAM_ID or write DEVELOPMENT_TEAM in Configs/Signing.local.xcconfig)" +else + [ "$app_team" = "$EXPECTED_TEAM_ID" ] || + fail "main app TeamIdentifier is $EXPECTED_TEAM_ID (got: ${app_team:-none})" +fi check_devid_and_runtime "main app" "$app_dv" +case "$app_authority" in + "Developer ID Application:"*) ;; + *) + echo "gate: hint ad-hoc / non-Developer-ID artifacts come from the certificate-less Debug baseline; only a Developer ID signed release artifact can pass" + ;; +esac app_entitlements=$(codesign -d --entitlements - --xml "$APP" 2>/dev/null || true) for forbidden in com.apple.security.app-sandbox com.apple.security.network.server; do diff --git a/ShadowsocksX-NG2/Scripts/sign-embedded-helpers.sh b/ShadowsocksX-NG2/Scripts/sign-embedded-helpers.sh index 9d87b314..b3f66def 100755 --- a/ShadowsocksX-NG2/Scripts/sign-embedded-helpers.sh +++ b/ShadowsocksX-NG2/Scripts/sign-embedded-helpers.sh @@ -2,11 +2,17 @@ # Re-sign every embedded helper binary with the Developer ID identity before # Xcode signs the app itself (spec #21 D6/D10). # -# Upstream ships these binaries ad-hoc signed; they must carry our Developer ID -# signature with Hardened Runtime and a stable code-signing identifier so the -# outer app seal, notarization and Gatekeeper cover them. Xcode runs this as a -# post-build script phase and applies the app's own signature afterwards, so -# the outer signature embeds the re-signed nested code. +# Upstream ships these binaries ad-hoc signed; they must carry a signature with +# Hardened Runtime and a stable code-signing identifier so the outer app seal, +# notarization and Gatekeeper cover them. Xcode runs this as a post-build script +# phase and applies the app's own signature afterwards, so the outer signature +# embeds the re-signed nested code. +# +# The identity comes from the build configuration: Developer ID on a release +# build, ad-hoc ("-") on the certificate-less Debug baseline. Ad-hoc mirrors +# Xcode's own ad-hoc signing: no secure timestamp and no Hardened Runtime (Xcode +# omits -o runtime for ad-hoc; hardened runtime with an untrusted identity would +# only get in the way of local debugging). The release path is unchanged. # # The identifier scheme is pinned in the manifests: . for # sslocal, .plugin. for SIP003 plugins. @@ -45,7 +51,11 @@ for manifest in $manifests; do exit 1 } - codesign --force --sign "$IDENTITY" --timestamp --options runtime \ - --identifier "$identifier" "$target" + if [ "$IDENTITY" = "-" ]; then + codesign --force --sign - --identifier "$identifier" "$target" + else + codesign --force --sign "$IDENTITY" --timestamp --options runtime \ + --identifier "$identifier" "$target" + fi echo "sign($binary): $identifier" done diff --git a/ShadowsocksX-NG2/Tests/Architecture/SigningConfigurationTests.swift b/ShadowsocksX-NG2/Tests/Architecture/SigningConfigurationTests.swift new file mode 100644 index 00000000..ff614bfb --- /dev/null +++ b/ShadowsocksX-NG2/Tests/Architecture/SigningConfigurationTests.swift @@ -0,0 +1,141 @@ +import Foundation +import Testing + +/// 签名身份注入守卫:证书与 Team 都不入库(开源仓库),由 config 基线 + 本机 +/// xcconfig 在构建期决定。 +/// +/// 这条约束容易被无声破坏:`project.yml` 一旦重新设置 `DEVELOPMENT_TEAM` 或 +/// `CODE_SIGN_IDENTITY`,pbxproj 里的构建设置会盖掉 `Configs/Signing-*.xcconfig` +/// (Xcode 构建设置优先级,实测),注入失效却不报错。本套用例把整条链条钉住: +/// 工程定义不碰签名键 → config 基线各自正确且本机文件能(或不能)盖掉它 → +/// 本机文件不入库 → 供应链脚本与发布门槛跟着基线走。 +struct SigningConfigurationTests { + private static let ng2Root = TestSourceTree.ng2Root() + private static let repositoryRoot = ng2Root.deletingLastPathComponent() + + /// 逐行匹配:`String.CompareOptions` 没有 `.anchorsMatchLines`,整段文本上 + /// `^`/`$` 只锚定字符串首尾,因此把行切开再套正则。 + private static func matchingLines(_ pattern: String, in text: String) -> [String] { + text.split(separator: "\n", omittingEmptySubsequences: false) + .filter { $0.range(of: pattern, options: .regularExpression) != nil } + .map(String.init) + } + + private static func firstLineIndex(matching pattern: String, in text: String) -> Int? { + text.split(separator: "\n", omittingEmptySubsequences: false) + .firstIndex { $0.range(of: pattern, options: .regularExpression) != nil } + } + + private static func text(_ relativePath: String) throws -> String { + try String(contentsOf: ng2Root.appendingPathComponent(relativePath), encoding: .utf8) + } + + /// 只命中 YAML 键(行首为 `KEY:`),注释行以 `#` 开头,不算设置。 + private static func yamlKeyPattern(_ key: String) -> String { #"^\s*\#(key)\s*:"# } + + private static let identityAssignment = #"^\s*CODE_SIGN_IDENTITY\s*="# + private static let localInclude = #"^\s*#include\?\s+"Signing\.local\.xcconfig"# + + @Test + func projectSpecDoesNotPinSigningKeys() throws { + let spec = try Self.text("project.yml") + for key in ["DEVELOPMENT_TEAM", "CODE_SIGN_IDENTITY"] { + let pinned = Self.matchingLines(Self.yamlKeyPattern(key), in: spec) + #expect( + pinned.isEmpty, + "project.yml 不得设置 \(key)(pbxproj 设置会静默盖掉 Configs/Signing-*.xcconfig,且证书/Team 不应入库):\(pinned)" + ) + } + } + + @Test + func projectSpecInjectsPerConfigurationBaselines() throws { + let spec = try Self.text("project.yml") + let expected = [ + "Debug": "Configs/Signing-Debug.xcconfig", + "Release": "Configs/Signing-Release.xcconfig", + ] + for (configuration, path) in expected { + let pattern = #"^\s*\#(configuration):\s*\#(path)\s*$"# + #expect( + !Self.matchingLines(pattern, in: spec).isEmpty, + "project.yml 的 configFiles 必须把 \(configuration) 挂到 \(path)") + } + } + + /// Debug 必须无证书可构建:基线 ad-hoc,且本机文件能把它升级成真证书。 + @Test + func debugBaselineIsAdHocAndUpgradableByLocalFile() throws { + let config = try Self.text("Configs/Signing-Debug.xcconfig") + #expect( + Self.matchingLines(#"^\s*CODE_SIGN_IDENTITY\s*=\s*-\s*$"#, in: config).count == 1, + "Configs/Signing-Debug.xcconfig 必须恰好有一条 ad-hoc(-)CODE_SIGN_IDENTITY 基线") + let baseline = try #require( + Self.firstLineIndex(matching: Self.identityAssignment, in: config), + "Configs/Signing-Debug.xcconfig 缺少 CODE_SIGN_IDENTITY 基线") + let include = try #require( + Self.firstLineIndex(matching: Self.localInclude, in: config), + "Configs/Signing-Debug.xcconfig 必须包含 Configs/Signing.local.xcconfig") + #expect( + baseline < include, + "Debug 的 #include? 必须在 ad-hoc 基线之后:xcconfig 后赋值者胜,本机文件才能升级身份") + } + + /// Release 必须真签名:本机文件只能补 Team,不能把发布身份改成 ad-hoc。 + @Test + func releaseBaselineStaysDeveloperID() throws { + let config = try Self.text("Configs/Signing-Release.xcconfig") + let assignments = Self.matchingLines(Self.identityAssignment, in: config) + #expect( + assignments.last?.contains("Developer ID Application") == true, + "Configs/Signing-Release.xcconfig 最后生效的 CODE_SIGN_IDENTITY 必须是 Developer ID Application:\(assignments)" + ) + let include = try #require( + Self.firstLineIndex(matching: Self.localInclude, in: config), + "Configs/Signing-Release.xcconfig 必须包含 Configs/Signing.local.xcconfig") + let baseline = try #require( + Self.firstLineIndex(matching: Self.identityAssignment, in: config), + "Configs/Signing-Release.xcconfig 缺少 CODE_SIGN_IDENTITY 基线") + #expect(include < baseline, "Release 的 #include? 必须在 Developer ID 基线之前,发布身份不被本机文件削弱") + } + + @Test + func committedBaselinesDoNotPinTeamID() throws { + for path in ["Configs/Signing-Debug.xcconfig", "Configs/Signing-Release.xcconfig"] { + let config = try Self.text(path) + #expect( + Self.matchingLines(#"^\s*DEVELOPMENT_TEAM\s*="#, in: config).isEmpty, + "入库的 \(path) 不得固化 DEVELOPMENT_TEAM") + } + } + + @Test + func localSigningConfigIsGitIgnored() throws { + let ignoreList = try String( + contentsOf: Self.repositoryRoot.appendingPathComponent(".gitignore"), encoding: .utf8) + #expect( + ignoreList.contains("ShadowsocksX-NG2/Configs/Signing.local.xcconfig"), + ".gitignore 必须忽略 Configs/Signing.local.xcconfig(本机证书/Team 不入库)") + } + + /// 嵌套二进制重签必须跟着基线走:ad-hoc 构建没有 secure timestamp。 + @Test + func embeddedHelperSigningHandlesAdHocIdentity() throws { + let script = try Self.text("Scripts/sign-embedded-helpers.sh") + #expect( + !Self.matchingLines(#"--sign -"#, in: script).isEmpty, + "sign-embedded-helpers.sh 必须有无证书(ad-hoc)分支,否则 Debug 基线无法封套嵌套二进制") + } + + /// 发布门槛的 Team 期望值同样不许写死:只能取自同一个注入文件或显式环境变量。 + @Test + func packagingGateDerivesTeamIDFromInjectedConfig() throws { + let gate = try Self.text("Scripts/packaging-gate.sh") + #expect( + gate.contains("Signing.local.xcconfig"), + "packaging-gate.sh 必须从 Configs/Signing.local.xcconfig 读取期望 Team ID") + #expect( + Self.matchingLines(#":-[A-Z0-9]{10}\}"#, in: gate).isEmpty, + "packaging-gate.sh 不得给 Team ID 留 10 位写死的默认值") + } +} diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml index 024446b6..f98e9b52 100644 --- a/ShadowsocksX-NG2/project.yml +++ b/ShadowsocksX-NG2/project.yml @@ -13,12 +13,22 @@ settings: # 自动抽取会在 IDE 构建时膨胀文件(2026-10-04 #81 验收时实测)。 LOCALIZED_STRING_SWIFTUI_SUPPORT: NO CODE_SIGN_STYLE: Manual - DEVELOPMENT_TEAM: S878RH3PA8 ENABLE_HARDENED_RUNTIME: YES configs: Release: # 发布基线:签名 entitlements 必须只来自项目 entitlements 文件(保持最小)。 CODE_SIGN_INJECT_BASE_ENTITLEMENTS: NO +# 签名身份注入:证书与 Team 都不入库(开源仓库;写死会让 fork 与贡献者的构建去 +# 找维护者的证书)。Debug 基线是 ad-hoc,无证书即可构建并跑单测;Release 基线是 +# Developer ID,发布产物必须真签名。两者的本机 Team / 身份都来自 gitignore 的 +# Configs/Signing.local.xcconfig,见 Configs/Signing-Debug.xcconfig 与 +# Configs/Signing-Release.xcconfig。 +# 所有 target 一律不设置 CODE_SIGN_IDENTITY:pbxproj 构建设置优先于 project +# xcconfig,写在 target 上会静默盖掉两个基线(实测)。该约束由 +# Tests/Architecture/SigningConfigurationTests.swift 守卫。 +configFiles: + Debug: Configs/Signing-Debug.xcconfig + Release: Configs/Signing-Release.xcconfig targets: ShadowsocksX-NG2: type: application @@ -110,7 +120,6 @@ targets: # 共享 Products 目录,重名会互相覆盖;app 可执行自身不 import 任何 # 消费方,改名零影响。 PRODUCT_MODULE_NAME: ShadowsocksX_NG2App - CODE_SIGN_IDENTITY: "Developer ID Application" ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon MARKETING_VERSION: "2.0.0" # 必须 > 旧版 1.x 已发布构建的 CFBundleVersion(始终为 1),保证 build 号严格递增。 @@ -194,7 +203,6 @@ targets: base: PRODUCT_NAME: ShadowsocksX-NG2Agent PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2.agent - CODE_SIGN_IDENTITY: "Developer ID Application" ShadowsocksX-NG2SystemProxyHelper: type: tool platform: macOS @@ -213,7 +221,6 @@ targets: base: PRODUCT_NAME: ShadowsocksX-NG2SystemProxyHelper PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2.systemproxy.helper - CODE_SIGN_IDENTITY: "Developer ID Application" ShadowsocksX-NG2Tests: type: bundle.unit-test platform: macOS @@ -229,9 +236,9 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2.Tests - # 必须与宿主 app 同 Team 签名:Hardened Runtime 的 library validation 拒绝 - # 加载不同 Team ID 的非平台库,ad-hoc("-")签名的 xctest 包无法注入。 - CODE_SIGN_IDENTITY: "Apple Development" + # 身份随 config 基线走(Debug ad-hoc / Release DevID,见 configFiles): + # 去宿主化(ADR 0021)后 xctest 包不再注入宿主 app,ad-hoc 可加载执行 + # (实测),因此测试不再需要证书,CI 无需签名密钥。 info: path: Tests/Info.plist properties: {} @@ -250,8 +257,7 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2.SmokeTests - # 同 Team 签名理由同 ShadowsocksX-NG2Tests。 - CODE_SIGN_IDENTITY: "Apple Development" + # 身份理由同 ShadowsocksX-NG2Tests:随 config 基线,Debug 无证书可跑。 info: path: Tests/Smoke/Info.plist properties: {}