fix(ios): support Xcode 27 with pinned secp plugin approval

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-15 17:55:33 -07:00
parent 93ffc37cbd
commit 9525c1c8f2
10 changed files with 126 additions and 8 deletions

View File

@@ -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

View File

@@ -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: [

View File

@@ -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.

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
import CryptoKit
import Foundation
import P256K
@_implementationOnly import P256K
public enum NostrHTTPAuthError: Error, Equatable {
case invalidHex

View File

@@ -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|

View File

@@ -227,6 +227,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
PODFILE CHECKSUM: ed10001903303a0ae660957e1d727012a3136046
PODFILE CHECKSUM: 36153deab10e24b51e0a03a6491abddbc3bcfc8b
COCOAPODS: 1.16.2

View File

@@ -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"
}
},
{

View File

@@ -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

View File

@@ -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