From fc2da0dd5cbbf1691e1b0d6d7d9652e0d3aa1d72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:04:44 +0000 Subject: [PATCH 1/5] chore: bump github.com/21-dot-dev/swift-secp256k1 Bumps the swift group with 1 update in the / directory: [github.com/21-dot-dev/swift-secp256k1](https://github.com/21-DOT-DEV/swift-secp256k1). Updates `github.com/21-dot-dev/swift-secp256k1` from 0.17.0 to 0.24.0 - [Release notes](https://github.com/21-DOT-DEV/swift-secp256k1/releases) - [Changelog](https://github.com/21-DOT-DEV/swift-secp256k1/blob/main/CHANGELOG.md) - [Commits](https://github.com/21-DOT-DEV/swift-secp256k1/compare/0.17.0...bcd6dd8e615ce243eab7097f213c8c4b39305a2e) --- updated-dependencies: - dependency-name: github.com/21-dot-dev/swift-secp256k1 dependency-version: 0.24.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: swift ... Signed-off-by: dependabot[bot] --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index f83b181fa..5de28ed39 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1240,7 +1240,7 @@ repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1"; requirement = { kind = exactVersion; - version = 0.17.0; + version = 0.24.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4a921d6b3..d0490865b 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/21-DOT-DEV/swift-secp256k1", "state" : { - "revision" : "9683e8e311c76d8114cd308b697dad2f9fc58fed", - "version" : "0.17.0" + "revision" : "bcd6dd8e615ce243eab7097f213c8c4b39305a2e", + "version" : "0.24.0" } }, { From 020a50f2a309852a72192ab3f0f5705737003aa0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 5 Nov 2026 16:17:59 -0300 Subject: [PATCH 2/5] fix: migrate to P256K and libsecp256k1 products for swift-secp256k1 0.24.0 0.24.0 renames the secp256k1 product/module to P256K and stops re-exporting the C API, which now lives in the libsecp256k1 product. sharedSecretFromKeyAgreement no longer throws. Add fixed-vector tests for Crypto.sign and Crypto.getPublicKey. Co-Authored-By: Claude Opus 5.5 --- Bitkit.xcodeproj/project.pbxproj | 54 +++++++++++++------ Bitkit/Services/WatchOnlyAccountService.swift | 4 +- Bitkit/Utilities/Crypto.swift | 9 ++-- BitkitTests/CryptoTests.swift | 23 ++++++++ 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 5de28ed39..c688f66c5 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -22,9 +22,12 @@ 968FE1402DFB016B0053CD7F /* LDKNode in Frameworks */ = {isa = PBXBuildFile; productRef = 968FE13F2DFB016B0053CD7F /* LDKNode */; }; 96DEA03A2DE8BBA1009932BF /* BitkitCore in Frameworks */ = {isa = PBXBuildFile; productRef = 96DEA0392DE8BBA1009932BF /* BitkitCore */; }; 96DEA03C2DE8BBAB009932BF /* BitkitCore in Frameworks */ = {isa = PBXBuildFile; productRef = 96DEA03B2DE8BBAB009932BF /* BitkitCore */; }; - 96E493A42C942FD1000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A32C942FD1000E8BC2 /* secp256k1 */; }; - 96E493A62C94317D000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A52C94317D000E8BC2 /* secp256k1 */; }; - 96E493A82C943184000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A72C943184000E8BC2 /* secp256k1 */; }; + 96E493A42C942FD1000E8BC2 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A32C942FD1000E8BC2 /* P256K */; }; + 96E493B22C942FD1000E8BC2 /* libsecp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493B12C942FD1000E8BC2 /* libsecp256k1 */; }; + 96E493A62C94317D000E8BC2 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A52C94317D000E8BC2 /* P256K */; }; + 96E493B42C94317D000E8BC2 /* libsecp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493B32C94317D000E8BC2 /* libsecp256k1 */; }; + 96E493A82C943184000E8BC2 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A72C943184000E8BC2 /* P256K */; }; + 96E493B62C943184000E8BC2 /* libsecp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493B52C943184000E8BC2 /* libsecp256k1 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -252,7 +255,8 @@ 3D7626104C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */, 968FE1402DFB016B0053CD7F /* LDKNode in Frameworks */, 96DEA03C2DE8BBAB009932BF /* BitkitCore in Frameworks */, - 96E493A82C943184000E8BC2 /* secp256k1 in Frameworks */, + 96E493A82C943184000E8BC2 /* P256K in Frameworks */, + 96E493B62C943184000E8BC2 /* libsecp256k1 in Frameworks */, 18D65E022EB964BD00252335 /* VssRustClientFfi in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -265,7 +269,8 @@ 3D76260F4C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */, 968FDF162DFAFE230053CD7F /* LDKNode in Frameworks */, 18D65E002EB964B500252335 /* VssRustClientFfi in Frameworks */, - 96E493A42C942FD1000E8BC2 /* secp256k1 in Frameworks */, + 96E493A42C942FD1000E8BC2 /* P256K in Frameworks */, + 96E493B22C942FD1000E8BC2 /* libsecp256k1 in Frameworks */, 96DEA03A2DE8BBA1009932BF /* BitkitCore in Frameworks */, 4AAB08CA2E1FE77600BA63DF /* Lottie in Frameworks */, ); @@ -275,7 +280,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 96E493A62C94317D000E8BC2 /* secp256k1 in Frameworks */, + 96E493A62C94317D000E8BC2 /* P256K in Frameworks */, + 96E493B42C94317D000E8BC2 /* libsecp256k1 in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -365,7 +371,8 @@ ); name = BitkitNotification; packageProductDependencies = ( - 96E493A72C943184000E8BC2 /* secp256k1 */, + 96E493A72C943184000E8BC2 /* P256K */, + 96E493B52C943184000E8BC2 /* libsecp256k1 */, 96DEA03B2DE8BBAB009932BF /* BitkitCore */, 968FE13F2DFB016B0053CD7F /* LDKNode */, 18D65E012EB964BD00252335 /* VssRustClientFfi */, @@ -397,7 +404,8 @@ name = Bitkit; packageProductDependencies = ( 9613018B2C5022D700878183 /* LDKNode */, - 96E493A32C942FD1000E8BC2 /* secp256k1 */, + 96E493A32C942FD1000E8BC2 /* P256K */, + 96E493B12C942FD1000E8BC2 /* libsecp256k1 */, 96DEA0392DE8BBA1009932BF /* BitkitCore */, 4AAB08C92E1FE77600BA63DF /* Lottie */, 18D65DFF2EB964B500252335 /* VssRustClientFfi */, @@ -425,7 +433,8 @@ ); name = BitkitTests; packageProductDependencies = ( - 96E493A52C94317D000E8BC2 /* secp256k1 */, + 96E493A52C94317D000E8BC2 /* P256K */, + 96E493B32C94317D000E8BC2 /* libsecp256k1 */, ); productName = BitkitTests; productReference = 96FE1F722C2DE6AC006D0C8B /* BitkitTests.xctest */; @@ -1286,20 +1295,35 @@ package = 96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */; productName = BitkitCore; }; - 96E493A32C942FD1000E8BC2 /* secp256k1 */ = { + 96E493A32C942FD1000E8BC2 /* P256K */ = { isa = XCSwiftPackageProductDependency; package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; - productName = secp256k1; + productName = P256K; }; - 96E493A52C94317D000E8BC2 /* secp256k1 */ = { + 96E493B12C942FD1000E8BC2 /* libsecp256k1 */ = { isa = XCSwiftPackageProductDependency; package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; - productName = secp256k1; + productName = libsecp256k1; }; - 96E493A72C943184000E8BC2 /* secp256k1 */ = { + 96E493A52C94317D000E8BC2 /* P256K */ = { isa = XCSwiftPackageProductDependency; package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; - productName = secp256k1; + productName = P256K; + }; + 96E493B32C94317D000E8BC2 /* libsecp256k1 */ = { + isa = XCSwiftPackageProductDependency; + package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; + productName = libsecp256k1; + }; + 96E493A72C943184000E8BC2 /* P256K */ = { + isa = XCSwiftPackageProductDependency; + package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; + productName = P256K; + }; + 96E493B52C943184000E8BC2 /* libsecp256k1 */ = { + isa = XCSwiftPackageProductDependency; + package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; + productName = libsecp256k1; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Bitkit/Services/WatchOnlyAccountService.swift b/Bitkit/Services/WatchOnlyAccountService.swift index 9f9a04b9e..e99eb1536 100644 --- a/Bitkit/Services/WatchOnlyAccountService.swift +++ b/Bitkit/Services/WatchOnlyAccountService.swift @@ -3,7 +3,7 @@ import Combine import CryptoKit import Foundation import LDKNode -import secp256k1 +import P256K enum WatchOnlyAccountSetupState: String, Codable { case pendingDelivery @@ -994,6 +994,6 @@ enum WatchOnlyAccountClaimCodec { } private static func isValidCompressedPublicKey(_ key: Data) -> Bool { - (try? secp256k1.KeyAgreement.PublicKey(dataRepresentation: key, format: .compressed)) != nil + (try? P256K.KeyAgreement.PublicKey(dataRepresentation: key, format: .compressed)) != nil } } diff --git a/Bitkit/Utilities/Crypto.swift b/Bitkit/Utilities/Crypto.swift index 952af5e46..367e21496 100644 --- a/Bitkit/Utilities/Crypto.swift +++ b/Bitkit/Utilities/Crypto.swift @@ -1,6 +1,7 @@ import CryptoKit import Foundation -import secp256k1 +import libsecp256k1 +import P256K class Crypto { struct KeyPair { @@ -23,11 +24,11 @@ class Crypto { } static func generateSharedSecret(privateKey: Data, nodePubkey: String, derivationName: String? = nil) throws -> Data { - let privateKey = try secp256k1.KeyAgreement.PrivateKey(dataRepresentation: privateKey.bytes) + let privateKey = try P256K.KeyAgreement.PrivateKey(dataRepresentation: privateKey.bytes) - let publicKey = try secp256k1.KeyAgreement.PublicKey(dataRepresentation: nodePubkey.hexaBytes) + let publicKey = try P256K.KeyAgreement.PublicKey(dataRepresentation: nodePubkey.hexaBytes) - let baseSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey, format: .compressed).bytes + let baseSecret = privateKey.sharedSecretFromKeyAgreement(with: publicKey, format: .compressed).bytes if let derivationName { let bytes = stringToBytes(derivationName) diff --git a/BitkitTests/CryptoTests.swift b/BitkitTests/CryptoTests.swift index ef9b8e6cd..cb135bb98 100644 --- a/BitkitTests/CryptoTests.swift +++ b/BitkitTests/CryptoTests.swift @@ -90,4 +90,27 @@ final class CryptoTests: XCTestCase { XCTAssertEqual(String(data: value, encoding: .utf8), decryptedPayload) } + + /// Expected values computed with an independent secp256k1 implementation (RFC 6979 nonces, low-S). + func testGetPublicKeyMatchesReferenceVector() throws { + let privateKey = "cc74b1a4fdcd35916c766d3318c5a93b7e33a36ebeff0463128bf284975c2680".hexaData + let publicKey = try Crypto.getPublicKey(privateKey: privateKey) + XCTAssertEqual(publicKey.hex, "02f7c231fe139475b8193194b42043ae546768167e5cf836f5a4063f5d448a127f") + } + + func testSignMatchesReferenceVector() throws { + let privateKey = "cc74b1a4fdcd35916c766d3318c5a93b7e33a36ebeff0463128bf284975c2680".hexaData + let signature = try Crypto.sign(message: "hello bitkit", privateKey: privateKey) + XCTAssertEqual( + signature, + "204b84a4bfc77965c2ea561930829fca5a05610027d3591268e13f4e0cbb65aea2176f20bd78ca453a23214323ac1a6df88045f71b40f2e9dadab360743606a655" + ) + } + + func testGenerateKeyPairPublicKeyMatchesPrivateKey() throws { + let keyPair = try Crypto.generateKeyPair() + XCTAssertEqual(keyPair.privateKey.count, 32) + XCTAssertEqual(keyPair.publicKey.count, 33) + XCTAssertEqual(try Crypto.getPublicKey(privateKey: keyPair.privateKey), keyPair.publicKey) + } } From b1792d867ab6bf2ccc3bda9510801688609d442f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 5 Nov 2026 16:29:16 -0300 Subject: [PATCH 3/5] fix: validate key lengths and throw on ECDH failure in Crypto generateSharedSecret now calls secp256k1_ecdh directly with a compressed-point hash, so a failure throws instead of hitting the P256K wrapper's fatalError. Output is unchanged (Blocktank vector). Local shared-secret buffers are zeroized. sign and getPublicKey reject private keys that are not 32 bytes; shorter Data was read past its end by the C calls. Add regression tests for key length, out-of-range keys and invalid public keys. Co-Authored-By: Claude Opus 5.5 --- Bitkit/Utilities/Crypto.swift | 68 +++++++++++++++++++++++++++--- BitkitTests/CryptoTests.swift | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/Bitkit/Utilities/Crypto.swift b/Bitkit/Utilities/Crypto.swift index 367e21496..4195de3d9 100644 --- a/Bitkit/Utilities/Crypto.swift +++ b/Bitkit/Utilities/Crypto.swift @@ -1,9 +1,19 @@ import CryptoKit import Foundation import libsecp256k1 -import P256K + +/// ECDH output: the shared point serialized as a 33-byte compressed key (no hashing). +private let compressedPointHash: secp256k1_ecdh_hash_function = { output, x32, y32, _ in + guard let output, let x32, let y32 else { return 0 } + output[0] = 0x02 | (y32[31] & 1) + (output + 1).update(from: x32, count: 32) + return 1 +} class Crypto { + private static let privateKeyLength = 32 + private static let compressedPublicKeyLength = 33 + struct KeyPair { let privateKey: Data let publicKey: Data @@ -24,15 +34,47 @@ class Crypto { } static func generateSharedSecret(privateKey: Data, nodePubkey: String, derivationName: String? = nil) throws -> Data { - let privateKey = try P256K.KeyAgreement.PrivateKey(dataRepresentation: privateKey.bytes) + guard privateKey.count == privateKeyLength else { + throw CryptoError.invalidPrivateKey + } + + let publicKeyBytes = nodePubkey.hexaBytes + guard publicKeyBytes.count == compressedPublicKeyLength else { + throw CryptoError.invalidPublicKey + } + + guard let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_NONE)) else { + throw CryptoError.contextCreationFailed + } + defer { secp256k1_context_destroy(context) } + + var publicKey = secp256k1_pubkey() + guard secp256k1_ec_pubkey_parse(context, &publicKey, publicKeyBytes, publicKeyBytes.count) == 1 else { + throw CryptoError.invalidPublicKey + } - let publicKey = try P256K.KeyAgreement.PublicKey(dataRepresentation: nodePubkey.hexaBytes) + var baseSecret = [UInt8](repeating: 0, count: compressedPublicKeyLength) + defer { zeroize(&baseSecret) } + let result = privateKey.withUnsafeBytes { keyPtr in + secp256k1_ecdh( + context, + &baseSecret, + &publicKey, + keyPtr.bindMemory(to: UInt8.self).baseAddress!, + compressedPointHash, + nil + ) + } - let baseSecret = privateKey.sharedSecretFromKeyAgreement(with: publicKey, format: .compressed).bytes + // 0 means the private key is zero or not below the curve order. + guard result == 1 else { + throw CryptoError.sharedSecretGenerationFailed + } if let derivationName { let bytes = stringToBytes(derivationName) - let merged = baseSecret + bytes + var merged = baseSecret + bytes + defer { zeroize(&merged) } return try Data(dsha256(merged)) } @@ -40,6 +82,13 @@ class Crypto { return Data(baseSecret) } + private static func zeroize(_ bytes: inout [UInt8]) { + bytes.withUnsafeMutableBytes { buffer in + guard let base = buffer.baseAddress else { return } + _ = memset_s(base, buffer.count, 0, buffer.count) + } + } + static func generateKeyPair() throws -> KeyPair { guard let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN)) else { throw CryptoError.contextCreationFailed @@ -103,6 +152,10 @@ class Crypto { /// Sign using Lightning Network message signing format static func sign(message: String, privateKey: Data) throws -> String { + guard privateKey.count == privateKeyLength else { + throw CryptoError.invalidPrivateKey + } + guard let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN)) else { throw CryptoError.contextCreationFailed } @@ -143,6 +196,10 @@ class Crypto { } static func getPublicKey(privateKey: Data) throws -> Data { + guard privateKey.count == privateKeyLength else { + throw CryptoError.invalidPrivateKey + } + guard let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN)) else { throw CryptoError.contextCreationFailed } @@ -175,6 +232,7 @@ class Crypto { case invalidDerivationName case contextCreationFailed case invalidPublicKey + case invalidPrivateKey case publicKeyCreationFailed case randomGenerationFailed case publicKeySerializationFailed diff --git a/BitkitTests/CryptoTests.swift b/BitkitTests/CryptoTests.swift index cb135bb98..3719e7c61 100644 --- a/BitkitTests/CryptoTests.swift +++ b/BitkitTests/CryptoTests.swift @@ -113,4 +113,82 @@ final class CryptoTests: XCTestCase { XCTAssertEqual(keyPair.publicKey.count, 33) XCTAssertEqual(try Crypto.getPublicKey(privateKey: keyPair.privateKey), keyPair.publicKey) } + + // MARK: - Input validation (regression: sign/getPublicKey read past the end of short keys) + + private static let validPrivateKey = "cc74b1a4fdcd35916c766d3318c5a93b7e33a36ebeff0463128bf284975c2680".hexaData + private static let validPublicKey = "031e9923e689a181a803486b7d8c0d4a5aad360edb70c8bb413a98458d91652213" + private static let curveOrder = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141".hexaData + + private func assertThrows( + _ expected: Crypto.CryptoError, + file: StaticString = #filePath, + line: UInt = #line, + _ body: () throws -> some Any + ) { + XCTAssertThrowsError(try body(), file: file, line: line) { error in + guard let error = error as? Crypto.CryptoError, error == expected else { + return XCTFail("Expected \(expected), got \(error)", file: file, line: line) + } + } + } + + func testSignRejectsPrivateKeyOfWrongLength() { + for length in [0, 1, 31, 33, 64] { + assertThrows(.invalidPrivateKey) { try Crypto.sign(message: "hello", privateKey: Data(repeating: 1, count: length)) } + } + } + + func testSignRejectsOutOfRangePrivateKey() { + assertThrows(.signingFailed) { try Crypto.sign(message: "hello", privateKey: Data(repeating: 0, count: 32)) } + assertThrows(.signingFailed) { try Crypto.sign(message: "hello", privateKey: Self.curveOrder) } + } + + func testGetPublicKeyRejectsPrivateKeyOfWrongLength() { + for length in [0, 1, 31, 33, 64] { + assertThrows(.invalidPrivateKey) { try Crypto.getPublicKey(privateKey: Data(repeating: 1, count: length)) } + } + } + + func testGetPublicKeyRejectsOutOfRangePrivateKey() { + assertThrows(.publicKeyCreationFailed) { try Crypto.getPublicKey(privateKey: Data(repeating: 0, count: 32)) } + assertThrows(.publicKeyCreationFailed) { try Crypto.getPublicKey(privateKey: Self.curveOrder) } + } + + func testGenerateSharedSecretRejectsPrivateKeyOfWrongLength() { + for length in [0, 1, 31, 33, 64] { + assertThrows(.invalidPrivateKey) { + try Crypto.generateSharedSecret(privateKey: Data(repeating: 1, count: length), nodePubkey: Self.validPublicKey) + } + } + } + + func testGenerateSharedSecretRejectsOutOfRangePrivateKey() { + assertThrows(.sharedSecretGenerationFailed) { + try Crypto.generateSharedSecret(privateKey: Data(repeating: 0, count: 32), nodePubkey: Self.validPublicKey) + } + assertThrows(.sharedSecretGenerationFailed) { + try Crypto.generateSharedSecret(privateKey: Self.curveOrder, nodePubkey: Self.validPublicKey) + } + } + + func testGenerateSharedSecretRejectsInvalidPublicKeys() throws { + let compressedHex = try Crypto.getPublicKey(privateKey: Self.validPrivateKey).hex + let xCoordinate = String(compressedHex.dropFirst(2)) + let offCurveX = String(repeating: "0", count: 63) + "5" // x = 5 has no y on secp256k1 + + let invalid = [ + "", + String(Self.validPublicKey.dropLast(2)), // 32 bytes + Self.validPublicKey + "00", // 34 bytes + "04" + xCoordinate + xCoordinate, // 65 bytes: only compressed keys are accepted + "04" + xCoordinate, // uncompressed prefix on 33 bytes + "05" + xCoordinate, // unknown prefix + "02" + offCurveX, + "02" + String(repeating: "f", count: 64), // x >= field prime + ] + for pubkey in invalid { + assertThrows(.invalidPublicKey) { try Crypto.generateSharedSecret(privateKey: Self.validPrivateKey, nodePubkey: pubkey) } + } + } } From 0af764cde1aa503abeff15dd15cff88a4153b2a9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 5 Nov 2026 18:07:56 -0300 Subject: [PATCH 4/5] ci: pin macos-26 runners, Xcode 26.6 and iOS 26.5 simulators swift-secp256k1 0.22.0+ gates its ECDH, recovery, Schnorr and MuSig C modules behind SwiftPM package traits, which Xcode supports from 26.4. On Xcode 16.4 and 26.2 those modules are compiled out and the app fails to link. Move every Xcode workflow to Xcode 26.6 on a pinned macos-26 runner (macos-15 tops out at 26.3) with an explicit iOS 26.5 iPhone 17 simulator, replacing macos-latest, OS=latest and newest-runtime lookups. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ai-device-tests.yml | 4 ++-- .github/workflows/e2e-staging.yml | 8 ++++---- .github/workflows/e2e-tests.yml | 8 ++++---- .github/workflows/e2e_migration.yml | 10 +++++----- .github/workflows/integration-tests.yml | 16 +++++++++------- .github/workflows/unit-tests.yml | 8 ++++---- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ai-device-tests.yml b/.github/workflows/ai-device-tests.yml index 18075d77b..b3ee75040 100644 --- a/.github/workflows/ai-device-tests.yml +++ b/.github/workflows/ai-device-tests.yml @@ -17,7 +17,7 @@ on: simulator_os: description: "iOS Simulator OS version" required: false - default: "26.2" + default: "26.5" env: TERM: xterm-256color @@ -49,7 +49,7 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.2" + xcode-version: "26.6" - name: Install xcbeautify run: | diff --git a/.github/workflows/e2e-staging.yml b/.github/workflows/e2e-staging.yml index 2c3fba80b..4a8dca2c4 100644 --- a/.github/workflows/e2e-staging.yml +++ b/.github/workflows/e2e-staging.yml @@ -20,7 +20,7 @@ env: FORCE_COLOR: 1 GIT_LFS_SKIP_SMUDGE: "1" SIMULATOR_NAME: "iPhone 17" - IOS_VERSION: "26.2" + IOS_VERSION: "26.5" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} @@ -28,7 +28,7 @@ concurrency: jobs: build-staging: - runs-on: macos-latest + runs-on: macos-26 steps: - name: Checkout @@ -37,7 +37,7 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.2" + xcode-version: "26.6" - name: System Information run: | @@ -109,7 +109,7 @@ jobs: e2e_branch_input: ${{ github.event.inputs.e2e_branch || 'default-feature-branch' }} e2e-tests-staging: - runs-on: macos-latest + runs-on: macos-26 timeout-minutes: 120 needs: [build-staging, e2e-branch] diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6877cd5fb..50be9cc4a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -92,7 +92,7 @@ jobs: checks: write actions: write if: github.event.pull_request.draft == false && needs.detect-changes.outputs.code == 'true' - runs-on: macos-latest + runs-on: macos-26 steps: - name: Checkout @@ -125,7 +125,7 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.2" + xcode-version: "26.6" - name: System Information run: | @@ -172,7 +172,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CHATWOOT_API: ${{ secrets.CHATWOOT_API }} SIMULATOR_NAME: "iPhone 17" - OS_VERSION: "26.2" + OS_VERSION: "26.5" run: | echo "=== Building iOS app (local) ===" echo "Using simulator: $SIMULATOR_NAME (iOS $OS_VERSION)" @@ -232,7 +232,7 @@ jobs: needs: [detect-changes, build-local, e2e-branch] env: SIMULATOR_NAME: "iPhone 17" - SIMULATOR_OS_VERSION: "26.2" + SIMULATOR_OS_VERSION: "26.5" strategy: fail-fast: false diff --git a/.github/workflows/e2e_migration.yml b/.github/workflows/e2e_migration.yml index 9153cbcc5..133f7f28c 100644 --- a/.github/workflows/e2e_migration.yml +++ b/.github/workflows/e2e_migration.yml @@ -30,7 +30,7 @@ env: FORCE_COLOR: 1 GIT_LFS_SKIP_SMUDGE: "1" SIMULATOR_NAME: "iPhone 17" - IOS_VERSION: "26.2" + IOS_VERSION: "26.5" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} @@ -39,7 +39,7 @@ concurrency: jobs: build: if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-'))) - runs-on: macos-latest + runs-on: macos-26 steps: - name: Checkout @@ -48,7 +48,7 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.2" + xcode-version: "26.6" - name: System Information run: | @@ -180,7 +180,7 @@ jobs: rn-tests: if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-'))) - runs-on: macos-latest + runs-on: macos-26 needs: [build, e2e-branch, migration-plan, prepare-wallets] strategy: @@ -356,7 +356,7 @@ jobs: native-tests: if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-'))) - runs-on: macos-latest + runs-on: macos-26 needs: [build, e2e-branch, migration-plan] strategy: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 02a79cc44..868029a64 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -46,7 +46,7 @@ jobs: cache-mode: read if: github.event.pull_request.draft == false name: Run Integration Tests - runs-on: macos-15 + runs-on: macos-26 timeout-minutes: 60 permissions: contents: read @@ -89,18 +89,20 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.4' + xcode-version: '26.6' # A fresh simulator takes over 10 minutes to boot on the runner. Booting it now lets that # overlap with resolving and building instead of stalling the test step. - name: Boot simulator + env: + SIMULATOR_NAME: "iPhone 17" + SIMULATOR_OS: "26.5" run: | udid=$(xcrun simctl list devices available -j | python3 -c ' - import json, sys - runtimes = json.load(sys.stdin)["devices"] - version = lambda runtime: [int(part) for part in runtime.rsplit(".", 1)[-1].split("-")[1:]] - ios = sorted((r for r in runtimes if ".iOS-" in r and any(d["name"] == "iPhone 16" for d in runtimes[r])), key=version) - print(next(d["udid"] for d in runtimes[ios[-1]] if d["name"] == "iPhone 16")) + import json, os, sys + runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + os.environ["SIMULATOR_OS"].replace(".", "-") + devices = json.load(sys.stdin)["devices"].get(runtime, []) + print(next(d["udid"] for d in devices if d["name"] == os.environ["SIMULATOR_NAME"])) ') echo "SIMULATOR_UDID=$udid" >> "$GITHUB_ENV" echo "DESTINATION=platform=iOS Simulator,id=$udid" >> "$GITHUB_ENV" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 055d68e24..44973cdd2 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -19,7 +19,7 @@ jobs: test: if: github.event.pull_request.draft == false name: Run Tests - runs-on: macos-15 + runs-on: macos-26 timeout-minutes: 60 steps: @@ -29,7 +29,7 @@ jobs: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.4' + xcode-version: '26.6' - name: Install xcbeautify run: | @@ -41,7 +41,7 @@ jobs: - name: Pre-start simulator run: | echo "⏱️ Starting simulator at $(date)" - xcrun simctl boot "iPhone 16" || true + xcrun simctl boot "iPhone 17" || true echo "✅ Simulator started at $(date)" - name: Run unit tests @@ -49,7 +49,7 @@ jobs: echo "⏱️ Starting unit tests at $(date)" set -o pipefail && xcodebuild test \ -scheme Bitkit \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ + -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' \ -clonedSourcePackagesDirPath "$SOURCE_PACKAGES" \ -disablePackageRepositoryCache \ -disableAutomaticPackageResolution \ From cc5118a73b980c33e45d4819d99d706c7e643b10 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 5 Nov 2026 18:41:03 -0300 Subject: [PATCH 5/5] ci: pin ubuntu-24.04 instead of ubuntu-latest ubuntu-latest currently resolves to 24.04; pin it so image upgrades happen deliberately. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/e2e-retarget.yml | 2 +- .github/workflows/e2e-staging.yml | 2 +- .github/workflows/e2e-tests.yml | 6 +++--- .github/workflows/e2e_migration.yml | 8 ++++---- .github/workflows/integration-tests.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/retarget-execution-cleanup.yml | 2 +- .github/workflows/validate-translations.yml | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 75e843baa..7b7f23139 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -20,7 +20,7 @@ jobs: # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: write diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 85e0b5406..b8536ec0c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,7 +10,7 @@ on: jobs: claude: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write # Allow creating branches/commits pull-requests: write # Allow pushing to PR branches diff --git a/.github/workflows/e2e-retarget.yml b/.github/workflows/e2e-retarget.yml index ea581e416..a1c3599a1 100644 --- a/.github/workflows/e2e-retarget.yml +++ b/.github/workflows/e2e-retarget.yml @@ -26,7 +26,7 @@ jobs: (github.event.changes.base != null && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: write diff --git a/.github/workflows/e2e-staging.yml b/.github/workflows/e2e-staging.yml index 4a8dca2c4..a1050d2e9 100644 --- a/.github/workflows/e2e-staging.yml +++ b/.github/workflows/e2e-staging.yml @@ -254,7 +254,7 @@ jobs: always() && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.post_to_slack == true)) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: [build-staging, e2e-branch, e2e-tests-staging] steps: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 50be9cc4a..e53032691 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -51,7 +51,7 @@ concurrency: jobs: detect-changes: if: github.event.pull_request.draft == false - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: code: ${{ (github.event_name == 'workflow_dispatch' && !inputs.pr) || steps.filter.outputs.code == 'true' }} steps: @@ -404,7 +404,7 @@ jobs: e2e-status: if: always() && github.event.pull_request.draft == false name: e2e-status - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: [detect-changes, e2e-tests-local] steps: - name: Verify change detection succeeded @@ -430,7 +430,7 @@ jobs: name: Report retarget E2E result if: always() && inputs.retarget_run != '' needs: [detect-changes, build-local, e2e-branch, e2e-tests-local, e2e-status] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: read diff --git a/.github/workflows/e2e_migration.yml b/.github/workflows/e2e_migration.yml index 133f7f28c..6df318e75 100644 --- a/.github/workflows/e2e_migration.yml +++ b/.github/workflows/e2e_migration.yml @@ -125,7 +125,7 @@ jobs: resolve-e2e-ref: if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-'))) needs: [e2e-branch] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: ref: ${{ steps.e2e-ref.outputs.ref }} steps: @@ -139,7 +139,7 @@ jobs: migration-plan: if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-'))) needs: [e2e-branch] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: version: ${{ steps.plan.outputs.version }} all: ${{ steps.plan.outputs.all }} @@ -510,7 +510,7 @@ jobs: migration-result: if: always() && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (startsWith(github.head_ref, 'release-') || startsWith(github.base_ref, 'release-')))) needs: [build, e2e-branch, resolve-e2e-ref, migration-plan, prepare-wallets, rn-tests, native-tests] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Require all migration jobs to succeed env: @@ -529,7 +529,7 @@ jobs: always() && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.post_to_slack == true)) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: [build, e2e-branch, resolve-e2e-ref, migration-plan, prepare-wallets, rn-tests, native-tests, migration-result] steps: - name: Resolve E2E tests ref diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 868029a64..f53411ab4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -178,7 +178,7 @@ jobs: name: Report retarget integration result if: always() && inputs.retarget_run != '' needs: [test] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: read diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 60f2423a9..9e3d3e8e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ env: jobs: swiftformat: name: SwiftFormat - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: diff --git a/.github/workflows/retarget-execution-cleanup.yml b/.github/workflows/retarget-execution-cleanup.yml index 8312f868c..b5ae2411d 100644 --- a/.github/workflows/retarget-execution-cleanup.yml +++ b/.github/workflows/retarget-execution-cleanup.yml @@ -15,7 +15,7 @@ permissions: jobs: cleanup: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 - uses: actions/github-script@v9 diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml index fbdc01325..6d4260ec5 100644 --- a/.github/workflows/validate-translations.yml +++ b/.github/workflows/validate-translations.yml @@ -9,7 +9,7 @@ on: jobs: validate: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7