diff --git a/.swiftformat b/.swiftformat index 1450d37..47f4e72 100644 --- a/.swiftformat +++ b/.swiftformat @@ -6,7 +6,6 @@ --tabwidth 4 --stripunusedargs closure-only ---enable marktypes --disable redundantNilInit,redundantSelf,extensionAccessControl,simplifyGenericConstraints --lineaftermarks false --ifdef no-indent diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..64b81f0 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "bafc1bc23908f9330b2a4379eb8285e73a7e19c88dc8f8074a04329794fe01f5", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "branch" : "603.0.0-prerelease-2025-12-17", + "revision" : "9b6d2d09c474336c8a5477a669ca976e58586f75" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 78f2960..4a38327 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,7 @@ // swift-tools-version:6.2.1 // The swift-tools-version declares the minimum version of Swift required to build this package. +import CompilerPluginSupport import PackageDescription let development = false @@ -21,9 +22,30 @@ let package = Package( ] ), ], - dependencies: [], + dependencies: [ + .package( + url: "https://github.com/swiftlang/swift-syntax.git", + revision: "603.0.0-prerelease-2025-12-17" + ), + ], targets: [ - // Target `libsecp256k1` https://github.com/bitcoin-core/secp256k1 + // Macro target(s) + .target( + name: "K1Macros", + dependencies: [ + "K1MacrosImpl", + ] + ), + .macro( + name: "K1MacrosImpl", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + ], + path: "Sources/K1MacrosImpl" + ), .target( name: "secp256k1", exclude: [ @@ -54,6 +76,7 @@ let package = Package( "libsecp256k1/SECURITY.md", ], cSettings: [ + .headerSearchPath("libsecp256k1/include"), // Basic config values that are universal and require no dependencies. // https://github.com/bitcoin-core/secp256k1/blob/master/src/basic-config.h#L12-L13 .define("ECMULT_WINDOW_SIZE", to: "15"), @@ -65,12 +88,16 @@ let package = Package( .define("ENABLE_MODULE_RECOVERY"), .define("ENABLE_MODULE_SCHNORRSIG"), .define("ENABLE_MODULE_EXTRAKEYS"), + ], + swiftSettings: [ + .enableExperimentalFeature("SafeInteropWrappers"), ] ), .target( name: "K1", dependencies: [ "secp256k1", + "K1Macros", ], exclude: [ "K1/Keys/Keys.swift.gyb", @@ -79,6 +106,7 @@ let package = Package( ], swiftSettings: [ .define("CRYPTO_IN_SWIFTPM_FORCE_BUILD_API"), + .enableExperimentalFeature("SafeInteropWrappers"), ] ), .testTarget( @@ -103,7 +131,6 @@ if development { .unsafeFlags([ "-Xfrontend", "-warn-concurrency", "-Xfrontend", "-enable-actor-data-race-checks", - "-enable-library-evolution", ]) ) } diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift index 7237ada..4be4c3b 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift @@ -40,6 +40,22 @@ extension K1.ECDSA.Signature { wrapped: FFI.ECDSA.from(compactBytes: [UInt8](rawRepresentation)) ) } + + /// Creates a `secp256k1` ECDSA signature from the raw representation. + /// + /// Accepts 64 bytes on format: `R || S`, as defined in [rfc4754][rfc]. In + /// `libsecp256k1` this representation is called "compact". + /// + /// - Parameter rawRepresentation: A raw representation of the ECDSA signature as an + /// `InlineArray<64, UInt8>` + /// + /// [rfc]: https://tools.ietf.org/html/rfc4754 + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + public init(rawRepresentation array64: InlineArray<64, UInt8>) throws { + try self.init( + wrapped: FFI.ECDSA.from(compactBytes: array64) + ) + } } // MARK: ContiguousBytes @@ -60,11 +76,7 @@ extension K1.ECDSA.Signature { /// Returns 64 bytes on format: `R || S`, as defined in [rfc4754][rfc]. In /// `libsecp256k1` this representation is called "compact". public var rawRepresentation: Data { - do { - return try FFI.ECDSA.compact(wrapped) - } catch { - fatalError("Should never fail to convert ECDSA signatures to rawRepresentation.") - } + FFI.ECDSA.compact(wrapped) } /// A Distinguished Encoding Rules (DER) encoded representation of a diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift index 59f0632..6c6a0f3 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift @@ -64,10 +64,10 @@ extension K1.ECDSAWithKeyRecovery.Signature { /// Compact aka `IEEE P1363` aka `R||S` and `V` (`RecoveryID`). public func compact() throws -> Compact { // swiftlint:disable:next identifier_name - let (rs, recid) = try FFI.ECDSAWithKeyRecovery.serializeCompact( + let (rs, recid) = FFI.ECDSAWithKeyRecovery.serializeCompact( wrapped ) - return try .init( + return try Compact( compact: Data(rs), recoveryID: .init(recid: recid) ) @@ -199,8 +199,8 @@ extension K1.ECDSAWithKeyRecovery.Signature { // MARK: Conversion extension K1.ECDSAWithKeyRecovery.Signature { /// Converts this recoverable ECDSA signature to a non-recoverable version. - public func nonRecoverable() throws -> K1.ECDSA.Signature { - try K1.ECDSA.Signature( + public func nonRecoverable() -> K1.ECDSA.Signature { + K1.ECDSA.Signature( wrapped: FFI.ECDSAWithKeyRecovery.nonRecoverable(self.wrapped) ) } diff --git a/Sources/K1/K1/Keys/Keys.generated.swift b/Sources/K1/K1/Keys/Keys.generated.swift index 67678d5..87cc432 100644 --- a/Sources/K1/K1/Keys/Keys.generated.swift +++ b/Sources/K1/K1/Keys/Keys.generated.swift @@ -17,13 +17,16 @@ extension K1.KeyAgreement { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for key agreement. @@ -216,7 +219,7 @@ extension K1.KeyAgreement { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -239,13 +242,16 @@ extension K1.Schnorr { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. @@ -439,7 +445,7 @@ extension K1.Schnorr { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -462,13 +468,16 @@ extension K1.ECDSA { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. @@ -662,7 +671,7 @@ extension K1.ECDSA { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -685,13 +694,16 @@ extension K1.ECDSAWithKeyRecovery { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. @@ -885,7 +897,7 @@ extension K1.ECDSAWithKeyRecovery { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -896,28 +908,13 @@ extension K1.ECDSAWithKeyRecovery { } } -// MARK: - K1.KeyAgreement.PrivateKey + _K1PrivateKeyProtocol extension K1.KeyAgreement.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.KeyAgreement.PublicKey + _K1PublicKeyProtocol extension K1.KeyAgreement.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.Schnorr.PrivateKey + _K1PrivateKeyProtocol extension K1.Schnorr.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.Schnorr.PublicKey + _K1PublicKeyProtocol extension K1.Schnorr.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.ECDSA.PrivateKey + _K1PrivateKeyProtocol extension K1.ECDSA.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.ECDSA.PublicKey + _K1PublicKeyProtocol extension K1.ECDSA.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.ECDSAWithKeyRecovery.PrivateKey + _K1PrivateKeyProtocol extension K1.ECDSAWithKeyRecovery.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.ECDSAWithKeyRecovery.PublicKey + _K1PublicKeyProtocol extension K1.ECDSAWithKeyRecovery.PublicKey: _K1PublicKeyProtocol {} // swiftlint:enable all diff --git a/Sources/K1/K1/Keys/Keys.swift.gyb b/Sources/K1/K1/Keys/Keys.swift.gyb index 0b51bfd..83705ff 100644 --- a/Sources/K1/K1/Keys/Keys.swift.gyb +++ b/Sources/K1/K1/Keys/Keys.swift.gyb @@ -58,13 +58,17 @@ extension K1.${FEATURE} { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey + init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for ${PURPOSE_PRIVATEKEY}. @@ -259,7 +263,7 @@ extension K1.${FEATURE} { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve diff --git a/Sources/K1/K1/Keys/PublicKeyImplementation.swift b/Sources/K1/K1/Keys/PublicKeyImplementation.swift index 53158f5..867f1f5 100644 --- a/Sources/K1/K1/Keys/PublicKeyImplementation.swift +++ b/Sources/K1/K1/Keys/PublicKeyImplementation.swift @@ -83,14 +83,12 @@ extension K1._PublicKeyImplementation { /// `04 || X || Y` (65 bytes) var x963Representation: Data { - // swiftlint:disable:next force_try - try! FFI.PublicKey.serialize(wrapped, format: .uncompressed) + FFI.PublicKey.serialize(wrapped, format: .uncompressed) } /// `02|03 || X` (33 bytes) var compressedRepresentation: Data { - // swiftlint:disable:next force_try - try! FFI.PublicKey.serialize(wrapped, format: .compressed) + FFI.PublicKey.serialize(wrapped, format: .compressed) } /// `DER` @@ -119,15 +117,7 @@ extension K1._PublicKeyImplementation { static func == (lhsSelf: Self, rhsSelf: Self) -> Bool { let lhs = lhsSelf.wrapped let rhs = rhsSelf.wrapped - do { - return try lhs.compare(to: rhs) - } catch { - return lhs.withUnsafeBytes { lhsBytes in - rhs.withUnsafeBytes { rhsBytes in - safeCompare(lhsBytes, rhsBytes) - } - } - } + return lhs.isEqual(to: rhs) } } @@ -153,8 +143,8 @@ extension K1._PublicKeyImplementation { } /// Negates a public key (point) on the secp256k1 curve - func negate() throws -> Self { - try Self(wrapped: wrapped.negate()) + func negate() -> Self { + Self(wrapped: wrapped.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve diff --git a/Sources/K1/K1/Validation/Validation.generated.swift b/Sources/K1/K1/Validation/Validation.generated.swift index 1e0a64b..080e8d1 100644 --- a/Sources/K1/K1/Validation/Validation.generated.swift +++ b/Sources/K1/K1/Validation/Validation.generated.swift @@ -21,16 +21,12 @@ extension K1.ECDSA.PublicKey { hashed: some DataProtocol, options: K1.ECDSA.ValidationOptions = .default ) -> Bool { - do { - return try FFI.ECDSA.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed), - options: options - ) - } catch { - return false - } + FFI.ECDSA.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options + ) } /// Verifies an Elliptic Curve Digital Signature Algorithm (ECDSA) non recoverable signature on a digest over the `secp256k1` elliptic curve. @@ -87,16 +83,12 @@ extension K1.ECDSAWithKeyRecovery.PublicKey { hashed: some DataProtocol, options: K1.ECDSA.ValidationOptions = .default ) -> Bool { - do { - return try FFI.ECDSAWithKeyRecovery.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed), - options: options - ) - } catch { - return false - } + FFI.ECDSAWithKeyRecovery.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options + ) } /// Verifies an Elliptic Curve Digital Signature Algorithm (ECDSA) recoverable signature on a digest over the `secp256k1` elliptic curve. @@ -149,16 +141,48 @@ extension K1.Schnorr.PublicKey { /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. public func isValidSignature( _ signature: K1.Schnorr.Signature, - hashed: some DataProtocol + hashed: Span + ) -> Bool { + FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: hashed + ) + } + + /// Verifies Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed bytes: [UInt8] + ) -> Bool { + withSpanFromArray(bytes) { span in + FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span + ) + } + } + + /// Verifies Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed data: some DataProtocol ) -> Bool { - do { - return try FFI.Schnorr.isValid( + withSpanFromData(data) { span in + FFI.Schnorr.isValid( signature: signature.wrapped, publicKey: self.impl.wrapped, - message: [UInt8](hashed) + message: span ) - } catch { - return false } } diff --git a/Sources/K1/K1/Validation/Validation.swift.gyb b/Sources/K1/K1/Validation/Validation.swift.gyb index d44ddde..e81ddeb 100644 --- a/Sources/K1/K1/Validation/Validation.swift.gyb +++ b/Sources/K1/K1/Validation/Validation.swift.gyb @@ -50,23 +50,71 @@ import protocol CryptoKit.Digest extension K1.${FEATURE}.PublicKey { /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. /// - Parameters: +% if FEATURE == "Schnorr": /// - signature: The ${VARIANT} signature to check against the _hashed_ data. /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. public func isValidSignature( _ signature: K1.${FEATURE}.Signature, - hashed: some DataProtocol${VALIDATION_OPTIONS_ARG} + hashed: Span${VALIDATION_OPTIONS_ARG} + ) -> Bool { + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: hashed${VALIDATION_OPTIONS_FWD} + ) + } + + /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed bytes: [UInt8]${VALIDATION_OPTIONS_ARG} + ) -> Bool { + withSpanFromArray(bytes) { span in + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span${VALIDATION_OPTIONS_FWD} + ) + } + } + + /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed data: some DataProtocol${VALIDATION_OPTIONS_ARG} ) -> Bool { - do { - return try FFI.${FEATURE}.isValid( + withSpanFromData(data) { span in + FFI.${FEATURE}.isValid( signature: signature.wrapped, publicKey: self.impl.wrapped, - message: [UInt8](hashed)${VALIDATION_OPTIONS_FWD} + message: span${VALIDATION_OPTIONS_FWD} ) - } catch { - return false } } +% else: + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed: some DataProtocol${VALIDATION_OPTIONS_ARG} + ) -> Bool { + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed)${VALIDATION_OPTIONS_FWD} + ) + } +% end /// Verifies ${VARIANT} signature on a digest over the `secp256k1` elliptic curve. /// - Parameters: diff --git a/Sources/K1/Support/Extensions/Data+Extensions.swift b/Sources/K1/Support/Extensions/Foundation/Data+Extensions.swift similarity index 100% rename from Sources/K1/Support/Extensions/Data+Extensions.swift rename to Sources/K1/Support/Extensions/Foundation/Data+Extensions.swift diff --git a/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift new file mode 100644 index 0000000..aaaefc8 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift @@ -0,0 +1,23 @@ +import Secp256k1 + +/// Outcome of comparing two values with each other +enum ComparisonOutcome { + /// The compared values are equal. + case equal + + /// The RHS of the compared values is greater than the LHS. + case lhsIsGreater + + /// The RHS of the compared values is greater than the LHS. + case rhsIsGreater +} + +extension ComparisonOutcome { + init(raw: ComparisonOutcomeRaw) { + switch raw { + case .SECP256K1_PUBKEY_CMP_EQUAL: self = .equal + case .SECP256K1_PUBKEY_CMP_LHS_IS_GREATER: self = .lhsIsGreater + case .SECP256K1_PUBKEY_CMP_RHS_IS_GREATER: self = .rhsIsGreater + } + } +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift new file mode 100644 index 0000000..660d284 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift @@ -0,0 +1,6 @@ +import Secp256k1 + +extension NormalizeSignatureOutcomeRaw { + /// The checked signature was not normalized. + static let wasntNormalized: Self = .SECP256K1_NORMALIZE_SIG_WASNT_NORMALIZED +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift new file mode 100644 index 0000000..acd4238 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift @@ -0,0 +1,9 @@ +import Secp256k1 + +extension ResultRaw { + /// The call to the `secp256k1` function returned with successful result. + static let success: Self = .SECP256K1_RESULT_SUCCESS + + /// The call to the `secp256k1` function returned with failure result. + static let failure: Self = .SECP256K1_RESULT_FAILURE +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift new file mode 100644 index 0000000..9393229 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift @@ -0,0 +1,9 @@ +import Secp256k1 + +extension VerifySignatureOutcomeRaw { + /// The checked Signature is valid. + static let signatureValid: Self = .SECP256K1_VERIFY_SIG_CORRECT + + /// The checked Signature is invalid or we failed to parse it. + static let signatureInvalid: Self = .SECP256K1_VERIFY_SIG_UNPARSABLE_OR_INCORRECT +} diff --git a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift index 9602de9..75dd624 100644 --- a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift +++ b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDH extension FFI { @@ -34,8 +34,8 @@ extension FFI.ECDH { func hashfp() -> ((@convention(c) (UnsafeMutablePointer?, UnsafePointer?, UnsafePointer?, UnsafeMutableRawPointer?) -> Int32)?) { switch self { case .libsecp256kDefault: return secp256k1_ecdh_hash_function_default - case .ansiX963: return ecdh_asn1_x963 - case .noHashWholePoint: return ecdh_unsafe_whole_point + case .ansiX963: return ecdh_hash_function_asn1_x963 + case .noHashWholePoint: return ecdh_hash_function_unsafe_whole_point } } @@ -73,23 +73,23 @@ extension FFI.ECDH { ) { context in if var arbitraryData { arbitraryData.withUnsafeMutableBytes { ptr in - secp256k1_ecdh( - context, - &sharedPublicPointBytes, // output - &publicKeyRaw, // pubkey - privateKey.secureBytes.backing.bytes, // seckey - hashFp.hashfp(), // hashfp - ptr.baseAddress // properly formed pointer + ecdh( + context: context, + outputSharedPointBytes: &sharedPublicPointBytes, + publicKey: &publicKeyRaw, + privateKeyBytes: privateKey.secureBytes.backing.bytes, + hashFunction: hashFp.hashfp(), + arbitraryData: ptr.baseAddress ) } } else { - secp256k1_ecdh( - context, - &sharedPublicPointBytes, - &publicKeyRaw, - privateKey.secureBytes.backing.bytes, - hashFp.hashfp(), - nil // No arbitrary data + ecdh( + context: context, + outputSharedPointBytes: &sharedPublicPointBytes, + publicKey: &publicKeyRaw, + privateKeyBytes: privateKey.secureBytes.backing.bytes, + hashFunction: hashFp.hashfp(), + arbitraryData: nil // No arbitrary data ) } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift index 4284049..80eecf2 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSA extension FFI { @@ -7,25 +7,25 @@ extension FFI { public enum ECDSA {} } -// MARK: - RawECDSASignature -protocol RawECDSASignature { +// MARK: - EmptyInitializable +protocol EmptyInitializable { init() } -// MARK: - secp256k1_ecdsa_recoverable_signature + RawECDSASignature -extension secp256k1_ecdsa_recoverable_signature: RawECDSASignature {} +// MARK: - ECDSARecoverableSignatureRaw + EmptyInitializable +extension ECDSARecoverableSignatureRaw: EmptyInitializable {} -// MARK: - secp256k1_ecdsa_signature + RawECDSASignature -extension secp256k1_ecdsa_signature: RawECDSASignature {} +// MARK: - ECDSASignatureRaw + EmptyInitializable +extension ECDSASignatureRaw: EmptyInitializable {} // MARK: - WrappedECDSASignature protocol WrappedECDSASignature { - associatedtype Raw: RawECDSASignature + associatedtype Raw: EmptyInitializable init(raw: Raw) var raw: Raw { get } // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 + static func sign() -> ECDSAFunctionPointer } // MARK: ECDSA Shared @@ -70,7 +70,7 @@ extension K1.ECDSA.SigningOptions { extension K1.ECDSA.SigningOptions.NonceFunction { // swiftlint:disable:next line_length - fileprivate func function() -> (@convention(c) (UnsafeMutablePointer?, UnsafePointer?, UnsafePointer?, UnsafePointer?, UnsafeMutableRawPointer?, UInt32) -> Int32)? { + fileprivate func function() -> secp256k1_nonce_function? { switch self { case .deterministic: return secp256k1_nonce_function_rfc6979 diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift index 384fb3a..bc89925 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -1,11 +1,11 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSA.Wrapped extension FFI.ECDSA { struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { // swiftlint:disable:next nesting - typealias Raw = secp256k1_ecdsa_signature + typealias Raw = ECDSASignatureRaw let raw: Raw init(raw: Raw) { self.raw = raw @@ -16,8 +16,15 @@ extension FFI.ECDSA { // MARK: Sign extension FFI.ECDSA.Wrapped { // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign + static func sign() -> ECDSAFunctionPointer { + ecdsaSignNonRecoverable( + context: + outputSignature: + hashedMessageBytes: + privateKeyBytes: + nonceFunctionPointer: + arbitraryNonceData: + ) } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index ea5e643..687dc76 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.ECDSA { @@ -14,8 +14,26 @@ extension FFI.ECDSA { ) } + /// Compact aka `IEEE P1363` aka `R||S`. + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + static func from( + compactBytes array64: InlineArray<64, UInt8> + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(compactBytes: array64) + ) + } + static func from( derRepresentation: [UInt8] + ) throws -> Wrapped { + try withSpanFromArray(derRepresentation) { span in + try Self.from(derRepresentation: span) + } + } + + static func from( + derRepresentation: Span ) throws -> Wrapped { try Wrapped( raw: Raw.nonRecoverableSignature(derBytes: derRepresentation) @@ -25,14 +43,14 @@ extension FFI.ECDSA { // MARK: Serialize extension FFI.ECDSA { - static func compact(_ wrapped: Wrapped) throws -> Data { + static func compact(_ wrapped: Wrapped) -> Data { var out = [UInt8](repeating: 0, count: Self.byteCount) var rawSignature = wrapped.raw - try FFI.call(ifFailThrow: .ecdsaSignatureSerializeCompact) { context in - secp256k1_ecdsa_signature_serialize_compact( - context, - &out, - &rawSignature + FFI.call { context in + serializeEcdsaSignatureCompact( + context: context, + outputBytes: &out, + signature: &rawSignature ) } return Data(out) @@ -45,11 +63,11 @@ extension FFI.ECDSA { var derSignature = [UInt8](repeating: 0, count: derMaxLength) var rawSignature = wrapped.raw try FFI.call(ifFailThrow: .ecdsaSignatureSerializeDER) { context in - secp256k1_ecdsa_signature_serialize_der( - context, - &derSignature, - &derMaxLength, - &rawSignature + serializeEcdsaSignatureDER( + context: context, + outputBytes: &derSignature, + outputByteCount: &derMaxLength, + signature: &rawSignature ) } return Data(derSignature.prefix(derMaxLength)) @@ -66,7 +84,7 @@ extension FFI.ECDSA { guard message.count == Curve.Field.byteCount else { throw K1.Error.incorrectParameterSize } - let nonRecoverableCompact = try FFI.ECDSA.compact(wrapped) + let nonRecoverableCompact = FFI.ECDSA.compact(wrapped) return try Self.recoverPublicKey( nonRecoverableCompact: nonRecoverableCompact, recoveryID: recoveryID, @@ -83,22 +101,22 @@ extension FFI.ECDSA { throw K1.Error.incorrectParameterSize } var compact = [UInt8](nonRecoverableCompact) - var recoverable = secp256k1_ecdsa_recoverable_signature() + var recovered = ECDSARecoverableSignatureRaw() try FFI.call(ifFailThrow: .recoverableSignatureParseCompact) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &recoverable, - &compact, - recoveryID + parseRecoverableECDSASignatureFromCompactBytes( + context: context, + outputRecoveredSignature: &recovered, + compactBytes: &compact, + recoveryID: recoveryID ) } - var publicKeyRaw = secp256k1_pubkey() + var publicKeyRaw = PublicKeyRaw() try FFI.call(ifFailThrow: .recover) { context in - secp256k1_ecdsa_recover( - context, - &publicKeyRaw, - &recoverable, - message + recoverPublicKeyFromECDSASignature( + context: context, + publicKey: &publicKeyRaw, + signature: &recovered, + hashedMessage: message ) } return FFI.PublicKey.Wrapped(raw: publicKeyRaw) @@ -112,25 +130,25 @@ extension FFI.ECDSA { publicKey: FFI.PublicKey.Wrapped, message: [UInt8], options: K1.ECDSA.ValidationOptions = .default - ) throws -> Bool { - try FFI.toC { ffi -> Bool in + ) -> Bool { + FFI.toC { ffi -> Bool in var publicKeyRaw = publicKey.raw var maybeMalleable = signature.raw - var normalized = secp256k1_ecdsa_signature() + var normalized = ECDSASignatureRaw() - let codeForSignatureWasMalleable = 1 - let signatureWasMalleableResult = ffi.callWithResultCode { context in - secp256k1_ecdsa_signature_normalize(context, &normalized, &maybeMalleable) - } - let signatureWasMalleable = signatureWasMalleableResult == codeForSignatureWasMalleable - let isSignatureValid = ffi.validate { context in - secp256k1_ecdsa_verify( - context, - &normalized, - message, - &publicKeyRaw + let signatureWasMalleable = ffi.call { context in + normalizeEcdsaSignature(context: context, outputSignature: &normalized, inputSignature: &maybeMalleable) + } == .wasntNormalized + + let isSignatureValid = ffi.call { context in + verifyEcdsaSignature( + context: context, + signature: &normalized, + messageHash: message, + publicKey: &publicKeyRaw ) - } + } == .signatureValid + let acceptMalleableSignatures = options.malleabilityStrictness == .accepted switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { case (true, false, _): diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index 31bcb37..b01d835 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -1,11 +1,11 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSAWithKeyRecovery.Wrapped extension FFI.ECDSAWithKeyRecovery { struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { // swiftlint:disable:next nesting - typealias Raw = secp256k1_ecdsa_recoverable_signature + typealias Raw = ECDSARecoverableSignatureRaw let raw: Raw init(raw: Raw) { self.raw = raw @@ -13,11 +13,27 @@ extension FFI.ECDSAWithKeyRecovery { } } +typealias ECDSAFunctionPointer = ( + OpaquePointer, + UnsafeMutablePointer, + UnsafePointer, + UnsafePointer, + secp256k1_nonce_function?, + UnsafeRawPointer? +) -> ResultRaw + // MARK: Sign extension FFI.ECDSAWithKeyRecovery.Wrapped { // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign_recoverable + static func sign() -> ECDSAFunctionPointer { + ecdsaSignRecoverable( + context: + outputSignature: + hashedMessageBytes: + privateKeyBytes: + nonceFunctionPointer: + arbitraryNonceData: + ) } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index dab52be..87d7d1b 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.ECDSAWithKeyRecovery { @@ -21,11 +21,11 @@ extension FFI.ECDSAWithKeyRecovery { try FFI.call( ifFailThrow: .recoverableSignatureParseCompact ) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &raw, - rs, - recid + parseRecoverableECDSASignatureFromCompactBytes( + context: context, + outputRecoveredSignature: &raw, + compactBytes: rs, + recoveryID: recid ) } return .init(raw: raw) @@ -36,19 +36,17 @@ extension FFI.ECDSAWithKeyRecovery { extension FFI.ECDSAWithKeyRecovery { static func serializeCompact( _ wrapped: Wrapped - ) throws -> (rs: [UInt8], recoveryID: Int32) { + ) -> (rs: [UInt8], recoveryID: Int32) { // swiftlint:disable:next identifier_name var rs = [UInt8](repeating: 0, count: FFI.ECDSA.byteCount) var recoveryID: Int32 = 0 var rawSignature = wrapped.raw - try FFI.call( - ifFailThrow: .recoverableSignatureSerializeCompact - ) { context in - secp256k1_ecdsa_recoverable_signature_serialize_compact( - context, - &rs, - &recoveryID, - &rawSignature + FFI.call { context in + serializeRecoverableECDSASignatureCompact( + context: context, + outputBytes: &rs, + recoveryID: &recoveryID, + recoverableSignature: &rawSignature ) } return (rs, recoveryID) @@ -57,19 +55,18 @@ extension FFI.ECDSAWithKeyRecovery { // MARK: Convert extension FFI.ECDSAWithKeyRecovery { + /// Convert a recoverable signature into a normal signature. static func nonRecoverable( _ wrapped: Wrapped - ) throws -> FFI.ECDSA.Wrapped { - var nonRecoverable = secp256k1_ecdsa_signature() + ) -> FFI.ECDSA.Wrapped { + var nonRecoverable = ECDSASignatureRaw() var recoverable = wrapped.raw - try FFI.call( - ifFailThrow: .recoverableSignatureConvert - ) { context in - secp256k1_ecdsa_recoverable_signature_convert( - context, - &nonRecoverable, - &recoverable + FFI.call { context in + ecdsaRecoverableSignatureToNonRecoverable( + context: context, + outputNonRecoverableSignature: &nonRecoverable, + recoverableSignature: &recoverable ) } @@ -87,15 +84,15 @@ extension FFI.ECDSAWithKeyRecovery { throw K1.Error.incorrectParameterSize } var rawSignature = wrapped.raw - var rawPublicKey = secp256k1_pubkey() + var rawPublicKey = PublicKeyRaw() try FFI.call( ifFailThrow: .recover ) { context in - secp256k1_ecdsa_recover( - context, - &rawPublicKey, - &rawSignature, - message + recoverPublicKeyFromECDSASignature( + context: context, + publicKey: &rawPublicKey, + signature: &rawSignature, + hashedMessage: message ) } return FFI.PublicKey.Wrapped(raw: rawPublicKey) @@ -109,19 +106,15 @@ extension FFI.ECDSAWithKeyRecovery { publicKey: FFI.PublicKey.Wrapped, message: [UInt8], options: K1.ECDSA.ValidationOptions = .default - ) throws -> Bool { - do { - let publicKeyNonRecoverable = FFI.PublicKey.Wrapped(raw: publicKey.raw) - let signatureNonRecoverable = try FFI.ECDSAWithKeyRecovery.nonRecoverable(signature) - return try FFI.ECDSA.isValid( - signature: signatureNonRecoverable, - publicKey: publicKeyNonRecoverable, - message: message, - options: options - ) - } catch { - return false - } + ) -> Bool { + let publicKeyNonRecoverable = FFI.PublicKey.Wrapped(raw: publicKey.raw) + let signatureNonRecoverable = FFI.ECDSAWithKeyRecovery.nonRecoverable(signature) + return FFI.ECDSA.isValid( + signature: signatureNonRecoverable, + publicKey: publicKeyNonRecoverable, + message: message, + options: options + ) } } diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index e2b26fc..8ed4904 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.PrivateKey extension FFI { @@ -21,16 +21,15 @@ extension FFI { self.secureBytes = secureBytes var secureBytes = secureBytes self.publicKey = try secureBytes.withUnsafeMutableBytes { seckey in - var raw = secp256k1_pubkey() + var raw = PublicKeyRaw() try FFI.call(ifFailThrow: .publicKeyCreate) { context in - secp256k1_ec_pubkey_create( - context, - &raw, - seckey.baseAddress! + createPublicKey( + context: context, + outputPublicKey: &raw, + privateKeyBytes: seckey.baseAddress! ) } - return FFI.PublicKey.Wrapped(raw: raw) } } diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift index 8e7ae40..60041cd 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.PublicKey { @@ -16,12 +16,12 @@ extension FFI.PublicKey { static func deserialize( x963Representation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.x963ByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) + return try Self._deserialize(span: span) } } @@ -29,12 +29,20 @@ extension FFI.PublicKey { static func deserialize( rawRepresentation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.rawByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self.deserialize(x963Representation: [0x04] + [UInt8](bufferPointer)) + // Prepend 0x04 prefix into a small temporary buffer + var prefixed = [UInt8](repeating: 0, count: expected + 1) + prefixed[0] = 0x04 + for index in 0 ..< span.count { + prefixed[index + 1] = span[index] + } + return try prefixed.withUnsafeBufferPointer { buf in + try Self._deserialize(span: Span(_unsafeElements: buf)) + } } } @@ -42,26 +50,27 @@ extension FFI.PublicKey { static func deserialize( compressedRepresentation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.compressedByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) + return try Self._deserialize(span: span) } } - private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { - var raw = secp256k1_pubkey() + static func deserialize( + compressedRepresentation span: Span + ) throws -> Wrapped { + try _deserialize(span: span) + } + + private static func _deserialize(span: Span) throws -> Wrapped { + var raw = PublicKeyRaw() try FFI.call( ifFailThrow: .publicKeyParse ) { context in - secp256k1_ec_pubkey_parse( - context, - &raw, - bytes, - bytes.count - ) + parsePublicKey(context: context, outputPublicKey: &raw, inputBytes: span) } return .init(raw: raw) } @@ -72,17 +81,17 @@ extension FFI.PublicKey { static func serialize( _ wrapped: Wrapped, format: K1.Format - ) throws -> Data { + ) -> Data { var byteCount = format.length var out = [UInt8](repeating: 0x00, count: byteCount) var publicKeyRaw = wrapped.raw - try FFI.call(ifFailThrow: .publicKeySerialize) { context in - secp256k1_ec_pubkey_serialize( - context, - &out, - &byteCount, - &publicKeyRaw, - format.rawValue + FFI.call { context in + serializePublicKey( + context: context, + outputBytes: &out, + outputByteCount: &byteCount, + publicKey: &publicKeyRaw, + formatFlags: format.rawValue ) } return Data(out.prefix(byteCount)) diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index 63e0cbb..4417a47 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.PublicKey extension FFI { @@ -10,7 +10,7 @@ extension FFI { extension FFI.PublicKey { struct Wrapped: @unchecked Sendable, ContiguousBytes { // swiftlint:disable:next nesting - typealias Raw = secp256k1_pubkey + typealias Raw = PublicKeyRaw let raw: Raw init(raw: Raw) { self.raw = raw @@ -30,15 +30,22 @@ extension FFI.PublicKey.Wrapped { // MARK: Comparison extension FFI.PublicKey.Wrapped { - func compare(to other: FFI.PublicKey.Wrapped) throws -> Bool { - var selfRaw = self.raw - var otherRaw = other.raw - return try FFI.toC { ffi in - ffi.callWithResultCode { context in - secp256k1_ec_pubkey_cmp(context, &selfRaw, &otherRaw) - } == 0 + func compare(to other: FFI.PublicKey.Wrapped) -> ComparisonOutcome { + var lhs = self.raw + var rhs = other.raw + return FFI.call { context in + let outcomeRaw = comparePublicKeys( + context: context, + lhs: &lhs, + rhs: &rhs + ) + return ComparisonOutcome(raw: outcomeRaw) } } + + func isEqual(to other: FFI.PublicKey.Wrapped) -> Bool { + compare(to: other) == .equal + } } // MARK: Group Operations @@ -54,12 +61,10 @@ extension FFI.PublicKey.Wrapped { } /// Negates a public key (point) on the secp256k1 curve - func negate() throws -> Self { + func negate() -> Self { var result = self.raw - try FFI.toC { ffi in - try ffi.call(ifFailThrow: .publicKeyCreate) { context in - secp256k1_ec_pubkey_negate(context, &result) - } + FFI.call { context in + negatePublicKey(context: context, publicKey: &result) } return Self(raw: result) } @@ -70,22 +75,25 @@ extension FFI.PublicKey.Wrapped { throw K1.Error.invalidParameter } - var result = secp256k1_pubkey() + var result = PublicKeyRaw() var mutableKeys = keys.map(\.raw) - try mutableKeys.withUnsafeMutableBufferPointer { keysBuffer in - var keyPointers = [UnsafePointer?]() - keyPointers.reserveCapacity(keys.count) + try FFI.call(ifFailThrow: .groupOperation) { context in + mutableKeys.withUnsafeMutableBufferPointer { keysBuffer in + var keyPointers = [UnsafePointer?]() + keyPointers.reserveCapacity(keys.count) - for index in 0 ..< keys.count { - keyPointers.append(keysBuffer.baseAddress!.advanced(by: index)) - } + for index in 0 ..< keys.count { + keyPointers.append(keysBuffer.baseAddress!.advanced(by: index)) + } - try keyPointers.withUnsafeBufferPointer { pointers in - try FFI.toC { ffi in - try ffi.call(ifFailThrow: .groupOperation) { context in - secp256k1_ec_pubkey_combine(context, &result, pointers.baseAddress!, keys.count) - } + return keyPointers.withUnsafeBufferPointer { pointers in + combinePublicKeys( + context: context, + outputPublicKey: &result, + inputs: pointers.baseAddress!, + inputCount: keys.count + ) } } } diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 027efdf..9489814 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.Schnorr extension FFI { @@ -11,29 +11,28 @@ extension FFI.Schnorr { static func isValid( signature: FFI.Schnorr.Wrapped, publicKey: FFI.PublicKey.Wrapped, - message: [UInt8] - ) throws -> Bool { - try FFI.toC { ffi -> Bool in - var publicKeyX = secp256k1_xonly_pubkey() + message: Span + ) -> Bool { + FFI.toC { ffi -> Bool in + var publicKeyX = PublicKeyXOnlyRaw() var publicKeyRaw = publicKey.raw - try FFI.call(ifFailThrow: .xonlyPublicKeyFromPublicKey) { context in - secp256k1_xonly_pubkey_from_pubkey( - context, - &publicKeyX, - nil, - &publicKeyRaw + ffi.call { context in + _ = xOnlyPublicKeyFromPublicKey( + context: context, + outputXOnlyPublicKey: &publicKeyX, + parity: nil, + publicKey: &publicKeyRaw ) } - return ffi.validate { context in - secp256k1_schnorrsig_verify( - context, - signature.bytes, - message, - message.count, - &publicKeyX + return ffi.call { context in + verifySchnorrSignature( + context: context, + signatureBytes: signature.bytes, + msg: message, + xOnlyPublicKey: &publicKeyX ) - } + } == .success } } } @@ -53,27 +52,27 @@ extension FFI.Schnorr { var signatureOut = [UInt8](repeating: 0, count: FFI.Schnorr.Wrapped.byteCount) - var keyPair = secp256k1_keypair() + var keyPair = KeypairRaw() try FFI.call( ifFailThrow: .keypairCreate ) { context in - secp256k1_keypair_create( - context, - &keyPair, - privateKey.secureBytes.backing.bytes + keypairFromPrivateKey( + context: context, + outputKeyPair: &keyPair, + privateKeyBytes: privateKey.secureBytes.backing.bytes ) } try FFI.call( ifFailThrow: .schnorrSign ) { context in - secp256k1_schnorrsig_sign32( - context, - &signatureOut, - message, - &keyPair, - options.auxiliaryRandomData.bytes + schnorrSign( + context: context, + outputSignatureBytes: &signatureOut, + message: message, + keypair: &keyPair, + auxiliaryRandomData: options.auxiliaryRandomData.bytes ) } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Call.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift index a9a688a..4e5d55d 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Call.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -1,64 +1,120 @@ import CryptoKit -import secp256k1 +import Secp256k1 + +/// `secp256k1_context` +/// +/// In the best of worlds this would not be a typealias +/// for an `OpaquePointer`, mapping to a Swift class using +/// apinotes have been attempted without success, seems the +/// implementation of `secp256k1` prevents it. +/// +/// An `UnsafePointer` would also +/// have been better, but mapping to it using apinotes also +/// failed. +typealias Secp256k1ContextRaw = OpaquePointer // MARK: - FFI + +/// A swift wrapper around an `OpaquePointer` to `secp256k1_context` +/// created with `secp256k1_context_create`. +/// +/// In the best of worlds this would not be a typealias +/// for an `OpaquePointer`, mapping to a Swift class using +/// apinotes have been attempted without success, seems the +/// implementation of `secp256k1` prevents it. +/// +/// An `UnsafePointer` would also +/// have been better, but mapping to it using apinotes also +/// failed. final class FFI { - let context: OpaquePointer - init() throws { + /// The wrapped `secp256k1_context` (`OpaquePointer`). + let context: Secp256k1ContextRaw + + /// Creates a new `secp256k1_context` using `secp256k1_context_create` to be used + /// for both signing and verification operations. + /// + /// Will crash if `secp256k1_context_create` fails, which it never should. + init() { guard - /* "Create a secp256k1 context object." */ - let context = secp256k1_context_create(Context.sign.rawValue | Context.verify.rawValue) + // Create secp256k1 context object + let context = createContext(flags: Context.sign.rawValue | Context.verify.rawValue) else { - throw K1.Error.underlyingLibsecp256k1Error(.failedToCreateContextForSecp256k1) + fatalError( + """ + Failed to create context, did you run out of memory? + + `secp256k1_context_create` call failed. Which under most circumstances should never ever happen. + + Please report a bug at: + https://github.com/Sajjon/K1/issues/new + + And provide OS and Swift version details. + """ + ) } self.context = context } deinit { - secp256k1_context_destroy(context) + destroyContext(context) } } +// MARK: Helper Methods extension FFI { - static func toC( - _ closure: (FFI) throws -> T - ) throws -> T { - let ffi = try FFI() - return try closure(ffi) + func call( + ifFailThrow error: FFI.Error, + _ method: (Secp256k1ContextRaw) -> ResultRaw + ) throws { + guard method(context) == ResultRaw.success else { + throw K1.Error.underlyingLibsecp256k1Error(error) + } } - /// Returns `true` iff result code is `1` - func validate( - _ method: (OpaquePointer) -> Int32 - ) -> Bool { - method(context) == 1 + func call( + _ method: (OpaquePointer) throws -> R + ) rethrows -> R { + try method(context) } - func callWithResultCode( - _ method: (OpaquePointer) -> Int32 - ) -> Int { - let result = method(context) - return Int(result) + func callGetResult( + _ method: (OpaquePointer) -> ResultRaw + ) -> ResultRaw { + method(context) } +} - func call( - ifFailThrow error: FFI.Error, - _ method: (OpaquePointer) -> Int32 - ) throws { - let result = callWithResultCode(method) - let successCode = 1 - guard result == successCode else { - throw K1.Error.underlyingLibsecp256k1Error(error) - } +// MARK: - Static +extension FFI { + static func toC( + _ body: (FFI) throws -> R + ) rethrows -> R { + try body(FFI()) + } + + static func call( + _ method: @escaping (OpaquePointer) throws -> R + ) rethrows -> R { + try method(FFI().context) + } + + static func callGetResult( + _ method: (OpaquePointer) -> ResultRaw + ) -> ResultRaw { + FFI().callGetResult(method) } static func call( ifFailThrow error: FFI.Error, - _ method: (OpaquePointer) -> Int32 + _ method: (OpaquePointer) -> ResultRaw ) throws { - try toC { ffi in - try ffi.call(ifFailThrow: error, method) - } + try FFI().call(ifFailThrow: error, method) + } + + static func call( + _ method: @escaping (OpaquePointer) -> Int32 + ) { + _ = method(FFI().context) } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Context.swift b/Sources/K1/Support/FFI/Internals/FFI+Context.swift index 5c6e6db..ff9a1d8 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Context.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Context.swift @@ -1,5 +1,5 @@ // FFI to C -import secp256k1 +import Secp256k1 // MARK: - FFI.Context extension FFI { diff --git a/Sources/K1/Support/FFI/Internals/FFI+Format.swift b/Sources/K1/Support/FFI/Internals/FFI+Format.swift index fca91fd..5e3be3f 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Format.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Format.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Format extension K1.Format { @@ -18,7 +18,7 @@ extension K1.Format { // MARK: - K1.Format extension K1 { - /// Bridging type for: `secp256k1_ec_pubkey_serialize` + /// Bridging type for: `serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:)` (`secp256k1_ec_pubkey_serialize`) enum Format: UInt32, CaseIterable { case compressed, uncompressed } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index 38c3c15..1d8466b 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -1,5 +1,6 @@ import Foundation -import secp256k1 +import K1Macros +import Secp256k1 // MARK: - Raw enum Raw {} @@ -7,14 +8,14 @@ enum Raw {} extension Raw { static func recoverableSignature( _ rawRepresentation: some DataProtocol - ) throws -> secp256k1_ecdsa_recoverable_signature { + ) throws -> ECDSARecoverableSignatureRaw { let expected = K1.ECDSAWithKeyRecovery.Signature.Compact.byteCount guard rawRepresentation.count == expected else { throw K1.Error.incorrectParameterSize } - var raw = secp256k1_ecdsa_recoverable_signature() + var raw = ECDSARecoverableSignatureRaw() withUnsafeMutableBytes(of: &raw.data) { pointer in pointer.copyBytes( from: rawRepresentation.prefix(pointer.count) @@ -22,35 +23,38 @@ extension Raw { } return raw } +} - static func nonRecoverableSignature( - compactBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - var raw = secp256k1_ecdsa_signature() +// MARK: NonRecoverable Compact +extension Raw { + + @declareSafeApi(byteCount: 64) + static func __nonRecoverableSignature( + compactBytes: UnsafePointer + ) throws -> ECDSASignatureRaw { + var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in - secp256k1_ecdsa_signature_parse_compact( - context, - &raw, - compactBytes + parseEcdsaSignatureCompact( + context: context, + outputSignature: &raw, + inputBytes: compactBytes ) } return raw } +} +// MARK: NonRecoverable DER +extension Raw { static func nonRecoverableSignature( - derBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - var raw = secp256k1_ecdsa_signature() + derBytes: Span + ) throws -> ECDSASignatureRaw { + var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseDER) { context in - secp256k1_ecdsa_signature_parse_der( - context, - &raw, - derBytes, - derBytes.count - ) + parseEcdsaSignatureDER(context: context, outputSignature: &raw, input: derBytes) } return raw diff --git a/Sources/K1/Support/Misc/InlineArray+Data.swift b/Sources/K1/Support/Misc/InlineArray+Data.swift new file mode 100644 index 0000000..b3d0f48 --- /dev/null +++ b/Sources/K1/Support/Misc/InlineArray+Data.swift @@ -0,0 +1,26 @@ +import Foundation + +@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +extension InlineArray where Element == UInt8 { + init(hex: String) throws { + let data = try Data(hex: hex) + try self.init(data: data) + } + + init(data: Data) throws { + guard data.count == count else { + throw InlineArrayError.wrongLength(expected: count, got: data.count) + } + self = .init(repeating: 0) + data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in + for index in 0 ..< count { + self[index] = raw[index] + } + } + } +} + +@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +enum InlineArrayError: Error { + case wrongLength(expected: Int, got: Int) +} diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index 2b49130..5fd8d34 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -43,54 +43,52 @@ enum InternalFailure: UInt, Sendable, Swift.Error, Hashable { // MARK: - FFI.Error extension FFI { enum Error: Int, Sendable, Swift.Error, Hashable { - case failedToCreateContextForSecp256k1 - - //// `secp256k1_ecdsa_signature_parse_compact` failed + /// `parseEcdsaSignatureCompact(context:outputSignature:inputBytes:)` (`secp256k1_ecdsa_signature_parse_compact`) failed case ecdsaSignatureParseCompact - /// `secp256k1_ecdsa_signature_parse_der` failed + /// `parseEcdsaSignatureDER(context:outputSignature:input:inputlen:)` (`secp256k1_ecdsa_signature_parse_der`) failed case ecdsaSignatureParseDER - /// `secp256k1_ecdsa_signature_serialize_compact` failed + /// `serializeEcdsaSignatureCompact(context:outputBytes:signature:)` (`secp256k1_ecdsa_signature_serialize_compact`) failed case ecdsaSignatureSerializeCompact - /// `secp256k1_ecdsa_signature_serialize_der` failed + /// `serializeEcdsaSignatureDER(context:outputBytes:outputByteCount:signature:)` (`secp256k1_ecdsa_signature_serialize_der`) failed case ecdsaSignatureSerializeDER - /// `secp256k1_ecdsa_recoverable_signature_parse_compact` failed + /// `parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:)` (`secp256k1_ecdsa_recoverable_signature_parse_compact`) failed case recoverableSignatureParseCompact - /// `secp256k1_ecdsa_recoverable_signature_serialize_compact` failed + /// `serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:)` (`secp256k1_ecdsa_recoverable_signature_serialize_compact`) failed case recoverableSignatureSerializeCompact - /// `secp256k1_ecdsa_recoverable_signature_convert` failed + /// `ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:)` (`secp256k1_ecdsa_recoverable_signature_convert`) failed case recoverableSignatureConvert - /// `secp256k1_ecdsa_recover` failed + /// `recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:)` (`secp256k1_ecdsa_recover`) failed case recover - /// `secp256k1_ec_pubkey_parse` failed + /// `parsePublicKey(context:outputPublicKey:inputBytes:inputlen:)` (`secp256k1_ec_pubkey_parse`) failed case publicKeyParse - /// `secp256k1_ec_pubkey_serialize` failed + /// `serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:)` (`secp256k1_ec_pubkey_serialize`) failed case publicKeySerialize - /// `secp256k1_ecdh` failed + /// `ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:)` (`secp256k1_ecdh`) failed case ecdh - /// `secp256k1_ecdsa_sign_recoverable` or `secp256k1_ecdsa_sign` failed + /// `ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` (`secp256k1_ecdsa_sign_recoverable`) or `ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` (`secp256k1_ecdsa_sign`) failed case ecdsaSign - /// `secp256k1_xonly_pubkey_from_pubkey` failed + /// `xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:)` (`secp256k1_xonly_pubkey_from_pubkey`) failed case xonlyPublicKeyFromPublicKey - /// `secp256k1_keypair_create` failed + /// `keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:)` (`secp256k1_keypair_create`) failed case keypairCreate - /// `secp256k1_schnorrsig_sign32` failed + /// `schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:)` (`secp256k1_schnorrsig_sign32`) failed case schnorrSign - /// `secp256k1_ec_pubkey_create` + /// `createPublicKey(context:outputPublicKey:privateKeyBytes:)` (`secp256k1_ec_pubkey_create`) case publicKeyCreate /// Group operation (point addition, subtraction, etc.) failed @@ -129,8 +127,6 @@ extension K1.Error: CustomDebugStringConvertible { case .ecdh: return "ecdh" case .ecdsaSign: return "ECDSA sign" case .ecdsaSignatureParseCompact: return "ECDSA signature parse compact" - case .failedToCreateContextForSecp256k1: - return "create context" case .ecdsaSignatureParseDER: return "ECDSA signature parse DER" case .ecdsaSignatureSerializeCompact: diff --git a/Sources/K1/Support/Misc/Span+Data.swift b/Sources/K1/Support/Misc/Span+Data.swift new file mode 100644 index 0000000..611d481 --- /dev/null +++ b/Sources/K1/Support/Misc/Span+Data.swift @@ -0,0 +1,44 @@ +import Foundation + +// FIXME: These are temporary, and should be removed. We should upgrade whole code base to use Span/InlineArray as much as possible. + +// MARK: - Shared helpers +@usableFromInline +func withSpanFromArray( + _ bytes: [UInt8], + _ body: (Span) throws -> R +) rethrows -> R { + try bytes.withUnsafeBufferPointer { buf in + try body(Span(_unsafeElements: buf)) + } +} + +@usableFromInline +func withSpanFromData( + _ data: some DataProtocol, + _ body: (Span) throws -> R +) rethrows -> R { + if let contiguous = data as? any ContiguousBytes { + return try contiguous.withUnsafeBytes { raw in + let buf = raw.bindMemory(to: UInt8.self) + return try body(Span(_unsafeElements: buf)) + } + } + + var copy = [UInt8](data) + return try copy.withUnsafeMutableBufferPointer { buf in + let readonly = UnsafeBufferPointer(buf) + return try body(Span(_unsafeElements: readonly)) + } +} + +@usableFromInline +func withSpanFromContiguousBytes( + _ bytes: C, + _ body: (Span) throws -> R +) rethrows -> R { + try bytes.withUnsafeBytes { raw in + let buf = raw.bindMemory(to: UInt8.self) + return try body(Span(_unsafeElements: buf)) + } +} diff --git a/Sources/K1Macros/DeclareSafeApi.swift b/Sources/K1Macros/DeclareSafeApi.swift new file mode 100644 index 0000000..22e1eee --- /dev/null +++ b/Sources/K1Macros/DeclareSafeApi.swift @@ -0,0 +1,84 @@ +/// # Usage +/// Attach to a (meant to be private) function which accepts `UnsafePointer` in order to +/// generate four functions instead accepting: +/// * `InlineArray` +/// * `Span` +/// * `Data` +/// * `[UInt8]` +/// +/// ```swift +/// @declareSafeApi(byteCount: 64) +/// static func __nonRecoverableSignature( +/// compactBytes: UnsafePointer +/// ) throws -> ECDSASignatureRaw { ... } +/// ``` +/// +/// # Example +/// ```swift +/// @declareSafeApi(byteCount: 64) +/// static func __nonRecoverableSignature( +/// compactBytes: UnsafePointer +/// ) throws -> ECDSASignatureRaw { +/// var raw = ECDSASignatureRaw() +/// +/// try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in +/// parseEcdsaSignatureCompact( +/// context: context, +/// outputSignature: &raw, +/// inputBytes: compactBytes +/// ) +/// } +/// +/// return raw +/// } +/// ``` +/// +/// This macro will produce one private helper and four methods meant for +/// public/internal API: +/// +/// ```swift +/// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +/// static func nonRecoverableSignature( +/// compactBytes: InlineArray<64, UInt8> +/// ) throws -> ECDSASignatureRaw { +/// try Self.nonRecoverableSignature(compactBytes: compactBytes.span) +/// } +/// +/// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +/// static func nonRecoverableSignature( +/// compactBytes: Span +/// ) throws -> ECDSASignatureRaw { +/// try compactBytes.withUnsafeBytes { pointer in +/// try Self._nonRecoverableSignature(compactBytes: pointer) +/// } +/// } +/// +/// static func nonRecoverableSignature( +/// compactBytes: [UInt8] +/// ) throws -> ECDSASignatureRaw { +/// try Self._nonRecoverableSignature(compactBytes: compactBytes) +/// } +/// +/// static func nonRecoverableSignature( +/// compactBytes: Data +/// ) throws -> ECDSASignatureRaw { +/// try Self._nonRecoverableSignature(compactBytes: compactBytes) +/// } +/// +/// private static func _nonRecoverableSignature( +/// compactBytes: some ContiguousBytes +/// ) throws -> ECDSASignatureRaw { +/// try compactBytes.withUnsafeBytes { pointer in +/// guard pointer.count == 64 else { +/// throw K1.Error.incorrectParameterSize +/// } +/// let bytesPointer = pointer.bindMemory(to: UInt8.self) +/// guard let base = bytesPointer.baseAddress else { +/// throw K1.Error.invalidParameter +/// } +/// return try __nonRecoverableSignature(compactBytes: base) +/// } +/// } +/// ``` +@attached(peer, names: arbitrary) +public macro declareSafeApi(byteCount: Int) = #externalMacro(module: "K1MacrosImpl", type: "DeclareSafeApiMacro") diff --git a/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift b/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift new file mode 100644 index 0000000..970fe3b --- /dev/null +++ b/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift @@ -0,0 +1,328 @@ +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct DeclareSafeApiMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { + throw DeclareSafeApiMacroError.message("@declareSafeApi can only be attached to a function.") + } + + let byteCount = try parseByteCount(from: node) + guard byteCount > 0 else { + throw DeclareSafeApiMacroError.message("byteCount must be >= 1.") + } + + if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil { + throw DeclareSafeApiMacroError.message("@declareSafeApi does not support async functions.") + } + + guard let body = funcDecl.body else { + throw DeclareSafeApiMacroError.message("@declareSafeApi requires a function body.") + } + + let parameters = funcDecl.signature.parameterClause.parameters + guard parameters.count == 1, let param = parameters.first else { + throw DeclareSafeApiMacroError.message("@declareSafeApi currently supports exactly one parameter.") + } + + guard isUnsafePointerToUInt8(param.type) else { + throw DeclareSafeApiMacroError.message( + "Parameter must be UnsafePointer." + ) + } + + let originalName = funcDecl.name.text + let hasUnsafePrefix = originalName.hasPrefix("__") + let publicName = hasUnsafePrefix ? String(originalName.dropFirst(2)) : originalName + let unsafeName = hasUnsafePrefix ? originalName : "__\(originalName)" + let internalName = param.secondName?.text ?? (param.firstName.text == "_" ? "bytes" : param.firstName.text) + let paramDecl = parameterNameClause(firstName: param.firstName.text, secondName: param.secondName?.text, fallback: internalName) + let callArgString = callArgument(label: param.firstName.text, expr: internalName) + + let effectText = trimmed(funcDecl.signature.effectSpecifiers?.throwsClause?.description ?? "") + let returnText = trimmed(funcDecl.signature.returnClause?.description ?? "") + let signatureSuffix = [effectText, returnText].filter { !$0.isEmpty }.joined(separator: " ") + let signatureSuffixWithSpace = signatureSuffix.isEmpty ? "" : " \(signatureSuffix)" + let hasReturn = funcDecl.signature.returnClause != nil + + let tryPrefix = effectText.isEmpty ? "" : "try " + let returnPrefix = hasReturn ? "return " : "" + + let accessModifier = firstAccessModifier(in: funcDecl.modifiers) + if accessModifier == "private" || accessModifier == "fileprivate" { + throw DeclareSafeApiMacroError.message( + "@declareSafeApi cannot be applied to private/fileprivate functions. Use internal/public/open so the generated safe API is visible." + ) + } + let nonAccessModifiers = nonAccessModifiers(in: funcDecl.modifiers) + let wrapperModifiers = modifiersText(access: accessModifier, others: nonAccessModifiers) + let privateModifiers = modifiersText(access: "private", others: nonAccessModifiers) + + let filteredAttributes = AttributeListSyntax( + funcDecl.attributes.filter { element in + guard case .attribute(let attr) = element else { return true } + let nameText = trimmed(attr.attributeName.description) + let simpleName = nameText.split(separator: ".").last.map(String.init) ?? nameText + return simpleName != "declareSafeApi" + }.map { $0 } + ) + let attributesText = trimmed(filteredAttributes.description) + + var unsafeDecl: DeclSyntax? + if !hasUnsafePrefix { + let unsafeSignature = trimmed(funcDecl.signature.description) + let unsafeDeclText = """ + \(attributesText.isEmpty ? "" : attributesText + "\n")\ + \(privateModifiers)func \(unsafeName)\(unsafeSignature) \(body.description) + """ + unsafeDecl = "\(raw: unsafeDeclText)" + } + + let helperBodyLine = callLine( + expr: "\(unsafeName)(\(callArgument(label: param.firstName.text, expr: "base")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let helperBody = """ + \(returnPrefix)\(tryPrefix)\(internalName).withUnsafeBytes { pointer in + \tguard pointer.count == \(byteCount) else { + \t\tthrow K1.Error.incorrectParameterSize + \t} + \tlet bytesPointer = pointer.bindMemory(to: UInt8.self) + \tguard let base = bytesPointer.baseAddress else { + \t\tthrow K1.Error.invalidParameter + \t} + \t\(helperBodyLine) + } + """ + + let helperDecl: DeclSyntax = """ + \(raw: privateModifiers)func _\(raw: publicName)( + \(raw: paramDecl): some ContiguousBytes + )\(raw: signatureSuffixWithSpace) { + \(raw: indent(helperBody, by: 1)) + } + """ + + let availableAttribute = + "@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)" + + let inlineArrayBodyLine = callLine( + expr: "\(publicName)(\(callArgument(label: param.firstName.text, expr: "\(internalName).span")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let byteCountText = String(byteCount) + let inlineArrayDecl: DeclSyntax = """ + \(raw: availableAttribute) + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): InlineArray<\(raw: byteCountText), UInt8> + )\(raw: signatureSuffixWithSpace) { + \t\(raw: inlineArrayBodyLine) + } + """ + + let spanBodyLine = callLine( + expr: "_\(publicName)(\(callArgument(label: param.firstName.text, expr: "pointer")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let spanCallLine = """ + \(returnPrefix)\(tryPrefix)\(internalName).withUnsafeBytes { pointer in + \t\(spanBodyLine) + } + """ + let spanDecl: DeclSyntax = """ + \(raw: availableAttribute) + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): Span + )\(raw: signatureSuffixWithSpace) { + \(raw: indent(spanCallLine, by: 1)) + } + """ + + let arrayBodyLine = callLine( + expr: "_\(publicName)(\(callArgString))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let arrayDecl: DeclSyntax = """ + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): [UInt8] + )\(raw: signatureSuffixWithSpace) { + \t\(raw: arrayBodyLine) + } + """ + + let dataBodyLine = callLine( + expr: "_\(publicName)(\(callArgString))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let dataDecl: DeclSyntax = """ + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): Data + )\(raw: signatureSuffixWithSpace) { + \t\(raw: dataBodyLine) + } + """ + + var decls: [DeclSyntax] = [] + if let unsafeDecl { decls.append(unsafeDecl) } + decls.append(contentsOf: [ + helperDecl, + inlineArrayDecl, + spanDecl, + arrayDecl, + dataDecl, + ]) + return decls + } +} + +// MARK: - Parsing helpers +private func parseByteCount(from node: AttributeSyntax) throws -> Int { + guard case .argumentList(let args) = node.arguments else { + throw DeclareSafeApiMacroError.message("Expected arguments: @declareSafeApi(byteCount: 64)") + } + + guard let byteArg = args.first(where: { $0.label?.text == "byteCount" }) else { + throw DeclareSafeApiMacroError.message("Missing byteCount: argument.") + } + + guard let intLit = byteArg.expression.as(IntegerLiteralExprSyntax.self), + let value = Int(intLit.literal.text) + else { + throw DeclareSafeApiMacroError.message("byteCount must be an integer literal.") + } + + return value +} + +private func isUnsafePointerToUInt8(_ type: TypeSyntax) -> Bool { + let baseType: TypeSyntax + if let opt = type.as(OptionalTypeSyntax.self) { + baseType = opt.wrappedType + } else if let opt = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { + baseType = opt.wrappedType + } else { + baseType = type + } + + if let ident = baseType.as(IdentifierTypeSyntax.self), + ident.name.text == "UnsafePointer", + isUInt8Generic(ident.genericArgumentClause) + { + return true + } + + if let member = baseType.as(MemberTypeSyntax.self), + member.name.text == "UnsafePointer", + isUInt8Generic(member.genericArgumentClause) + { + return true + } + + return false +} + +private func isUInt8Generic(_ clause: GenericArgumentClauseSyntax?) -> Bool { + guard let clause, clause.arguments.count == 1, + let arg = clause.arguments.first + else { return false } + + switch arg.argument { + case .type(let typeArg): + if let ident = typeArg.as(IdentifierTypeSyntax.self), ident.name.text == "UInt8" { + return true + } + if let member = typeArg.as(MemberTypeSyntax.self), member.name.text == "UInt8" { + return true + } + return false + case .expr: + return false + } +} + +private func parameterNameClause(firstName: String, secondName: String?, fallback: String) -> String { + if let secondName { + return "\(firstName) \(secondName)" + } + if firstName == "_" { + return "_ \(fallback)" + } + return firstName +} + +private func callArgument(label: String, expr: String) -> String { + label == "_" ? expr : "\(label): \(expr)" +} + +private func modifiersText(access: String?, others: [String]) -> String { + var parts: [String] = [] + if let access, !access.isEmpty { parts.append(access) } + parts.append(contentsOf: others.filter { !$0.isEmpty }) + if parts.isEmpty { return "" } + return parts.joined(separator: " ") + " " +} + +private func firstAccessModifier(in modifiers: DeclModifierListSyntax) -> String? { + for modifier in modifiers { + let text = trimmed(modifier.name.text) + if isAccessModifier(text) { + return text + } + } + return nil +} + +private func nonAccessModifiers(in modifiers: DeclModifierListSyntax) -> [String] { + modifiers.compactMap { modifier in + let text = trimmed(modifier.description) + return isAccessModifier(text) ? nil : text + } +} + +private func isAccessModifier(_ text: String) -> Bool { + switch text { + case "public", "internal", "fileprivate", "private", "open": + return true + default: + return false + } +} + +private func callLine(expr: String, tryPrefix: String, hasReturn: Bool) -> String { + let call = "\(tryPrefix)\(expr)" + return hasReturn ? "return \(call)" : call +} + +private func trimmed(_ string: String) -> String { + string.trimmingCharacters(in: .whitespacesAndNewlines) +} + +private func indent(_ text: String, by level: Int) -> String { + let prefix = String(repeating: "\t", count: level) + return text + .split(separator: "\n", omittingEmptySubsequences: false) + .map { prefix + $0 } + .joined(separator: "\n") +} + +// MARK: - Errors +private enum DeclareSafeApiMacroError: Error, CustomStringConvertible { + case message(String) + + var description: String { + switch self { + case let .message(message): return message + } + } +} diff --git a/Sources/K1MacrosImpl/K1MacrosPlugin.swift b/Sources/K1MacrosImpl/K1MacrosPlugin.swift new file mode 100644 index 0000000..0b904d7 --- /dev/null +++ b/Sources/K1MacrosImpl/K1MacrosPlugin.swift @@ -0,0 +1,7 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct MyMacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [DeclareSafeApiMacro.self] +} diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes new file mode 100644 index 0000000..d4df9ac --- /dev/null +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -0,0 +1,414 @@ +Name: Secp256k1 + +Tags: +- Name: secp256k1_ecdsa_signature + SwiftName: ECDSASignatureRaw + +- Name: secp256k1_ecdsa_recoverable_signature + SwiftName: ECDSARecoverableSignatureRaw + +- Name: secp256k1_keypair + SwiftName: KeypairRaw + +- Name: secp256k1_pubkey + SwiftName: PublicKeyRaw + +- Name: secp256k1_xonly_pubkey + SwiftName: PublicKeyXOnlyRaw + +- Name: secp256k1_result + SwiftName: ResultRaw + EnumExtensibility: closed + +- Name: secp256k1_pubkey_cmp_result + SwiftName: ComparisonOutcomeRaw + EnumExtensibility: closed + +- Name: secp256k1_normalize_sig_result + SwiftName: NormalizeSignatureOutcomeRaw + EnumExtensibility: closed + +- Name: secp256k1_verify_sig_result + SwiftName: VerifySignatureOutcomeRaw + EnumExtensibility: closed + +Functions: +- Name: secp256k1_ecdsa_recoverable_signature_convert + SwiftName: ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # sigin + Nullability: N + +- Name: secp256k1_ecdsa_recoverable_signature_serialize_compact + SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + - Position: 2 # recid + Nullability: N + - Position: 3 # sig + Nullability: N + +- Name: secp256k1_ecdsa_recoverable_signature_parse_compact + SwiftName: parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + - Position: 3 # recid + Nullability: N + +- Name: secp256k1_ecdsa_recover + SwiftName: recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # sig + Nullability: N + - Position: 3 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + +- Name: secp256k1_ecdsa_sign + SwiftName: ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig / outputSignature + Nullability: N + - Position: 2 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 4 # noncefp + Nullability: O + - Position: 5 # ndata + Nullability: O + +- Name: secp256k1_ecdsa_sign_recoverable + SwiftName: ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig / outputSignature + Nullability: N + - Position: 2 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 4 # noncefp + Nullability: O + - Position: 5 # ndata + Nullability: O + +- Name: secp256k1_schnorrsig_sign32 + SwiftName: schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # sig64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + - Position: 2 # msg32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 3 # keypair + Nullability: N + - Position: 4 # aux_rand32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: O + +- Name: secp256k1_keypair_create + SwiftName: keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # keypair output (secp256k1_keypair) + Nullability: N + - Position: 2 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + +- Name: secp256k1_schnorrsig_verify + SwiftName: verifySchnorrSignature(context:signatureBytes:msg:msglen:xOnlyPublicKey:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # sig64 + Nullability: N + - Position: 2 # msg + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "msglen" + Nullability: N + - Position: 3 # msglen + Nullability: N + - Position: 4 # pubkey (secp256k1_xonly_pubkey) + Nullability: N + +- Name: secp256k1_xonly_pubkey_from_pubkey + SwiftName: xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:) + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # output (secp256k1_xonly_pubkey) + Nullability: N + - Position: 2 # Parity (int) + Nullability: O + - Position: 3 # publicKey (secp256k1_pubkey) + Nullability: N + +- Name: secp256k1_ecdh + SwiftName: ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # pubkey + Nullability: N + - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 4 # hashfp + Nullability: O + - Position: 5 # data + Nullability: O + +# Context management +- Name: secp256k1_context_create + SwiftName: createContext(flags:) + Parameters: + - Position: 0 # flags + Nullability: N + +- Name: secp256k1_context_destroy + SwiftName: destroyContext(_:) + Parameters: + - Position: 0 # ctx + Nullability: N + +# Secret keys and pubkeys +- Name: secp256k1_ec_pubkey_create + SwiftName: createPublicKey(context:outputPublicKey:privateKeyBytes:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + +- Name: secp256k1_ec_pubkey_parse + SwiftName: parsePublicKey(context:outputPublicKey:inputBytes:inputlen:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # input + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "inputlen" + Nullability: N + - Position: 3 # inputlen + Nullability: N + +- Name: secp256k1_ec_pubkey_serialize + SwiftName: serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # outputlen + Nullability: N + - Position: 3 # pubkey + Nullability: N + - Position: 4 # flags + Nullability: N + +- Name: secp256k1_ec_pubkey_cmp_result + SwiftName: comparePublicKeys(context:lhs:rhs:) + ResultType: secp256k1_pubkey_cmp_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey1 + Nullability: N + - Position: 2 # pubkey2 + Nullability: N + +- Name: secp256k1_ec_pubkey_negate + SwiftName: negatePublicKey(context:publicKey:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + +- Name: secp256k1_ec_pubkey_combine + SwiftName: combinePublicKeys(context:outputPublicKey:inputs:inputCount:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # out + Nullability: N + - Position: 2 # ins + Nullability: N + - Position: 3 # n + Nullability: N + +- Name: secp256k1_ec_pubkey_sort + SwiftName: sortPublicKeys(context:pubkeys:count:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkeys + Nullability: N + - Position: 2 # len + Nullability: N + +# ECDSA signature parsing/serialization +- Name: secp256k1_ecdsa_signature_parse_compact + SwiftName: parseEcdsaSignatureCompact(context:outputSignature:inputBytes:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + +- Name: secp256k1_ecdsa_signature_parse_der + SwiftName: parseEcdsaSignatureDER(context:outputSignature:input:inputlen:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "inputlen" + Nullability: N + - Position: 3 # inputlen + Nullability: N + +- Name: secp256k1_ecdsa_signature_serialize_der + SwiftName: serializeEcdsaSignatureDER(context:outputBytes:outputByteCount:signature:) + ResultType: secp256k1_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # outputlen + Nullability: N + - Position: 3 # sig + Nullability: N + +- Name: secp256k1_ecdsa_signature_serialize_compact + SwiftName: serializeEcdsaSignatureCompact(context:outputBytes:signature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + - Position: 2 # sig + Nullability: N + +# ECDSA verify/normalize +- Name: secp256k1_ecdsa_signature_normalize + SwiftName: normalizeEcdsaSignature(context:outputSignature:inputSignature:) + ResultType: secp256k1_normalize_sig_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sigout + Nullability: N + - Position: 2 # sigin + Nullability: N + +- Name: secp256k1_ecdsa_verify + SwiftName: verifyEcdsaSignature(context:signature:messageHash:publicKey:) + ResultType: secp256k1_verify_sig_result + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # msg32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 3 # pubkey + Nullability: N diff --git a/Sources/secp256k1/include/secp256k1.h b/Sources/secp256k1/include/k1_umbrella.h similarity index 84% rename from Sources/secp256k1/include/secp256k1.h rename to Sources/secp256k1/include/k1_umbrella.h index 1d6d15a..9a1e223 100644 --- a/Sources/secp256k1/include/secp256k1.h +++ b/Sources/secp256k1/include/k1_umbrella.h @@ -1,8 +1,11 @@ +#include "../src/k1_shim.h" + +#include "../libsecp256k1/include/secp256k1.h" #include "../libsecp256k1/include/secp256k1_ecdh.h" #include "../libsecp256k1/include/secp256k1_extrakeys.h" #include "../libsecp256k1/include/secp256k1_preallocated.h" #include "../libsecp256k1/include/secp256k1_recovery.h" #include "../libsecp256k1/include/secp256k1_schnorrsig.h" -#include "../libsecp256k1/include/secp256k1.h" #include "../src/ecdh_variants.h" +#include "../src/apinotes_tutorial.h" diff --git a/Sources/secp256k1/include/module.modulemap b/Sources/secp256k1/include/module.modulemap new file mode 100644 index 0000000..4dc9902 --- /dev/null +++ b/Sources/secp256k1/include/module.modulemap @@ -0,0 +1,5 @@ +module Secp256k1 { + umbrella header "k1_umbrella.h" + export * + module * { export * } +} diff --git a/Sources/secp256k1/src/apinotes_tutorial.c b/Sources/secp256k1/src/apinotes_tutorial.c new file mode 100644 index 0000000..b3fdf85 --- /dev/null +++ b/Sources/secp256k1/src/apinotes_tutorial.c @@ -0,0 +1,34 @@ +// +// apinotes_tutorial.c +// K1 +// +// Created by Alexander Cyon on 2026-02-01. +// + +#include "./apinotes_tutorial.h" +#include "./ecdh_variants.h" +#include + +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +) { + return _ecdh_hash_function_asn1_x963_impl( + output, + x32 + ); +} + +void fill_with_fives( + unsigned char *buf, + int len +) { + memset(buf, 5, len); +} + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +) { + memcpy(destination, source, 3); +} diff --git a/Sources/secp256k1/src/apinotes_tutorial.h b/Sources/secp256k1/src/apinotes_tutorial.h new file mode 100644 index 0000000..2837646 --- /dev/null +++ b/Sources/secp256k1/src/apinotes_tutorial.h @@ -0,0 +1,31 @@ +#ifndef apinotes_tutorial_h +#define apinotes_tutorial_h + +#include "../libsecp256k1/include/secp256k1_ecdh.h" +/** + The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). + + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + */ +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +); + +void fill_with_fives( + unsigned char *buf, + int len +); + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +); + + +#endif /* apinotes_tutorial_h */ diff --git a/Sources/secp256k1/src/ecdh_variants.c b/Sources/secp256k1/src/ecdh_variants.c index 6b4df1e..ef70b06 100644 --- a/Sources/secp256k1/src/ecdh_variants.c +++ b/Sources/secp256k1/src/ecdh_variants.c @@ -8,7 +8,15 @@ #include "./ecdh_variants.h" #include -int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { +int _ecdh_hash_function_asn1_x963_impl( + unsigned char *output, + const unsigned char *x32 +) { + memcpy(output, x32, 32); + return 1; +} + +int ecdh_hash_function_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { (void)data; /* Save x and y as uncompressed public key */ output[0] = 0x04; @@ -17,8 +25,16 @@ int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, con return 1; } -int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { - (void)data; - memcpy(output, x32, 32); - return 1; +int ecdh_hash_function_asn1_x963( + unsigned char *output, + const unsigned char *x32, + const unsigned char *y32, + void *data +) { + (void)y32; + (void)data; + return _ecdh_hash_function_asn1_x963_impl( + output, + x32 + ); } diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h index 32c5d26..756c0a0 100644 --- a/Sources/secp256k1/src/ecdh_variants.h +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -1,10 +1,3 @@ -// -// ecdh_variants.h -// -// -// Created by Alexander Cyon on 2022-01-31. -// - #ifndef ecdh_variants_h #define ecdh_variants_h @@ -21,7 +14,7 @@ * y32: pointer to a 32-byte y coordinate * data: arbitrary data pointer that is passed through */ -int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); +int ecdh_hash_function_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); /** The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). * @@ -34,6 +27,11 @@ int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, con * y32: pointer to a 32-byte y coordinate * data: arbitrary data pointer that is passed through */ -int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); +int ecdh_hash_function_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); + +int _ecdh_hash_function_asn1_x963_impl( + unsigned char *output, + const unsigned char *x32 +); #endif /* ecdh_variants_h */ diff --git a/Sources/secp256k1/src/k1_shim.c b/Sources/secp256k1/src/k1_shim.c new file mode 100644 index 0000000..ce9c558 --- /dev/null +++ b/Sources/secp256k1/src/k1_shim.c @@ -0,0 +1,15 @@ +#include "./k1_shim.h" +#include "../libsecp256k1/include/secp256k1.h" +#include + +//secp256k1_pubkey_cmp_result +//secp256k1_ec_pubkey_cmp_result( +// const secp256k1_context *ctx, +// const secp256k1_pubkey *lhs, +// const secp256k1_pubkey *rhs +//) { +// int r = secp256k1_ec_pubkey_cmp(ctx, lhs, rhs); +// return r < 0 ? SECP256K1_PUBKEY_CMP_RHS_IS_GREATER +// : r > 0 ? SECP256K1_PUBKEY_CMP_LHS_IS_GREATER +// : SECP256K1_PUBKEY_CMP_EQUAL; +//} diff --git a/Sources/secp256k1/src/k1_shim.h b/Sources/secp256k1/src/k1_shim.h new file mode 100644 index 0000000..2ea9982 --- /dev/null +++ b/Sources/secp256k1/src/k1_shim.h @@ -0,0 +1,40 @@ +#ifndef k1_shim_h +#define k1_shim_h + +typedef enum secp256k1_result { + SECP256K1_RESULT_SUCCESS = 1, + SECP256K1_RESULT_FAILURE = 0, +} secp256k1_result; + +typedef enum secp256k1_pubkey_cmp_result { + SECP256K1_PUBKEY_CMP_EQUAL = 0, + SECP256K1_PUBKEY_CMP_LHS_IS_GREATER = 1, + SECP256K1_PUBKEY_CMP_RHS_IS_GREATER = -1, +} secp256k1_pubkey_cmp_result; + +typedef enum secp256k1_normalize_sig_result { + SECP256K1_NORMALIZE_SIG_ALREADY_NORMALIZED = 0, + SECP256K1_NORMALIZE_SIG_WASNT_NORMALIZED = 1, +} secp256k1_normalize_sig_result; + + +typedef enum secp256k1_verify_sig_result { + SECP256K1_VERIFY_SIG_UNPARSABLE_OR_INCORRECT = 0, + SECP256K1_VERIFY_SIG_CORRECT = 1, +} secp256k1_verify_sig_result; + +#include "../libsecp256k1/include/secp256k1.h" + +static inline secp256k1_pubkey_cmp_result +secp256k1_ec_pubkey_cmp_result( + const secp256k1_context *ctx, + const secp256k1_pubkey *lhs, + const secp256k1_pubkey *rhs +) { + int r = secp256k1_ec_pubkey_cmp(ctx, lhs, rhs); + return r < 0 ? SECP256K1_PUBKEY_CMP_RHS_IS_GREATER + : r > 0 ? SECP256K1_PUBKEY_CMP_LHS_IS_GREATER + : SECP256K1_PUBKEY_CMP_EQUAL; +} + +#endif /* k1_shim_h */ diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift index 23ea4ee..24775b1 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift @@ -35,4 +35,14 @@ final class ECDSASignatureTests: XCTestCase { signatures.insert(signature) } } + + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + func test_ecdsa_from_inline_array() throws { + let data = try Data(hex: "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8") + let inlineArray = try InlineArray<64, UInt8>(data: data) + let signature = try K1.ECDSA.Signature(rawRepresentation: inlineArray) + XCTAssertEqual(signature.rawRepresentation, data) + + } + } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift index 6fb5674..f038691 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift @@ -52,7 +52,7 @@ private extension XCTestCase { let privateKeyRecoverable = try K1.ECDSAWithKeyRecovery.PrivateKey(rawRepresentation: privateKey.rawRepresentation) let signatureRecoverableFromMessage = try privateKeyRecoverable.signature(for: messageDigest) - try XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) + XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) let recid = try signatureRecoverableFromMessage.compact().recoveryID XCTAssertEqual( diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift index 7198c88..17e0109 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift @@ -63,7 +63,7 @@ final class PublicKeyGroupOperationsTests: XCTestCase { ] let expectedGenerator = try K1.Schnorr.PublicKey(rawRepresentation: generatorPoint) - let actualGeneratorRaw = try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.g, format: .uncompressed) + let actualGeneratorRaw = FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.g, format: .uncompressed) let actualGenerator = try K1.Schnorr.PublicKey(rawRepresentation: actualGeneratorRaw.dropFirst()) // Drop the 0x04 prefix XCTAssertEqual(expectedGenerator.rawRepresentation, actualGenerator.rawRepresentation) @@ -84,11 +84,11 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let priv5 = try FFI.PrivateKey.Wrapped(scalar: 5) let priv6 = try FFI.PrivateKey.Wrapped(scalar: 6) - XCTAssertTrue(try gx2.compare(to: priv2.publicKey)) - XCTAssertTrue(try gx3.compare(to: priv3.publicKey)) - XCTAssertTrue(try gx4.compare(to: priv4.publicKey)) - XCTAssertTrue(try gx5.compare(to: priv5.publicKey)) - XCTAssertTrue(try gx6.compare(to: priv6.publicKey)) + XCTAssertTrue(gx2.isEqual(to: priv2.publicKey)) + XCTAssertTrue(gx3.isEqual(to: priv3.publicKey)) + XCTAssertTrue(gx4.isEqual(to: priv4.publicKey)) + XCTAssertTrue(gx5.isEqual(to: priv5.publicKey)) + XCTAssertTrue(gx6.isEqual(to: priv6.publicKey)) } func testBasicAddition() throws { @@ -98,27 +98,27 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test that we can add two points let sum = try a + b - let sumCompressed = try FFI.PublicKey.serialize(sum, format: .compressed) + let sumCompressed = FFI.PublicKey.serialize(sum, format: .compressed) // Verify the result is a valid point (33 bytes for compressed) XCTAssertEqual(sumCompressed.count, 33) // Verify the result is different from both inputs - XCTAssertFalse(try sum.compare(to: a)) - XCTAssertFalse(try sum.compare(to: b)) + XCTAssertFalse(sum.isEqual(to: a)) + XCTAssertFalse(sum.isEqual(to: b)) } - func testNegation() throws { + func testNegation() { // Test that negation works correctly let a = FFI.PublicKey.Wrapped.gx2 - let negA = try a.negate() - let negNegA = try negA.negate() + let negA = a.negate() + let negNegA = negA.negate() // Test that -(-a) = a - XCTAssertTrue(try negNegA.compare(to: a)) - + XCTAssertTrue(negNegA.isEqual(to: a)) + // Test that a != -a (unless a is the point at infinity, which gx2 is not) - XCTAssertFalse(try a.compare(to: negA)) + XCTAssertFalse(a.isEqual(to: negA)) } func testSubtraction() throws { @@ -129,12 +129,12 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test that g5 - g3 = g2 let g5MinusG3 = try g5 - g3 - XCTAssertTrue(try g5MinusG3.compare(to: g2), "g5 - g3 should equal g2") - + XCTAssertTrue(g5MinusG3.isEqual(to: g2), "g5 - g3 should equal g2") + // Test that g3 - g2 = g let g3MinusG2 = try g3 - g2 - XCTAssertTrue(try g3MinusG2.compare(to: FFI.PublicKey.Wrapped.g), "g3 - g2 should equal g") - + XCTAssertTrue(g3MinusG2.isEqual(to: FFI.PublicKey.Wrapped.g), "g3 - g2 should equal g") + // Test that g2 - g2 throws error (point at infinity) XCTAssertThrowsError(try g2 - g2) { error in if let ffiError = error as? FFI.Error { @@ -152,12 +152,12 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test sum([g, g2, g3]) = g6 let sum = try FFI.PublicKey.Wrapped.sum(keys: [g, g2, g3]) - XCTAssertTrue(try sum.compare(to: g6), "sum([g, g2, g3]) should equal g6") - + XCTAssertTrue(sum.isEqual(to: g6), "sum([g, g2, g3]) should equal g6") + // Test sum with single key let sumSingle = try FFI.PublicKey.Wrapped.sum(keys: [g3]) - XCTAssertTrue(try sumSingle.compare(to: g3), "sum([g3]) should equal g3") - + XCTAssertTrue(sumSingle.isEqual(to: g3), "sum([g3]) should equal g3") + // Test that empty array throws error XCTAssertThrowsError(try FFI.PublicKey.Wrapped.sum(keys: [])) { error in if let k1Error = error as? K1.Error { @@ -171,7 +171,7 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let g = FFI.PublicKey.Wrapped.g let gx2 = FFI.PublicKey.Wrapped.gx2 let gPlusG = try g + g - XCTAssertTrue(try gPlusG.compare(to: gx2), "g + g should equal gx2 (2*G) on secp256k1") + XCTAssertTrue(gPlusG.isEqual(to: gx2), "g + g should equal gx2 (2*G) on secp256k1") } func testGroupAdditionWithG2G3G4() throws { @@ -182,27 +182,27 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // g2 + g3 (combine) should equal g5 (scalar multiplication) let g2PlusG3 = try g2 + g3 - XCTAssertTrue(try g2PlusG3.compare(to: g5), "g2 + g3 (combine) should equal g5 (scalar multiplication)") + XCTAssertTrue(g2PlusG3.isEqual(to: g5), "g2 + g3 (combine) should equal g5 (scalar multiplication)") // Also test that sum([g2, g3]) == g2 + g3 let sum1 = try FFI.PublicKey.Wrapped.sum(keys: [g2, g3]) - XCTAssertTrue(try sum1.compare(to: g2PlusG3)) + XCTAssertTrue(sum1.isEqual(to: g2PlusG3)) } func testGroupNegationWithG2G3G4() throws { // Test negation operations // Test: -g2 should be different from g2 - let negG2 = try FFI.PublicKey.Wrapped.gx2.negate() - XCTAssertFalse(try negG2.compare(to: FFI.PublicKey.Wrapped.gx2)) - + let negG2 = FFI.PublicKey.Wrapped.gx2.negate() + XCTAssertFalse(negG2.isEqual(to: FFI.PublicKey.Wrapped.gx2)) + // Test: -g3 should be different from g3 - let negG3 = try FFI.PublicKey.Wrapped.gx3.negate() - XCTAssertFalse(try negG3.compare(to: FFI.PublicKey.Wrapped.gx3)) - + let negG3 = FFI.PublicKey.Wrapped.gx3.negate() + XCTAssertFalse(negG3.isEqual(to: FFI.PublicKey.Wrapped.gx3)) + // Test: -g4 should be different from g4 - let negG4 = try FFI.PublicKey.Wrapped.gx4.negate() - XCTAssertFalse(try negG4.compare(to: FFI.PublicKey.Wrapped.gx4)) + let negG4 = FFI.PublicKey.Wrapped.gx4.negate() + XCTAssertFalse(negG4.isEqual(to: FFI.PublicKey.Wrapped.gx4)) // Test: g2 + (-g2) = 0 (point at infinity) // This should throw an error because the point at infinity cannot be represented as a valid public key @@ -223,22 +223,31 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let schnorrSum = try schnorrG2 + schnorrG3 // g2 + g3 should equal g5 - XCTAssertEqual(schnorrSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) - + XCTAssertEqual( + schnorrSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) + // Test ECDSA public keys let ecdsaG2 = try K1.ECDSA.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx2, format: .compressed)) let ecdsaG3 = try K1.ECDSA.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx3, format: .compressed)) let ecdsaSum = try ecdsaG2 + ecdsaG3 // g2 + g3 should equal g5 - XCTAssertEqual(ecdsaSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) - + XCTAssertEqual( + ecdsaSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) + // Test KeyAgreement public keys let keyAgreementG2 = try K1.KeyAgreement.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx2, format: .compressed)) let keyAgreementG3 = try K1.KeyAgreement.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx3, format: .compressed)) let keyAgreementSum = try keyAgreementG2 + keyAgreementG3 // g2 + g3 should equal g5 - XCTAssertEqual(keyAgreementSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) + XCTAssertEqual( + keyAgreementSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) } } diff --git a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift index 0fc167a..87509e2 100644 --- a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift +++ b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift @@ -1,6 +1,9 @@ import Foundation @testable import K1 import XCTest +import K1Macros +import Secp256k1 + // MARK: - ECDASignaturePublicKeyRecoveryTests /// Test vectors: @@ -49,7 +52,7 @@ final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { let nonRecoverable = try K1.ECDSA.Signature(rawRepresentation: compactRecoverableSig.compact) - try XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) + XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) let nonRecovDer = nonRecoverable.derRepresentation let nonRecoveryDERHex = "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" XCTAssertEqual(nonRecovDer.hex, nonRecoveryDERHex) diff --git a/Tests/K1Tests/Util/Wycheproof.swift b/Tests/K1Tests/Util/Wycheproof.swift index 25ce3b6..f40ea32 100644 --- a/Tests/K1Tests/Util/Wycheproof.swift +++ b/Tests/K1Tests/Util/Wycheproof.swift @@ -82,7 +82,7 @@ extension XCTestCase { hashFunction: HF.Type, skipIfContainsFlags: [String] = [], skipIfContainsComment: [String] = [], - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> ResultOfTestGroup { guard group.key.curve == "secp256k1" else { diff --git a/justfile b/justfile index 96823a9..6aaf861 100644 --- a/justfile +++ b/justfile @@ -3,12 +3,12 @@ ROOT_DIR := justfile_directory() default: testdebug testdebug: - swift test + swift test --enable-experimental-prebuilts test: clean testdebug clean testprod testprod: - swift test -c release -Xswiftc -enable-testing + swift test -c release -Xswiftc -enable-testing --enable-experimental-prebuilts rmsubmod: rm -rf "$(ROOT_DIR)Sources/secp256k1/libsecp256k1" @@ -32,6 +32,11 @@ bootstrap: dev: bootstrap init +synthesize-interface: + mkdir -p "{{ROOT_DIR}}/.build/clang-module-cache" + CLANG_MODULE_CACHE_PATH="{{ROOT_DIR}}/.build/clang-module-cache" xcrun swift-synthesize-interface -I Sources/secp256k1/include -module-name Secp256k1 -target arm64-apple-macos15 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.sdk + + format: swiftformat --config .swiftformat "{{ROOT_DIR}}"