From 9525c1c8f2ae968b6dc7b1123890ab807bedfdbb Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Thu, 15 Oct 2026 17:55:33 -0700 Subject: [PATCH] fix(ios): support Xcode 27 with pinned secp plugin approval Signed-off-by: Tom Brow --- .github/workflows/_ci-clients.yml | 2 + mobile/ios/BuzzPushKit/Package.swift | 2 +- .../BuzzNotificationAvatarLoader.swift | 2 +- .../BuzzPushKit/BuzzNotificationCache.swift | 2 +- .../Sources/BuzzPushKit/NostrHTTPAuth.swift | 2 +- mobile/ios/Podfile | 7 ++ mobile/ios/Podfile.lock | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- mobile/ios/trust_swift_plugins.rb | 41 +++++++++++ scripts/test-swift-plugin-trust.rb | 68 +++++++++++++++++++ 10 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 mobile/ios/trust_swift_plugins.rb create mode 100644 scripts/test-swift-plugin-trust.rb diff --git a/.github/workflows/_ci-clients.yml b/.github/workflows/_ci-clients.yml index bb62f49e9b..c4f838b7a2 100644 --- a/.github/workflows/_ci-clients.yml +++ b/.github/workflows/_ci-clients.yml @@ -186,6 +186,8 @@ jobs: - name: Build if: matrix.lane == 'native' run: swift build --package-path mobile/ios/BuzzPushKit + - name: Test pinned Swift plugin approval + run: ruby scripts/test-swift-plugin-trust.rb - name: Build release if: matrix.lane == 'native' run: swift build -c release --package-path mobile/ios/BuzzPushKit diff --git a/mobile/ios/BuzzPushKit/Package.swift b/mobile/ios/BuzzPushKit/Package.swift index 8426b076e8..3787d0270d 100644 --- a/mobile/ios/BuzzPushKit/Package.swift +++ b/mobile/ios/BuzzPushKit/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library(name: "BuzzPushKit", targets: ["BuzzPushKit"]) ], dependencies: [ - .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1.git", exact: "0.21.1"), + .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1.git", exact: "0.23.2"), .package(url: "https://github.com/swhitty/SwiftDraw.git", exact: "0.29.0") ], targets: [ diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationAvatarLoader.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationAvatarLoader.swift index 1fa6436374..483bdac510 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationAvatarLoader.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationAvatarLoader.swift @@ -1,7 +1,7 @@ import CryptoKit import Foundation import ImageIO -import P256K +@_implementationOnly import P256K import UniformTypeIdentifiers /// Fetches an avatar with the extension's private HTTP cache and bounded native decoding. diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationCache.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationCache.swift index cffbb79aa7..4d072d7dca 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationCache.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzNotificationCache.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import P256K +@_implementationOnly import P256K /// Account and relay identity for one local sign-in's notification cache. public struct BuzzNotificationCacheScope: Codable, Equatable, Sendable { diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/NostrHTTPAuth.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/NostrHTTPAuth.swift index f8b18fdbdd..a605ac8b1e 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/NostrHTTPAuth.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/NostrHTTPAuth.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import P256K +@_implementationOnly import P256K public enum NostrHTTPAuthError: Error, Equatable { case invalidHex diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index ff3742742e..ed099eb136 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -36,6 +36,13 @@ target 'Runner' do end post_install do |installer| + require_relative 'trust_swift_plugins' + BuzzSwiftPluginTrust.install( + [ + File.join(__dir__, 'Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved'), + ], + File.expand_path('~/Library/org.swift.swiftpm/security'), + ) installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 0384f7355d..9ab7c8a7f2 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -227,6 +227,6 @@ SPEC CHECKSUMS: url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a -PODFILE CHECKSUM: ed10001903303a0ae660957e1d727012a3136046 +PODFILE CHECKSUM: 36153deab10e24b51e0a03a6491abddbc3bcfc8b COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index af6b7429b6..8b80e10725 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "62fc6df0fb0a452720ac04b0bfacf7c5dbe509dd06046cc6c44487a812f0502e", + "originHash" : "3a973186bec68a8d259cc720f0f2f4ba8fe661a4c753a8152a18075110bb26d4", "pins" : [ { "identity" : "swift-secp256k1", "kind" : "remoteSourceControl", "location" : "https://github.com/21-DOT-DEV/swift-secp256k1.git", "state" : { - "revision" : "8c62aba8a3011c9bcea232e5ee007fb0b34a15e2", - "version" : "0.21.1" + "revision" : "e70a10e036a55fffea31568f0af92d69b6d449cd", + "version" : "0.23.2" } }, { diff --git a/mobile/ios/trust_swift_plugins.rb b/mobile/ios/trust_swift_plugins.rb new file mode 100644 index 0000000000..d87992fde5 --- /dev/null +++ b/mobile/ios/trust_swift_plugins.rb @@ -0,0 +1,41 @@ +require 'json' +require 'fileutils' +require 'tempfile' + +# Xcode records approval per plugin and package revision. Never disable plugin +# validation globally: a dependency update must explicitly update this approval. +module BuzzSwiftPluginTrust + APPROVAL = { + 'fingerprint' => 'e70a10e036a55fffea31568f0af92d69b6d449cd', + 'packageIdentity' => 'swift-secp256k1', + 'targetName' => 'SharedSourcesPlugin', + }.freeze + PACKAGE_URL = 'https://github.com/21-DOT-DEV/swift-secp256k1.git'.freeze + + def self.install(lockfiles, directory) + lockfiles.each do |lockfile| + pins = JSON.parse(File.read(lockfile)).fetch('pins') + pin = pins.find { |entry| entry['identity'] == APPROVAL.fetch('packageIdentity') } + unless pin && pin['location'] == PACKAGE_URL && + pin.fetch('state').fetch('revision') == APPROVAL.fetch('fingerprint') + raise "Unapproved swift-secp256k1 revision in #{lockfile}; review the plugin before updating its approval" + end + end + + FileUtils.mkdir_p(directory) + path = File.join(directory, 'plugins.json') + File.open(File.join(directory, 'buzz-plugins.lock'), File::RDWR | File::CREAT, 0o600) do |lock| + lock.flock(File::LOCK_EX) + approvals = File.exist?(path) ? JSON.parse(File.read(path)) : [] + raise "Expected an array in #{path}" unless approvals.is_a?(Array) + return if approvals.include?(APPROVAL) + + # Preserve approvals for other projects and publish a complete JSON file. + Tempfile.create(['plugins', '.json'], directory) do |file| + file.write(JSON.pretty_generate(approvals + [APPROVAL]) + "\n") + file.flush + File.rename(file.path, path) + end + end + end +end diff --git a/scripts/test-swift-plugin-trust.rb b/scripts/test-swift-plugin-trust.rb new file mode 100644 index 0000000000..5788cffc0b --- /dev/null +++ b/scripts/test-swift-plugin-trust.rb @@ -0,0 +1,68 @@ +require 'minitest/autorun' +require 'tmpdir' +require_relative '../mobile/ios/trust_swift_plugins' + +class SwiftPluginTrustTest < Minitest::Test + def setup + @root = Dir.mktmpdir + @directory = File.join(@root, 'security') + @lockfile = File.join(@root, 'Package.resolved') + @pin = { + 'identity' => 'swift-secp256k1', + 'location' => BuzzSwiftPluginTrust::PACKAGE_URL, + 'state' => { 'revision' => BuzzSwiftPluginTrust::APPROVAL.fetch('fingerprint') }, + } + write_pin + end + + def teardown + FileUtils.remove_entry(@root) + end + + def write_pin + File.write(@lockfile, JSON.generate('pins' => [@pin])) + end + + def install + BuzzSwiftPluginTrust.install([@lockfile], @directory) + end + + def test_approves_only_the_pinned_plugin_and_is_idempotent + install + path = File.join(@directory, 'plugins.json') + assert_equal [BuzzSwiftPluginTrust::APPROVAL], JSON.parse(File.read(path)) + before = File.read(path) + install + assert_equal before, File.read(path) + end + + def test_preserves_other_project_approvals + FileUtils.mkdir_p(@directory) + other = { 'fingerprint' => 'other', 'packageIdentity' => 'other', 'targetName' => 'OtherPlugin' } + File.write(File.join(@directory, 'plugins.json'), JSON.generate([other])) + install + assert_equal [other, BuzzSwiftPluginTrust::APPROVAL], JSON.parse(File.read(File.join(@directory, 'plugins.json'))) + end + + def test_rejects_changed_revision_before_writing_trust + @pin['state']['revision'] = 'unreviewed' + write_pin + assert_raises(RuntimeError) { install } + refute File.exist?(@directory) + end + + def test_rejects_another_repository_with_the_same_package_name + @pin['location'] = 'https://example.com/swift-secp256k1.git' + write_pin + assert_raises(RuntimeError) { install } + refute File.exist?(@directory) + end + + def test_does_not_overwrite_malformed_existing_trust + FileUtils.mkdir_p(@directory) + path = File.join(@directory, 'plugins.json') + File.write(path, '{broken') + assert_raises(JSON::ParserError) { install } + assert_equal '{broken', File.read(path) + end +end