Repository navigation
perf(mobile): sign iOS events with native secp256k1 #8446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
46a98f7
8e60b76
05ceb11
58f54a2
4153655
7aa63ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| #include "include/CSecp256k1.h" | ||
| #include <Security/Security.h> | ||
| #include <pthread.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
|
|
||
| struct buzz_signer { secp256k1_keypair pair; }; | ||
| static pthread_once_t context_once = PTHREAD_ONCE_INIT; | ||
| static secp256k1_context *context; | ||
| static int context_status = 3; | ||
|
|
||
| static void wipe(void *buffer, size_t count) { | ||
| volatile unsigned char *bytes = buffer; | ||
| while (count--) *bytes++ = 0; | ||
| } | ||
|
|
||
| /* Published once, then immutable. libsecp permits concurrent const operations. */ | ||
| static void initialize_context(void) { | ||
| unsigned char seed[32]; | ||
| if (SecRandomCopyBytes(kSecRandomDefault, sizeof(seed), seed) != errSecSuccess) { | ||
| wipe(seed, sizeof(seed)); | ||
| context_status = 2; | ||
| return; | ||
| } | ||
| context = secp256k1_context_create(SECP256K1_CONTEXT_NONE); | ||
| if (context && secp256k1_context_randomize(context, seed)) context_status = 0; | ||
| wipe(seed, sizeof(seed)); | ||
| } | ||
|
|
||
| __attribute__((visibility("default"), used)) | ||
| int buzz_signer_create(const unsigned char secret[32], unsigned char public_key[32], buzz_signer **out) { | ||
| if (!secret || !public_key || !out) return 3; | ||
| *out = NULL; | ||
| pthread_once(&context_once, initialize_context); | ||
| if (context_status) return context_status; | ||
| buzz_signer *signer = calloc(1, sizeof(*signer)); | ||
| if (!signer) return 3; | ||
| if (!secp256k1_keypair_create(context, &signer->pair, secret)) { | ||
| buzz_signer_destroy(signer); | ||
| return 1; | ||
| } | ||
| secp256k1_xonly_pubkey key; | ||
| if (!secp256k1_keypair_xonly_pub(context, &key, NULL, &signer->pair) || | ||
| !secp256k1_xonly_pubkey_serialize(context, public_key, &key)) { | ||
| buzz_signer_destroy(signer); | ||
| return 3; | ||
| } | ||
| *out = signer; | ||
| return 0; | ||
| } | ||
|
|
||
| __attribute__((visibility("default"), used)) | ||
| int buzz_signer_sign(const buzz_signer *signer, const unsigned char message[32], unsigned char signature[64]) { | ||
| if (!signer || !message || !signature) return 3; | ||
| unsigned char aux[32]; | ||
| if (SecRandomCopyBytes(kSecRandomDefault, sizeof(aux), aux) != errSecSuccess) { | ||
| wipe(aux, sizeof(aux)); | ||
| return 2; | ||
| } | ||
| secp256k1_xonly_pubkey key; | ||
| int ok = secp256k1_schnorrsig_sign32(context, signature, message, &signer->pair, aux) && | ||
| secp256k1_keypair_xonly_pub(context, &key, NULL, &signer->pair) && | ||
| secp256k1_schnorrsig_verify(context, signature, message, 32, &key); | ||
| wipe(aux, sizeof(aux)); | ||
| if (!ok) { wipe(signature, 64); return 3; } | ||
| return 0; | ||
| } | ||
|
|
||
| __attribute__((visibility("default"), used)) | ||
| void buzz_signer_destroy(buzz_signer *signer) { | ||
| if (!signer) return; | ||
| wipe(signer, sizeof(*signer)); | ||
| free(signer); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,10 @@ | ||
| #include "../vendor/include/secp256k1.h" | ||
| #include "../vendor/include/secp256k1_extrakeys.h" | ||
| #include "../vendor/include/secp256k1_schnorrsig.h" | ||
|
|
||
| /* Short-lived signing handles own a keypair, never an account/session cache. */ | ||
| typedef struct buzz_signer buzz_signer; | ||
| /* 0 success, 1 invalid key, 2 random source, 3 allocation/operation failure. */ | ||
| int buzz_signer_create(const unsigned char secret[32], unsigned char public_key[32], buzz_signer **out); | ||
| int buzz_signer_sign(const buzz_signer *signer, const unsigned char message[32], unsigned char signature[64]); | ||
| void buzz_signer_destroy(buzz_signer *signer); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import CSecp256k1 | ||
| import Foundation | ||
| import XCTest | ||
|
|
||
| @testable import BuzzPushKit | ||
|
|
||
| final class NativeEventSignerTests: XCTestCase { | ||
| func testFixtureKeysAndCrossImplementationSignatures() throws { | ||
| let url = try XCTUnwrap(Bundle.module.url(forResource: "bip340", withExtension: "csv")) | ||
| for row in try String(contentsOf: url, encoding: .utf8).split(whereSeparator: \.isNewline).dropFirst() { | ||
| let fields = row.split(separator: ",", omittingEmptySubsequences: false).map(String.init) | ||
| guard !fields[1].isEmpty else { continue } | ||
| let secret = try XCTUnwrap(VerifiedNostrEvent.hexBytes(fields[1])) | ||
| let message = [UInt8](repeating: 42, count: 32) | ||
| var publicKey = [UInt8](repeating: 0, count: 32) | ||
| var handle: OpaquePointer? | ||
| XCTAssertEqual(buzz_signer_create(secret, &publicKey, &handle), 0) | ||
| let signer = try XCTUnwrap(handle) | ||
| defer { buzz_signer_destroy(signer) } | ||
| XCTAssertEqual(publicKey, try XCTUnwrap(VerifiedNostrEvent.hexBytes(fields[2]))) | ||
| var signature = [UInt8](repeating: 0, count: 64) | ||
| XCTAssertEqual(buzz_signer_sign(signer, message, &signature), 0) | ||
| XCTAssertTrue(NostrSchnorr.verify(signature: signature, message: message, publicKey: publicKey)) | ||
| } | ||
| } | ||
|
|
||
| func testInvalidScalarDoesNotReturnHandle() { | ||
| for secret in [[UInt8](repeating: 0, count: 32), [UInt8](repeating: 255, count: 32)] { | ||
| var publicKey = [UInt8](repeating: 0, count: 32) | ||
| var handle: OpaquePointer? | ||
| XCTAssertEqual(buzz_signer_create(secret, &publicKey, &handle), 1) | ||
| XCTAssertNil(handle) | ||
| } | ||
| buzz_signer_destroy(nil) | ||
| } | ||
|
|
||
| func testConcurrentIndependentKeypairs() { | ||
| DispatchQueue.concurrentPerform(iterations: 64) { index in | ||
| let secret = [UInt8](repeating: 0, count: 31) + [UInt8(index + 1)] | ||
| var publicKey = [UInt8](repeating: 0, count: 32) | ||
| var handle: OpaquePointer? | ||
| XCTAssertEqual(buzz_signer_create(secret, &publicKey, &handle), 0) | ||
| defer { buzz_signer_destroy(handle) } | ||
| let message = [UInt8](repeating: UInt8(index), count: 32) | ||
| var signature = [UInt8](repeating: 0, count: 64) | ||
| XCTAssertEqual(buzz_signer_sign(handle, message, &signature), 0) | ||
| XCTAssertTrue(NostrSchnorr.verify(signature: signature, message: message, publicKey: publicKey)) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,3 +17,9 @@ BUZZ_KEYCHAIN_ACCESS_GROUP = $(BUNDLE_IDENTIFIER) | |
| BUZZ_IOS_PUSH_ENVIRONMENT = production | ||
| BUZZ_APP_ATTEST_ENVIRONMENT = production | ||
| #include? "AppOverrides.xcconfig" | ||
|
|
||
| // Keep the Dart FFI entry points visible in the app executable. | ||
| OTHER_LDFLAGS = $(inherited) -Wl,-u,_buzz_signer_create -Wl,-u,_buzz_signer_sign -Wl,-u,_buzz_signer_destroy | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 [P1] Preserve the FFI exports through archive stripping These Set
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Addressed in 7aa63ef. Runner Release now uses non-global stripping, and CI inspects the postprocessed archive executable. A local unsigned device Release archive succeeded; its log confirms strip -x and nm -gU confirms all three buzz_signer exports remain. This validates archive retention without claiming installed physical-device execution. |
||
|
|
||
| // Preserve runtime-looked-up FFI exports during archive/install stripping. | ||
| STRIP_STYLE = non-global | ||
Uh oh!
There was an error while loading. Please reload this page.