From 94bc63ef68f572ef8052f409673dae0a0dce6f4c Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sat, 25 Mar 2023 15:53:41 +0100 Subject: [PATCH] Fix ci (#21) --- .github/workflows/swift.yml | 57 +- .swiftformat | 16 + Makefile | 47 +- Package.swift | 171 +++--- Sources/K1/K1.swift | 2 +- Sources/K1/K1/ECDH/KeyAgreement.swift | 210 ++++--- Sources/K1/K1/ECDSA/ECDSA.swift | 110 ++-- .../ECDSA/ECDSASignatureNonRecoverable.swift | 377 ++++++------ .../K1/ECDSA/ECDSASignatureRecoverable.swift | 571 +++++++++--------- Sources/K1/K1/ECDSA/RecoveryID.swift | 56 +- .../K1/K1/Keys/PrivateKey/PrivateKey.swift | 181 ------ .../PrivateKey/PrivateKeyImplementation.swift | 170 ++++++ .../PrivateKey/PrivateKeyOf+Feature.swift | 107 ++-- Sources/K1/K1/Keys/PublicKey/PublicKey.swift | 147 ----- .../PublicKey/PublicKeyImplementation.swift | 135 +++++ .../Keys/PublicKey/PublicKeyOf+Feature.swift | 98 ++- Sources/K1/K1/Schnorr/Schnorr+Signature.swift | 35 +- Sources/K1/K1/Schnorr/Schnorr.swift | 245 ++++---- .../Support/Extensions/Data+Extensions.swift | 22 +- .../K1/Support/FFI/API/ECDH/FFI+ECDH.swift | 159 +++-- .../K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift | 156 +++-- .../ECDSA+NonRecovery+Wrapped.swift | 38 +- .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 298 +++++---- .../Recovery/ECDSA+Recovery+Wrapped.swift | 37 +- .../ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 195 +++--- .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 148 +++-- .../API/Keys/PublicKey/FFI+PublicKey.swift | 182 +++--- .../Keys/PublicKey/PublicKey+Wrapped.swift | 59 +- .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 154 +++-- .../FFI/API/Schnorr/Schnorr+Wrapped.swift | 56 +- .../K1/Support/FFI/Internals/FFI+Call.swift | 122 ++-- .../Support/FFI/Internals/FFI+Context.swift | 48 +- .../K1/Support/FFI/Internals/FFI+Format.swift | 73 +-- .../K1/Support/FFI/Internals/FFI+Raw.swift | 117 ++-- Sources/K1/Support/Misc/Curve.swift | 18 +- Sources/K1/Support/Misc/K1+Error.swift | 98 ++- Sources/K1/Support/Misc/SharedSecret.swift | 37 +- Tests/K1Tests/TestCases/ECDH/ECDHTests.swift | 133 ++-- .../TestCases/ECDH/ECDHWycheproofTests.swift | 128 ++++ .../TestCases/ECDH/ECDHWychoproofTests.swift | 122 ---- .../ECDH/TwoVariantsOfECDHWithKDFTests.swift | 217 ++++--- ...SARecoverableSignatureRoundtripTests.swift | 22 +- .../TestCases/ECDSA/ECDSASignatureTests.swift | 73 +-- .../ECDSA/ECDSASignatureTrezorTests.swift | 175 +++--- ...cheproofASNDEREncodedSignaturesTests.swift | 94 +-- ...oofIEEEP1364RSEncodedSignaturesTests.swift | 89 ++- .../PrivateKey/PrivateKeyEncodingTests.swift | 83 ++- .../PrivateKeyGenerationTests.swift | 23 +- .../PrivateKey/PrivateKeyImportTests.swift | 103 ++-- .../PublicKey/PublicKeyEncodingTests.swift | 141 ++--- .../Keys/PublicKey/PublicKeyImportTests.swift | 123 ++-- .../Performance/PerformanceTests.swift | 156 +++-- .../ECDASignaturePublicKeyRecoveryTests.swift | 237 ++++---- .../SchnorrSignatureBitcoinCoreTests.swift | 258 ++++---- .../Schnorr/SchnorrSignatureTests.swift | 25 +- Tests/K1Tests/Util/ECSignature.swift | 12 +- Tests/K1Tests/Util/Wycheproof.swift | 353 +++++------ Tests/K1Tests/Util/XCTestUtils.swift | 26 +- scripts/bootstrap | 25 + scripts/build.sh | 11 - scripts/git-format-staged | 270 +++++++++ scripts/pre-commit | 5 + scripts/swiftformat-staged | 5 + 63 files changed, 3822 insertions(+), 3839 deletions(-) create mode 100644 .swiftformat delete mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift create mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift delete mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKey.swift create mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift create mode 100644 Tests/K1Tests/TestCases/ECDH/ECDHWycheproofTests.swift delete mode 100644 Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift create mode 100755 scripts/bootstrap delete mode 100755 scripts/build.sh create mode 100755 scripts/git-format-staged create mode 100755 scripts/pre-commit create mode 100755 scripts/swiftformat-staged diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 19d6cba..f617e23 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -1,28 +1,43 @@ name: CI on: - push: - branches: - - main - pull_request: - branches: - - '*' - workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - '*' + workflow_dispatch: concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: - library: - runs-on: macos-12 - strategy: - matrix: - xcode: ['13.4.1', '14.0.1'] - config: ['debug', 'release'] - steps: - - uses: actions/checkout@v3 - - name: Select Xcode ${{ matrix.xcode }} - run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - - name: Run ${{ matrix.config }} tests - run: CONFIG=${{ matrix.config }} make test-library \ No newline at end of file + linting: + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - name: "Run Lint" + run: | + brew update + brew upgrade swiftformat + swiftformat . --lint + + unittest: + runs-on: macos-12 + needs: + - linting + strategy: + matrix: + xcode: ['14.1'] + steps: + - uses: actions/checkout@v3 + - name: "Select Xcode ${{ matrix.xcode }}" + run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: "Clean and init submodules" + run: make init + - name: "Run test in debug" + run: make testdebug + - name: "Run test in production" + run: make testprod \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..51c8c4e --- /dev/null +++ b/.swiftformat @@ -0,0 +1,16 @@ +--swiftversion 5.6 + +# format options +--allman false # Prefer `K&R` https://en.wikipedia.org/wiki/Indentation_style#K&R_style +--indent tab +--tabwidth 4 + +--stripunusedargs closure-only +--enable marktypes +--disable redundantNilInit,redundantSelf,extensionAccessControl +--lineaftermarks false +--ifdef no-indent +--header strip + +# file options +--exclude .build,Sources/secp256k1,Sources/K1/Support/ThirdyParty diff --git a/Makefile b/Makefile index 35b0b8a..dd48517 100644 --- a/Makefile +++ b/Makefile @@ -4,18 +4,45 @@ PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst PLATFORM_TVOS = tvOS Simulator,name=Apple TV PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 7 (45mm) -default: test-all +ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) -test-all: test-library +.PHONY: submodules test dev clean testprod testdebug purge init format formatstaged -test-library: +rmsubmod: + rm -rf "$(ROOT_DIR)Sources/secp256k1/libsecp256k1" + +clean: + rm -rf .build + +purge: + make rmsubmod + make clean + +submodules: ## Update all sumodules . + git submodule update --init + +init: + make purge + make submodules + +dev: + ./scripts/bootstrap + make init + +test: + make clean + make testdebug + make clean + make testprod + +testdebug: swift test + +testprod: + swift test -c release -Xswiftc -enable-testing -format: - swift format \ - --ignore-unparsable-files \ - --in-place \ - --recursive \ - ./Examples ./Package.swift ./Sources ./Tests +formatstaged: + ./scripts/swiftformat-staged -.PHONY: format test-all \ No newline at end of file +format: + swiftformat --config .swiftformat "$(ROOT_DIR)" diff --git a/Package.swift b/Package.swift index b83b316..b93ef75 100644 --- a/Package.swift +++ b/Package.swift @@ -6,97 +6,96 @@ import PackageDescription let development = false let cSettings: [CSetting] = [ - // 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"), - .define("ECMULT_GEN_PREC_BITS", to: "4"), + // 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"), + .define("ECMULT_GEN_PREC_BITS", to: "4"), - // Enable modules in secp256k1, for list scroll down to bottom of: - // Sources/secp256k1/libsecp256k1/src/secp256k1.c - .define("ENABLE_MODULE_ECDH"), - .define("ENABLE_MODULE_RECOVERY"), - .define("ENABLE_MODULE_SCHNORRSIG"), - .define("ENABLE_MODULE_EXTRAKEYS"), + // Enable modules in secp256k1, for list scroll down to bottom of: + // Sources/secp256k1/libsecp256k1/src/secp256k1.c + .define("ENABLE_MODULE_ECDH"), + .define("ENABLE_MODULE_RECOVERY"), + .define("ENABLE_MODULE_SCHNORRSIG"), + .define("ENABLE_MODULE_EXTRAKEYS"), ] let package = Package( - name: "K1", - platforms: [ - .iOS(.v13), - .macOS(.v11), - .tvOS(.v13), - .watchOS(.v6), - ], - products: [ - .library( - name: "K1", - targets: [ - "K1" - ] - ), - ], - dependencies: [], - targets: [ - - // Target `libsecp256k1` https://github.com/bitcoin-core/secp256k1 - .target( - name: "secp256k1", - exclude: [ - "libsecp256k1/src/asm", - "libsecp256k1/src/bench.c", - "libsecp256k1/src/bench_ecmult.c", - "libsecp256k1/src/bench_internal.c", - "libsecp256k1/src/modules/extrakeys/tests_impl.h", - "libsecp256k1/src/modules/schnorrsig/tests_impl.h", - "libsecp256k1/src/precompute_ecmult.c", - "libsecp256k1/src/precompute_ecmult_gen.c", - "libsecp256k1/src/tests_exhaustive.c", - "libsecp256k1/src/tests.c", - "libsecp256k1/src/ctime_tests.c", - - "libsecp256k1/examples/", - - "libsecp256k1/autogen.sh", - "libsecp256k1/CMakeLists.txt", - "libsecp256k1/configure.ac", - "libsecp256k1/COPYING", - "libsecp256k1/libsecp256k1.pc.in", - "libsecp256k1/Makefile.am", - "libsecp256k1/README.md", - "libsecp256k1/SECURITY.md" - ], - cSettings: cSettings - ), - .target( - name: "K1", - dependencies: [ - "secp256k1", - ], - swiftSettings: [ - .define("CRYPTO_IN_SWIFTPM_FORCE_BUILD_API"), - ] - ), - .testTarget( - name: "K1Tests", - dependencies: [ - "K1", - ], - resources: [ - .process("TestVectors/"), - ] - ), - ] + name: "K1", + platforms: [ + .iOS(.v13), + .macOS(.v11), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "K1", + targets: [ + "K1", + ] + ), + ], + dependencies: [], + targets: [ + // Target `libsecp256k1` https://github.com/bitcoin-core/secp256k1 + .target( + name: "secp256k1", + exclude: [ + "libsecp256k1/src/asm", + "libsecp256k1/src/bench.c", + "libsecp256k1/src/bench_ecmult.c", + "libsecp256k1/src/bench_internal.c", + "libsecp256k1/src/modules/extrakeys/tests_impl.h", + "libsecp256k1/src/modules/schnorrsig/tests_impl.h", + "libsecp256k1/src/precompute_ecmult.c", + "libsecp256k1/src/precompute_ecmult_gen.c", + "libsecp256k1/src/tests_exhaustive.c", + "libsecp256k1/src/tests.c", + "libsecp256k1/src/ctime_tests.c", + + "libsecp256k1/examples/", + + "libsecp256k1/autogen.sh", + "libsecp256k1/CMakeLists.txt", + "libsecp256k1/configure.ac", + "libsecp256k1/COPYING", + "libsecp256k1/libsecp256k1.pc.in", + "libsecp256k1/Makefile.am", + "libsecp256k1/README.md", + "libsecp256k1/SECURITY.md", + ], + cSettings: cSettings + ), + .target( + name: "K1", + dependencies: [ + "secp256k1", + ], + swiftSettings: [ + .define("CRYPTO_IN_SWIFTPM_FORCE_BUILD_API"), + ] + ), + .testTarget( + name: "K1Tests", + dependencies: [ + "K1", + ], + resources: [ + .process("TestVectors/"), + ] + ), + ] ) if development { - for target in package.targets { - target.swiftSettings = target.swiftSettings ?? [] - target.swiftSettings?.append( - .unsafeFlags([ - "-Xfrontend", "-warn-concurrency", - "-Xfrontend", "-enable-actor-data-race-checks", - "-enable-library-evolution", - ]) - ) - } + for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings?.append( + .unsafeFlags([ + "-Xfrontend", "-warn-concurrency", + "-Xfrontend", "-enable-actor-data-race-checks", + "-enable-library-evolution", + ]) + ) + } } diff --git a/Sources/K1/K1.swift b/Sources/K1/K1.swift index 25aa707..fda3c33 100644 --- a/Sources/K1/K1.swift +++ b/Sources/K1/K1.swift @@ -2,5 +2,5 @@ import Foundation /// The Elliptic Curve `secp256k1` public enum K1 { - // The namespace for all structs and operations vendored by this library. + // The namespace for all structs and operations vendored by this library. } diff --git a/Sources/K1/K1/ECDH/KeyAgreement.swift b/Sources/K1/K1/ECDH/KeyAgreement.swift index b1b08f1..5d5c1ae 100644 --- a/Sources/K1/K1/ECDH/KeyAgreement.swift +++ b/Sources/K1/K1/ECDH/KeyAgreement.swift @@ -1,119 +1,111 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-24. -// - -import Foundation import struct CryptoKit.SharedSecret +import Foundation +// MARK: - K1Feature public protocol K1Feature { - associatedtype PublicKey: K1PublicKeyProtocol + associatedtype PublicKey: K1PublicKeyProtocol } +// MARK: - K1.KeyAgreement extension K1 { - - /// A mechanism used to create a shared secret between two users by performing `secp256k1` elliptic curve Diffie Hellman (ECDH) key exchange. - public enum KeyAgreement: K1Feature { - - /// A `secp256k1` private key used for key agreement. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used for key agreement. - public typealias PublicKey = PublicKeyOf - } + /// A mechanism used to create a shared secret between two users by performing `secp256k1` elliptic curve Diffie Hellman (ECDH) key exchange. + public enum KeyAgreement: K1Feature { + /// A `secp256k1` private key used for key agreement. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used for key agreement. + public typealias PublicKey = PublicKeyOf + } } extension K1.KeyAgreement.PrivateKey { - /// Computes a shared secret with the provided public key from another party, - /// returning only the `X` coordinate of the point, following [ANSI X9.63][ansix963] standards. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point <- this function - /// 2. `libsecp256k1`: SHA-256 hash the compressed point - /// 3. Custom: No hash, return point uncompressed - /// - /// This function uses 3. i.e. no hash, and returns only the `X` coordinate of the point. - /// This is following the [ANSI X9.63][ansix963] standard serialization of the shared point. - /// - /// Further more this function is compatible with CryptoKit, since it returns a CryptoKit - /// `SharedSecret` struct, thus offering you to use all of CryptoKit's Key Derivation Functions - /// (`KDF`s), which can be called on the `SharedSecret`. - /// - /// As seen on [StackExchange][cryptostackexchange], this version is compatible with the following - /// libraries: - /// - JS: `elliptic` (v6.4.0 in nodeJS v8.2.1) - /// - JS: `crypto` (builtin) - uses openssl under the hood (in nodeJS v8.2.1) - /// - .NET: `BouncyCastle` (BC v1.8.1.3, .NET v2.1.4) - /// - Python: pyca/cryptography (hazmat) - /// - /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 - /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 - public func sharedSecretFromKeyAgreement( - with publicKey: PublicKey - ) throws -> SharedSecret { - let data = try FFI.ECDH.keyExchange( - publicKey: publicKey.impl.wrapped, - privateKey: self.impl.wrapped, - serializeOutputFunction: .ansiX963 - ) - return try SharedSecret(data: data) - } - - - /// Computes a shared secret with the provided public key from another party, - /// using `libsecp256k1` default behaviour, returning a hashed of the compressed point. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point - /// 2. `libsecp256k1`: SHA-256 hash the compressed point <- this function - /// 3. Custom: No hash, return point uncompressed - /// - /// This function uses 1. i.e.SHA-256 hash the compressed point. - /// This is using the [default behaviour of `libsecp256k1`][libsecp256k1], which does not adhere to any - /// other standard. - /// - /// As seen on [StackExchange][cryptostackexchange], this version is compatible with all - /// libraries which wraps `libsecp256k1`, e.g.: - /// - Python wrapper: secp256k1 (v0.13.2, for python 3.6.4) - /// - JS wrapper: secp256k1 (v3.5.0, for nodeJS v8.2.1) - /// - /// [libsecp256k1]: https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L27 - /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 - /// - public func ecdh( - with publicKey: PublicKey, - arbitraryData: Data? = nil - ) throws -> SharedSecret { - let data = try FFI.ECDH.keyExchange( - publicKey: publicKey.impl.wrapped, - privateKey: self.impl.wrapped, - serializeOutputFunction: .libsecp256kDefault(arbitraryData: arbitraryData) - ) - return try SharedSecret(data: data) - } - - /// Computes a shared secret with the provided public key from another party, - /// returning an uncompressed public point, unhashed. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point - /// 2. `libsecp256k1`: SHA-256 hash the compressed point - /// 3. Custom: No hash, return point uncompressed <- this function - /// - /// This function uses 2. i.e. no hash, return point uncompressed - /// **This is not following any standard at all**, but might be useful if you want to write your - /// cryptographic functions, e.g. some ECIES scheme. - /// - public func ecdhPoint(with publicKey: PublicKey) throws -> Data { - try FFI.ECDH.keyExchange( - publicKey: publicKey.impl.wrapped, - privateKey: self.impl.wrapped, - serializeOutputFunction: .noHashWholePoint - ) - } + /// Computes a shared secret with the provided public key from another party, + /// returning only the `X` coordinate of the point, following [ANSI X9.63][ansix963] standards. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point <- this function + /// 2. `libsecp256k1`: SHA-256 hash the compressed point + /// 3. Custom: No hash, return point uncompressed + /// + /// This function uses 3. i.e. no hash, and returns only the `X` coordinate of the point. + /// This is following the [ANSI X9.63][ansix963] standard serialization of the shared point. + /// + /// Further more this function is compatible with CryptoKit, since it returns a CryptoKit + /// `SharedSecret` struct, thus offering you to use all of CryptoKit's Key Derivation Functions + /// (`KDF`s), which can be called on the `SharedSecret`. + /// + /// As seen on [StackExchange][cryptostackexchange], this version is compatible with the following + /// libraries: + /// - JS: `elliptic` (v6.4.0 in nodeJS v8.2.1) + /// - JS: `crypto` (builtin) - uses openssl under the hood (in nodeJS v8.2.1) + /// - .NET: `BouncyCastle` (BC v1.8.1.3, .NET v2.1.4) + /// - Python: pyca/cryptography (hazmat) + /// + /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 + /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 + public func sharedSecretFromKeyAgreement( + with publicKey: PublicKey + ) throws -> SharedSecret { + let data = try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .ansiX963 + ) + return try SharedSecret(data: data) + } + + /// Computes a shared secret with the provided public key from another party, + /// using `libsecp256k1` default behaviour, returning a hashed of the compressed point. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point + /// 2. `libsecp256k1`: SHA-256 hash the compressed point <- this function + /// 3. Custom: No hash, return point uncompressed + /// + /// This function uses 1. i.e.SHA-256 hash the compressed point. + /// This is using the [default behaviour of `libsecp256k1`][libsecp256k1], which does not adhere to any + /// other standard. + /// + /// As seen on [StackExchange][cryptostackexchange], this version is compatible with all + /// libraries which wraps `libsecp256k1`, e.g.: + /// - Python wrapper: secp256k1 (v0.13.2, for python 3.6.4) + /// - JS wrapper: secp256k1 (v3.5.0, for nodeJS v8.2.1) + /// + /// [libsecp256k1]: https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L27 + /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 + /// + public func ecdh( + with publicKey: PublicKey, + arbitraryData: Data? = nil + ) throws -> SharedSecret { + let data = try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .libsecp256kDefault(arbitraryData: arbitraryData) + ) + return try SharedSecret(data: data) + } + + /// Computes a shared secret with the provided public key from another party, + /// returning an uncompressed public point, unhashed. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point + /// 2. `libsecp256k1`: SHA-256 hash the compressed point + /// 3. Custom: No hash, return point uncompressed <- this function + /// + /// This function uses 2. i.e. no hash, return point uncompressed + /// **This is not following any standard at all**, but might be useful if you want to write your + /// cryptographic functions, e.g. some ECIES scheme. + /// + public func ecdhPoint(with publicKey: PublicKey) throws -> Data { + try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .noHashWholePoint + ) + } } diff --git a/Sources/K1/K1/ECDSA/ECDSA.swift b/Sources/K1/K1/ECDSA/ECDSA.swift index 569be98..0ba2c1f 100644 --- a/Sources/K1/K1/ECDSA/ECDSA.swift +++ b/Sources/K1/K1/ECDSA/ECDSA.swift @@ -1,80 +1,70 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-24. -// - import Foundation - +// MARK: - K1.ECDSA extension K1 { - - /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA). - public enum ECDSA {} + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA). + public enum ECDSA {} } +// MARK: - K1.ECDSA.ValidationOptions extension K1.ECDSA { - public struct ValidationOptions { - - public let malleabilityStrictness: MalleabilityStrictness - - public init(malleabilityStrictness: MalleabilityStrictness) { - self.malleabilityStrictness = malleabilityStrictness - } - - } + public struct ValidationOptions { + public let malleabilityStrictness: MalleabilityStrictness + + public init(malleabilityStrictness: MalleabilityStrictness) { + self.malleabilityStrictness = malleabilityStrictness + } + } } extension K1.ECDSA.ValidationOptions { - - public static let `default`: Self = .init( - malleabilityStrictness: .rejected - ) - - /// Whether or not to consider malleable signatures valid. - /// - /// [more]: https://github.com/bitcoin-core/secp256k1/blob/2e5e4b67dfb67950563c5f0ab2a62e25eb1f35c5/include/secp256k1.h#L510-L550 - public enum MalleabilityStrictness { - /// Considers all malleable signatures **invalid**. - case rejected - - /// Accepts malleable signatures valid. - case accepted - } + public static let `default`: Self = .init( + malleabilityStrictness: .rejected + ) + + /// Whether or not to consider malleable signatures valid. + /// + /// [more]: https://github.com/bitcoin-core/secp256k1/blob/2e5e4b67dfb67950563c5f0ab2a62e25eb1f35c5/include/secp256k1.h#L510-L550 + public enum MalleabilityStrictness { + /// Considers all malleable signatures **invalid**. + case rejected + + /// Accepts malleable signatures valid. + case accepted + } } -// MARK: SigningOptions +// MARK: - K1.ECDSA.SigningOptions extension K1.ECDSA { - public struct SigningOptions: Sendable, Hashable { - public let nonceFunction: NonceFunction + public struct SigningOptions: Sendable, Hashable { + public let nonceFunction: NonceFunction - public init(nonceFunction: NonceFunction) { - self.nonceFunction = nonceFunction - } - } + public init(nonceFunction: NonceFunction) { + self.nonceFunction = nonceFunction + } + } } extension K1.ECDSA.SigningOptions { - - public static let `default`: Self = .init(nonceFunction: .deterministic()) - - public enum NonceFunction: Sendable, Hashable { - case random - - /// RFC6979 - case deterministic(arbitraryData: RFC6979ArbitraryData? = nil) - } + public static let `default`: Self = .init(nonceFunction: .deterministic()) + + public enum NonceFunction: Sendable, Hashable { + case random + + /// RFC6979 + case deterministic(arbitraryData: RFC6979ArbitraryData? = nil) + } } +// MARK: - K1.ECDSA.SigningOptions.NonceFunction.RFC6979ArbitraryData extension K1.ECDSA.SigningOptions.NonceFunction { - public struct RFC6979ArbitraryData: Sendable, Hashable { - public let arbitraryData: [UInt8] - public init(arbitraryData: [UInt8]) throws { - guard arbitraryData.count == Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction - } - self.arbitraryData = arbitraryData - } - } + public struct RFC6979ArbitraryData: Sendable, Hashable { + public let arbitraryData: [UInt8] + public init(arbitraryData: [UInt8]) throws { + guard arbitraryData.count == Curve.Field.byteCount else { + throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction + } + self.arbitraryData = arbitraryData + } + } } diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift index 5b756f2..0ce6d1d 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift @@ -1,240 +1,223 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation import protocol CryptoKit.Digest import struct CryptoKit.SHA256 +import Foundation +// MARK: - K1.ECDSA.NonRecoverable extension K1.ECDSA { - - /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do not offer recovery of the public key. - public enum NonRecoverable: K1Feature { - - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically ECDSA signatures, that do not offer recovery of the public key. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures, - /// more specifically ECDSA signatures, that do not offer recovery of the public key. - public typealias PublicKey = PublicKeyOf - } + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do not offer recovery of the public key. + public enum NonRecoverable: K1Feature { + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public typealias PublicKey = PublicKeyOf + } } +// MARK: - K1.ECDSA.NonRecoverable.Signature extension K1.ECDSA.NonRecoverable { - public struct Signature: Sendable, Hashable, ContiguousBytes { - - typealias Wrapped = FFI.ECDSA.NonRecovery.Wrapped - internal let wrapped: Wrapped - - init(wrapped: Wrapped) { - self.wrapped = wrapped - } - } + public struct Signature: Sendable, Hashable, ContiguousBytes { + typealias Wrapped = FFI.ECDSA.NonRecovery.Wrapped + internal let wrapped: Wrapped + + init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } } // MARK: Sign extension K1.ECDSA.NonRecoverable.PrivateKey { - - /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. - /// - Parameters: - /// - hashed: The _hashed_ data to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - for hashed: some DataProtocol, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.NonRecoverable.Signature { - try K1.ECDSA.NonRecoverable.Signature( - wrapped: FFI.ECDSA.NonRecovery.sign( - hashedMessage: [UInt8](hashed), - privateKey: impl.wrapped, - options: options - ) - ) - } - - /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. - /// - Parameters: - /// - digest: The digest of the data to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - for digest: some Digest, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.NonRecoverable.Signature { - try signature( - for: Data(digest), - options: options - ) - } - - /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. - /// - Parameters: - /// - unhashed: The data hash and then to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - forUnhashed unhashed: some DataProtocol, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.NonRecoverable.Signature { - try signature( - for: SHA256.hash(data: unhashed), - options: options - ) - } + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - hashed: The _hashed_ data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for hashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try K1.ECDSA.NonRecoverable.Signature( + wrapped: FFI.ECDSA.NonRecovery.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) + } + + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - digest: The digest of the data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for digest: some Digest, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try signature( + for: Data(digest), + options: options + ) + } + + /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. + /// - Parameters: + /// - unhashed: The data hash and then to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } } - // MARK: Validate extension K1.ECDSA.NonRecoverable.PublicKey { - /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on some _hash_ over the `secp256k1` elliptic curve. - /// - Parameters: - /// - signature: The (non-recoverable) signature to check against the _hashed_ data. - /// - hashed: The _hashed_ data covered by the signature. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. - public func isValidSignature( - _ signature: K1.ECDSA.NonRecoverable.Signature, - hashed: some DataProtocol, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - do { - return try FFI.ECDSA.NonRecovery.isValid( - ecdsaSignature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed), - options: options - ) - } catch { - return false - } - } - - /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a digest over the `secp256k1` elliptic curve. - /// - Parameters: - /// - signature: The (non-recoverable) signature to check against the given digest. - /// - digest: The digest covered by the signature. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. - public func isValidSignature( - _ signature: K1.ECDSA.NonRecoverable.Signature, - digest: some Digest, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - isValidSignature( - signature, - hashed: Data(digest), - options: options - ) - } - - /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a block of data over the `secp256k1` elliptic curve. - /// - /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use - /// `isValidSignature(_:hashed:input)`. - /// - /// - Parameters: - /// - signature: The (non-recoverable) signature to check against the given digest. - /// - unhashed: The block of data covered by the signature. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. - public func isValidSignature( - _ signature: K1.ECDSA.NonRecoverable.Signature, - unhashed: some DataProtocol, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - isValidSignature( - signature, - digest: SHA256.hash(data: unhashed), - options: options - ) - } + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + hashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + do { + return try FFI.ECDSA.NonRecovery.isValid( + ecdsaSignature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options + ) + } catch { + return false + } + } + + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + digest: some Digest, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + hashed: Data(digest), + options: options + ) + } + + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)`. + /// + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the given digest. + /// - unhashed: The block of data covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + unhashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed), + options: options + ) + } } - // MARK: Inits extension K1.ECDSA.NonRecoverable.Signature { - - public init(compactRepresentation: some DataProtocol) throws { - try self.init( - wrapped: FFI.ECDSA.NonRecovery.from(compactBytes: [UInt8](compactRepresentation)) - ) - } - - public init(derRepresentation: some DataProtocol) throws { - try self.init( - wrapped: FFI.ECDSA.NonRecovery.from(derRepresentation: [UInt8](derRepresentation)) - ) - } + public init(compactRepresentation: some DataProtocol) throws { + try self.init( + wrapped: FFI.ECDSA.NonRecovery.from(compactBytes: [UInt8](compactRepresentation)) + ) + } + + public init(derRepresentation: some DataProtocol) throws { + try self.init( + wrapped: FFI.ECDSA.NonRecovery.from(derRepresentation: [UInt8](derRepresentation)) + ) + } } // MARK: ContiguousBytes extension K1.ECDSA.NonRecoverable.Signature { - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - try wrapped.withUnsafeBytes(body) - } + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try wrapped.withUnsafeBytes(body) + } } // MARK: Serialize extension K1.ECDSA.NonRecoverable.Signature { - - internal var rawRepresentation: Data { - Data(wrapped.bytes) - } - - public func compactRepresentation() throws -> Data { - try FFI.ECDSA.NonRecovery.compact(wrapped) - } - - public func derRepresentation() throws -> Data { - try FFI.ECDSA.NonRecovery.der(wrapped) - } -} + internal var rawRepresentation: Data { + Data(wrapped.bytes) + } + public func compactRepresentation() throws -> Data { + try FFI.ECDSA.NonRecovery.compact(wrapped) + } + + public func derRepresentation() throws -> Data { + try FFI.ECDSA.NonRecovery.der(wrapped) + } +} // MARK: Recover extension K1.ECDSA.NonRecoverable.Signature { - public func recoverPublicKey( - recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID, - message: some DataProtocol - ) throws -> K1.ECDSA.NonRecoverable.PublicKey { - let wrapped = try FFI.ECDSA.NonRecovery.recoverPublicKey( - self.wrapped, - recoveryID: recoveryID.recid, - message: [UInt8](message) - ) - let impl = K1.PublicKeyImpl(wrapped: wrapped) - return K1.ECDSA.NonRecoverable.PublicKey(impl: impl) - } + public func recoverPublicKey( + recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID, + message: some DataProtocol + ) throws -> K1.ECDSA.NonRecoverable.PublicKey { + let wrapped = try FFI.ECDSA.NonRecovery.recoverPublicKey( + self.wrapped, + recoveryID: recoveryID.recid, + message: [UInt8](message) + ) + let impl = K1._PublicKeyImplementation(wrapped: wrapped) + return K1.ECDSA.NonRecoverable.PublicKey(impl: impl) + } } extension K1.ECDSA.NonRecoverable.Signature { - internal static let byteCount = FFI.ECDSA.Recovery.byteCount + internal static let byteCount = FFI.ECDSA.Recovery.byteCount } // MARK: Equatable extension K1.ECDSA.NonRecoverable.Signature { - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.wrapped.withUnsafeBytes { lhsBytes in - rhs.wrapped.withUnsafeBytes { rhsBytes in - safeCompare(lhsBytes, rhsBytes) - } - } - } + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.withUnsafeBytes { lhsBytes in + rhs.wrapped.withUnsafeBytes { rhsBytes in + safeCompare(lhsBytes, rhsBytes) + } + } + } } // MARK: Hashable extension K1.ECDSA.NonRecoverable.Signature { - - public func hash(into hasher: inout Hasher) { - wrapped.withUnsafeBytes { - hasher.combine(bytes: $0) - } - } - + public func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } } diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift index aa1c45c..4a616f8 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift @@ -1,348 +1,327 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation import protocol CryptoKit.Digest import struct CryptoKit.SHA256 +import Foundation +// MARK: - K1.ECDSA.Recoverable extension K1.ECDSA { - - /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do offers recovery of the public key. - public enum Recoverable: K1Feature { - - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically ECDSA signatures that offers recovery of the public key. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures. - /// more specifically ECDSA signatures that offers recovery of the public key. - public typealias PublicKey = PublicKeyOf - } + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do offers recovery of the public key. + public enum Recoverable: K1Feature { + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures that offers recovery of the public key. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures. + /// more specifically ECDSA signatures that offers recovery of the public key. + public typealias PublicKey = PublicKeyOf + } } // MARK: Sign extension K1.ECDSA.Recoverable.PrivateKey { - - /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. - /// - Parameters: - /// - hashed: The _hashed_ data to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - for hashed: some DataProtocol, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.Recoverable.Signature { - try K1.ECDSA.Recoverable.Signature( - wrapped: FFI.ECDSA.Recovery.sign( - hashedMessage: [UInt8](hashed), - privateKey: impl.wrapped, - options: options - ) - ) - } - - /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. - /// - Parameters: - /// - digest: The digest of the data to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - for digest: some Digest, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.Recoverable.Signature { - try signature( - for: Data(digest), - options: options - ) - } - - /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. - /// - Parameters: - /// - unhashed: The data hash and then to sign. - /// - options: Whether or not to consider malleable signatures valid. - /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. - public func signature( - forUnhashed unhashed: some DataProtocol, - options: K1.ECDSA.SigningOptions = .default - ) throws -> K1.ECDSA.Recoverable.Signature { - try signature( - for: SHA256.hash(data: unhashed), - options: options - ) - } + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - hashed: The _hashed_ data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for hashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try K1.ECDSA.Recoverable.Signature( + wrapped: FFI.ECDSA.Recovery.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) + } + + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - digest: The digest of the data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for digest: some Digest, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try signature( + for: Data(digest), + options: options + ) + } + /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. + /// - Parameters: + /// - unhashed: The data hash and then to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } } // MARK: Validate extension K1.ECDSA.Recoverable.PublicKey { + /// Verifies a recoverable ECDSA signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - hashed: The _hashed_ data covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + hashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + do { + let publicKeyNonRecoverable = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: self.rawRepresentation) + let signatureNonRecoverable = try signature.nonRecoverable() + + return publicKeyNonRecoverable.isValidSignature( + signatureNonRecoverable, + hashed: hashed, + options: options + ) + } catch { + return false + } + } - /// Verifies a recoverable ECDSA signature on some _hash_ over the `secp256k1` elliptic curve. - /// - Parameters: - /// - signature: The recoverable ECDSA signature to check against the given digest. - /// - hashed: The _hashed_ data covered by the signature. - /// - options: ECDSA validation options used during validation - /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. - public func isValidSignature( - _ signature: K1.ECDSA.Recoverable.Signature, - hashed: some DataProtocol, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - do { - let publicKeyNonRecoverable = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: self.rawRepresentation) - let signatureNonRecoverable = try signature.nonRecoverable() - - return publicKeyNonRecoverable.isValidSignature( - signatureNonRecoverable, - hashed: hashed, - options: options - ) - } catch { - return false - } - } - - /// Verifies a recoverable ECDSA signature on a digest over the `secp256k1` elliptic curve. - /// - Parameters: - /// - signature: The recoverable ECDSA signature to check against the given digest. - /// - digest: The digest covered by the signature. - /// - options: ECDSA validation options used during validation - /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. - public func isValidSignature( - _ signature: K1.ECDSA.Recoverable.Signature, - digest: some Digest, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - isValidSignature( - signature, - hashed: Data(digest), - options: options - ) - } - - /// Verifies a recoverable ECDSA signature on a block of data over the `secp256k1` elliptic curve. - /// - /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use - /// `isValidSignature(_:hashed:input)`. - /// - /// - Parameters: - /// - signature: The recoverable ECDSA signature to check against the given digest. - /// - unhashed: The block of data covered by the signature. - /// - options: ECDSA validation options used during validation - /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. - public func isValidSignature( - _ signature: K1.ECDSA.Recoverable.Signature, - unhashed: some DataProtocol, - options: K1.ECDSA.ValidationOptions = .default - ) -> Bool { - isValidSignature( - signature, - digest: SHA256.hash(data: unhashed), - options: options - ) - } - + /// Verifies a recoverable ECDSA signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + digest: some Digest, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + hashed: Data(digest), + options: options + ) + } + + /// Verifies a recoverable ECDSA signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)`. + /// + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - unhashed: The block of data covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + unhashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed), + options: options + ) + } } +// MARK: - K1.ECDSA.Recoverable.Signature extension K1.ECDSA.Recoverable { - public struct Signature: Sendable, Hashable, ContiguousBytes { - - typealias Wrapped = FFI.ECDSA.Recovery.Wrapped - private let wrapped: Wrapped - - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - } - } -} + public struct Signature: Sendable, Hashable, ContiguousBytes { + typealias Wrapped = FFI.ECDSA.Recovery.Wrapped + private let wrapped: Wrapped + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } +} // MARK: Init extension K1.ECDSA.Recoverable.Signature { + /// Compact aka `IEEE P1363` aka `R||S`. + public init(compact: Compact) throws { + try self.init( + wrapped: FFI.ECDSA.Recovery.deserialize( + compact: [UInt8](compact.compact), + recoveryID: compact.recoveryID.recid + ) + ) + } - /// Compact aka `IEEE P1363` aka `R||S`. - public init(compact: Compact) throws { - try self.init( - wrapped: FFI.ECDSA.Recovery.deserialize( - compact: [UInt8](compact.compact), - recoveryID: compact.recoveryID.recid - ) - ) - } - - /// Compact aka `IEEE P1363` aka `R||S`. - public init(compact: Data, recoveryID: RecoveryID) throws { - try self.init(compact: .init(compact: compact, recoveryID: recoveryID)) - } - - - public init( - rawRepresentation: some DataProtocol - ) throws { - try self.init( - wrapped: FFI.ECDSA.Recovery.deserialize(rawRepresentation: rawRepresentation) - ) - } + /// Compact aka `IEEE P1363` aka `R||S`. + public init(compact: Data, recoveryID: RecoveryID) throws { + try self.init(compact: .init(compact: compact, recoveryID: recoveryID)) + } + + public init( + rawRepresentation: some DataProtocol + ) throws { + try self.init( + wrapped: FFI.ECDSA.Recovery.deserialize(rawRepresentation: rawRepresentation) + ) + } } // MARK: ContiguousBytes extension K1.ECDSA.Recoverable.Signature { - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - try wrapped.withUnsafeBytes(body) - } + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try wrapped.withUnsafeBytes(body) + } } // MARK: Serialize extension K1.ECDSA.Recoverable.Signature { - - internal var rawRepresentation: Data { - Data(wrapped.bytes) - } - - /// Compact aka `IEEE P1363` aka `R||S` with `RecoveryID` - public func compact() throws -> Compact { - let (rs, recid) = try FFI.ECDSA.Recovery.serializeCompact( - wrapped - ) - return try .init( - compact: Data(rs), - recoveryID: .init(recid: recid) - ) - } - - public struct Compact: Sendable, Hashable { - - /// Compact aka `IEEE P1363` aka `R||S`. - public let compact: Data - - public let recoveryID: RecoveryID - - /// Compact aka `IEEE P1363` aka `R||S`. - public init( - compact: Data, - recoveryID: RecoveryID - ) throws { - guard compact.count == Self.byteCountRS else { - throw K1.Error.failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount( - got: compact.count, - expected: Self.byteCountRS - ) - } - self.compact = compact - self.recoveryID = recoveryID - } - - } + internal var rawRepresentation: Data { + Data(wrapped.bytes) + } + + /// Compact aka `IEEE P1363` aka `R||S` with `RecoveryID` + public func compact() throws -> Compact { + let (rs, recid) = try FFI.ECDSA.Recovery.serializeCompact( + wrapped + ) + return try .init( + compact: Data(rs), + recoveryID: .init(recid: recid) + ) + } + + public struct Compact: Sendable, Hashable { + /// Compact aka `IEEE P1363` aka `R||S`. + public let compact: Data + + public let recoveryID: RecoveryID + + /// Compact aka `IEEE P1363` aka `R||S`. + public init( + compact: Data, + recoveryID: RecoveryID + ) throws { + guard compact.count == Self.byteCountRS else { + throw K1.Error.failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount( + got: compact.count, + expected: Self.byteCountRS + ) + } + self.compact = compact + self.recoveryID = recoveryID + } + } } extension K1.ECDSA.Recoverable.Signature.Compact { - - public static let byteCountRS = 2 * Curve.Field.byteCount - public static let byteCount = Self.byteCountRS + 1 - - /// Takes either `R || S || V` data or `V || R || S` data, as per specification of `format`. - public init( - rawRepresentation: some DataProtocol, - format: SerializationFormat - ) throws { - guard rawRepresentation.count == Self.byteCount else { - throw K1.Error.failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: rawRepresentation.count, expected: Self.byteCount - ) - } - switch format { - case .vrs: - try self.init( - compact: Data(rawRepresentation.suffix(Self.byteCountRS)), - recoveryID: .init(byte: rawRepresentation.first!) // force unwrap OK since we have checked length above. - ) - case .rsv: - try self.init( - compact: Data(rawRepresentation.prefix(Self.byteCountRS)), - recoveryID: .init(byte: rawRepresentation.last!) // force unwrap OK since we have checked length above. - ) - } - } - - public enum SerializationFormat { - - /// `R || S || V` - the format `libsecp256k1` v0.3.0 uses as internal representation - /// This is the default value of this library. - case rsv - - /// We use `R || S || V` as default values since `libsecp256k1` v0.3.0 uses it as its internal representation. - public static let `default`: Self = .rsv - - /// `V || R || S`. - case vrs - } - - private var v: Data { - recoveryID.vData - } - private var rs: Data { - compact - } - - func serialize(format: SerializationFormat) -> Data { - switch format { - case .rsv: - return rs + v - case .vrs: - return v + rs - } - } + public static let byteCountRS = 2 * Curve.Field.byteCount + public static let byteCount = Self.byteCountRS + 1 + + /// Takes either `R || S || V` data or `V || R || S` data, as per specification of `format`. + public init( + rawRepresentation: some DataProtocol, + format: SerializationFormat + ) throws { + guard rawRepresentation.count == Self.byteCount else { + throw K1.Error.failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: rawRepresentation.count, expected: Self.byteCount) + } + switch format { + case .vrs: + try self.init( + compact: Data(rawRepresentation.suffix(Self.byteCountRS)), + recoveryID: .init(byte: rawRepresentation.first!) // force unwrap OK since we have checked length above. + ) + case .rsv: + try self.init( + compact: Data(rawRepresentation.prefix(Self.byteCountRS)), + recoveryID: .init(byte: rawRepresentation.last!) // force unwrap OK since we have checked length above. + ) + } + } + + public enum SerializationFormat { + /// `R || S || V` - the format `libsecp256k1` v0.3.0 uses as internal representation + /// This is the default value of this library. + case rsv + + /// We use `R || S || V` as default values since `libsecp256k1` v0.3.0 uses it as its internal representation. + public static let `default`: Self = .rsv + + /// `V || R || S`. + case vrs + } + + private var v: Data { + recoveryID.vData + } + + private var rs: Data { + compact + } + + func serialize(format: SerializationFormat) -> Data { + switch format { + case .rsv: + return rs + v + case .vrs: + return v + rs + } + } } + extension K1.ECDSA.Recoverable.Signature.RecoveryID { - var vData: Data { - Data( - [UInt8(rawValue)] - ) - } + var vData: Data { + Data( + [UInt8(rawValue)] + ) + } } // MARK: Recovery extension K1.ECDSA.Recoverable.Signature { - public func recoverPublicKey( - message: some DataProtocol - ) throws -> K1.ECDSA.Recoverable.PublicKey { - let wrapped = try FFI.ECDSA.Recovery.recover(wrapped, message: [UInt8](message)) - let impl = K1.PublicKeyImpl(wrapped: wrapped) - return K1.ECDSA.Recoverable.PublicKey( - impl: impl - ) - } + public func recoverPublicKey( + message: some DataProtocol + ) throws -> K1.ECDSA.Recoverable.PublicKey { + let wrapped = try FFI.ECDSA.Recovery.recover(wrapped, message: [UInt8](message)) + let impl = K1._PublicKeyImplementation(wrapped: wrapped) + return K1.ECDSA.Recoverable.PublicKey( + impl: impl + ) + } } - // MARK: Conversion extension K1.ECDSA.Recoverable.Signature { - public func nonRecoverable() throws -> K1.ECDSA.NonRecoverable.Signature { - try K1.ECDSA.NonRecoverable.Signature( - wrapped: FFI.ECDSA.Recovery.nonRecoverable(self.wrapped) - ) - } + public func nonRecoverable() throws -> K1.ECDSA.NonRecoverable.Signature { + try K1.ECDSA.NonRecoverable.Signature( + wrapped: FFI.ECDSA.Recovery.nonRecoverable(self.wrapped) + ) + } } // MARK: Equatable extension K1.ECDSA.Recoverable.Signature { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.wrapped.withUnsafeBytes { lhsBytes in - rhs.wrapped.withUnsafeBytes { rhsBytes in - safeCompare(lhsBytes, rhsBytes) - } - } - } + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.withUnsafeBytes { lhsBytes in + rhs.wrapped.withUnsafeBytes { rhsBytes in + safeCompare(lhsBytes, rhsBytes) + } + } + } } // MARK: Hashable extension K1.ECDSA.Recoverable.Signature { - - public func hash(into hasher: inout Hasher) { - wrapped.withUnsafeBytes { - hasher.combine(bytes: $0) - } - } + public func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } } diff --git a/Sources/K1/K1/ECDSA/RecoveryID.swift b/Sources/K1/K1/ECDSA/RecoveryID.swift index 00ff388..bc9fac0 100644 --- a/Sources/K1/K1/ECDSA/RecoveryID.swift +++ b/Sources/K1/K1/ECDSA/RecoveryID.swift @@ -1,39 +1,31 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation -// MARK: Recovery +// MARK: - K1.ECDSA.Recoverable.Signature.RecoveryID extension K1.ECDSA.Recoverable.Signature { - - public enum RecoveryID: UInt8, Sendable, Hashable, Codable { - case _0 = 0 - case _1 = 1 - case _2 = 2 - case _3 = 3 - - internal var recid: Int32 { - Int32(rawValue) - } - } + public enum RecoveryID: UInt8, Sendable, Hashable, Codable { + case _0 = 0 + case _1 = 1 + case _2 = 2 + case _3 = 3 + + internal var recid: Int32 { + Int32(rawValue) + } + } } extension K1.ECDSA.Recoverable.Signature.RecoveryID { - public init(byte: UInt8) throws { - guard let self_ = Self(rawValue: byte) else { - throw K1.Error.invalidRecoveryID(got: Int(byte)) - } - self = self_ - } - - public init(recid: Int32) throws { - guard recid <= 3 && recid >= 0 else { - throw K1.Error.invalidRecoveryID(got: Int(recid)) - } - try self.init(byte: UInt8(recid)) - } + public init(byte: UInt8) throws { + guard let self_ = Self(rawValue: byte) else { + throw K1.Error.invalidRecoveryID(got: Int(byte)) + } + self = self_ + } + + public init(recid: Int32) throws { + guard recid <= 3, recid >= 0 else { + throw K1.Error.invalidRecoveryID(got: Int(recid)) + } + try self.init(byte: UInt8(recid)) + } } diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift deleted file mode 100644 index da6c070..0000000 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import CryptoKit -import Foundation - -public protocol K1KeyExportable { - var rawRepresentation: Data { get } - var x963Representation: Data { get } - var derRepresentation: Data { get } - var pemRepresentation: String { get } -} - -public protocol K1KeyImportable { - init(rawRepresentation: some ContiguousBytes) throws - init(x963Representation: some ContiguousBytes) throws - init(derRepresentation: some RandomAccessCollection) throws - init(pemRepresentation: String) throws -} - -public typealias K1KeyPortable = K1KeyImportable & K1KeyExportable - -public protocol K1PrivateKeyProtocol: K1KeyPortable { - associatedtype PublicKey: K1PublicKeyProtocol - var publicKey: PublicKey { get } - init() -} - -// MARK: - PrivateKey -extension K1 { - - struct PrivateKeyImpl: Sendable, Hashable, K1PrivateKeyProtocol { - - typealias Wrapped = FFI.PrivateKey.Wrapped - internal let wrapped: Wrapped - - let publicKey: PublicKeyImpl - - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - self.publicKey = PublicKey(wrapped: wrapped.publicKey) - } - } -} - -// MARK: Inits -extension K1.PrivateKeyImpl { - - /// Creates a `secp256k1` private key from a Privacy-Enhanced Mail (PEM) representation. - init( - pemRepresentation: String - ) throws { - let pem = try ASN1.PEMDocument(pemString: pemRepresentation) - - switch pem.type { - case "EC \(Self.pemType)": - let parsed = try ASN1.SEC1PrivateKey(asn1Encoded: Array(pem.derBytes)) - self = try .init(rawRepresentation: parsed.privateKey) - case Self.pemType: - let parsed = try ASN1.PKCS8PrivateKey(asn1Encoded: Array(pem.derBytes)) - self = try .init(rawRepresentation: parsed.privateKey.privateKey) - default: - throw K1.Error.invalidPEMDocument - } - } - - init( - rawRepresentation: some ContiguousBytes - ) throws { - try self.init( - wrapped: FFI.PrivateKey.deserialize(rawRepresentation: rawRepresentation) - ) - } - - init( - x963Representation: some ContiguousBytes - ) throws { - let length = x963Representation.withUnsafeBytes { $0.count } - guard length == Self.x963ByteCount else { - throw K1.Error.incorrectByteCountOfX963PrivateKey(got: length, expected: Self.x963ByteCount) - } - - let publicKeyX963 = x963Representation.bytes.prefix(K1.PublicKeyImpl.x963ByteCount) - let publicKeyFromX963 = try K1.PublicKeyImpl.init(x963Representation: publicKeyX963) - let privateKeyRaw = x963Representation.bytes.suffix(Self.rawByteCount) - try self.init(rawRepresentation: privateKeyRaw) - guard self.publicKey == publicKeyFromX963 else { - throw K1.Error.invalidPrivateX963RepresentationPublicKeyDiscrepancy - } - // All good - } - - - /// `DER` - init(derRepresentation: some RandomAccessCollection) throws { - let bytes = Array(derRepresentation) - - // We have to try to parse this twice because we have no informaton about what kind of key this is. - // We try with PKCS#8 first, and then fall back to SEC.1. - do { - let key = try ASN1.PKCS8PrivateKey(asn1Encoded: bytes) - self = try .init(rawRepresentation: key.privateKey.privateKey) - } catch { - let key = try ASN1.SEC1PrivateKey(asn1Encoded: bytes) - self = try .init(rawRepresentation: key.privateKey) - } - } - - init() { - self.init(wrapped: .init()) - } -} - -// MARK: Serialize -extension K1.PrivateKeyImpl { - - /// A raw representation of the private key. - var rawRepresentation: Data { - Data(wrapped.secureBytes.bytes) - } - - /// A Distinguished Encoding Rules (DER) encoded representation of the private key. - var derRepresentation: Data { - let pkey = ASN1.PKCS8PrivateKey( - algorithm: .secp256k1, - privateKey: Array(self.rawRepresentation), - publicKey: Array(self.publicKey.x963Representation) - ) - var serializer = ASN1.Serializer() - - // Serializing these keys can't throw - try! serializer.serialize(pkey) - return Data(serializer.serializedBytes) - } - - /// A Privacy-Enhanced Mail (PEM) representation of the private key. - var pemRepresentation: String { - let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) - return pemDocument.pemString - } - - /// An ANSI x9.63 representation of the private key. - var x963Representation: Data { - // The x9.63 private key format is a discriminator byte (0x4) concatenated with the X and Y points - // of the key, and the K value of the secret scalar. Let's load that in. - var bytes = Data() - bytes.reserveCapacity(Self.x963ByteCount) - bytes.append(contentsOf: publicKey.x963Representation) - bytes.append(self.rawRepresentation) - return bytes - } - - - static let rawByteCount = Curve.Field.byteCount - static let x963ByteCount = K1.PublicKeyImpl.x963ByteCount + K1.PrivateKeyImpl.rawByteCount -} - -extension K1.PrivateKeyImpl { - static let pemType = "PRIVATE KEY" -} - - -// MARK: - Equatable -extension K1.PrivateKeyImpl { - /// Constant-time comparision. - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.wrapped.secureBytes == rhs.wrapped.secureBytes - } -} - -// MARK: - Hashable -extension K1.PrivateKeyImpl { - /// We use the key of the private key as input to hash - func hash(into hasher: inout Hasher) { - hasher.combine(self.publicKey) - } -} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift new file mode 100644 index 0000000..5d87a37 --- /dev/null +++ b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift @@ -0,0 +1,170 @@ +import CryptoKit +import Foundation + +// MARK: - K1KeyExportable +public protocol K1KeyExportable { + var rawRepresentation: Data { get } + var x963Representation: Data { get } + var derRepresentation: Data { get } + var pemRepresentation: String { get } +} + +// MARK: - K1KeyImportable +public protocol K1KeyImportable { + init(rawRepresentation: some ContiguousBytes) throws + init(x963Representation: some ContiguousBytes) throws + init(derRepresentation: some RandomAccessCollection) throws + init(pemRepresentation: String) throws +} + +public typealias K1KeyPortable = K1KeyImportable & K1KeyExportable + +// MARK: - K1PrivateKeyProtocol +public protocol K1PrivateKeyProtocol: K1KeyPortable { + associatedtype PublicKey: K1PublicKeyProtocol + var publicKey: PublicKey { get } + init() +} + +// MARK: - K1._PrivateKeyImplementation +extension K1 { + struct _PrivateKeyImplementation: Sendable, Hashable, K1PrivateKeyProtocol { + typealias Wrapped = FFI.PrivateKey.Wrapped + internal let wrapped: Wrapped + + let publicKey: _PublicKeyImplementation + + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + self.publicKey = PublicKey(wrapped: wrapped.publicKey) + } + } +} + +// MARK: Inits +extension K1._PrivateKeyImplementation { + /// Creates a `secp256k1` private key from a Privacy-Enhanced Mail (PEM) representation. + init( + pemRepresentation: String + ) throws { + let pem = try ASN1.PEMDocument(pemString: pemRepresentation) + + switch pem.type { + case "EC \(Self.pemType)": + let parsed = try ASN1.SEC1PrivateKey(asn1Encoded: Array(pem.derBytes)) + self = try .init(rawRepresentation: parsed.privateKey) + case Self.pemType: + let parsed = try ASN1.PKCS8PrivateKey(asn1Encoded: Array(pem.derBytes)) + self = try .init(rawRepresentation: parsed.privateKey.privateKey) + default: + throw K1.Error.invalidPEMDocument + } + } + + init( + rawRepresentation: some ContiguousBytes + ) throws { + try self.init( + wrapped: FFI.PrivateKey.deserialize(rawRepresentation: rawRepresentation) + ) + } + + init( + x963Representation: some ContiguousBytes + ) throws { + let length = x963Representation.withUnsafeBytes { $0.count } + guard length == Self.x963ByteCount else { + throw K1.Error.incorrectByteCountOfX963PrivateKey(got: length, expected: Self.x963ByteCount) + } + + let publicKeyX963 = x963Representation.bytes.prefix(K1._PublicKeyImplementation.x963ByteCount) + let publicKeyFromX963 = try K1._PublicKeyImplementation(x963Representation: publicKeyX963) + let privateKeyRaw = x963Representation.bytes.suffix(Self.rawByteCount) + try self.init(rawRepresentation: privateKeyRaw) + guard self.publicKey == publicKeyFromX963 else { + throw K1.Error.invalidPrivateX963RepresentationPublicKeyDiscrepancy + } + // All good + } + + /// `DER` + init(derRepresentation: some RandomAccessCollection) throws { + let bytes = Array(derRepresentation) + + // We have to try to parse this twice because we have no informaton about what kind of key this is. + // We try with PKCS#8 first, and then fall back to SEC.1. + do { + let key = try ASN1.PKCS8PrivateKey(asn1Encoded: bytes) + self = try .init(rawRepresentation: key.privateKey.privateKey) + } catch { + let key = try ASN1.SEC1PrivateKey(asn1Encoded: bytes) + self = try .init(rawRepresentation: key.privateKey) + } + } + + init() { + self.init(wrapped: .init()) + } +} + +// MARK: Serialize +extension K1._PrivateKeyImplementation { + /// A raw representation of the private key. + var rawRepresentation: Data { + Data(wrapped.secureBytes.bytes) + } + + /// A Distinguished Encoding Rules (DER) encoded representation of the private key. + var derRepresentation: Data { + let pkey = ASN1.PKCS8PrivateKey( + algorithm: .secp256k1, + privateKey: Array(self.rawRepresentation), + publicKey: Array(self.publicKey.x963Representation) + ) + var serializer = ASN1.Serializer() + + // Serializing these keys can't throw + try! serializer.serialize(pkey) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the private key. + var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// An ANSI x9.63 representation of the private key. + var x963Representation: Data { + // The x9.63 private key format is a discriminator byte (0x4) concatenated with the X and Y points + // of the key, and the K value of the secret scalar. Let's load that in. + var bytes = Data() + bytes.reserveCapacity(Self.x963ByteCount) + bytes.append(contentsOf: publicKey.x963Representation) + bytes.append(self.rawRepresentation) + return bytes + } + + static let rawByteCount = Curve.Field.byteCount + static let x963ByteCount = K1._PublicKeyImplementation.x963ByteCount + K1._PrivateKeyImplementation.rawByteCount +} + +extension K1._PrivateKeyImplementation { + static let pemType = "PRIVATE KEY" +} + +// MARK: - Equatable +extension K1._PrivateKeyImplementation { + /// Constant-time comparision. + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.secureBytes == rhs.wrapped.secureBytes + } +} + +// MARK: - Hashable +extension K1._PrivateKeyImplementation { + /// We use the key of the private key as input to hash + func hash(into hasher: inout Hasher) { + hasher.combine(self.publicKey) + } +} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift index bedec43..8436038 100644 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift +++ b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift @@ -1,62 +1,55 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-24. -// - import Foundation // TODO: Replace typealiases and this single existential to use existentials generated with GYB. public struct PrivateKeyOf: Sendable, Hashable, K1PrivateKeyProtocol { - public init() { - self.init(impl: .init()) - } - - public init(rawRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(rawRepresentation: rawRepresentation)) - } - - public init(x963Representation: some ContiguousBytes) throws { - try self.init(impl: .init(x963Representation: x963Representation)) - } - - public init(derRepresentation: some RandomAccessCollection) throws { - try self.init(impl: .init(derRepresentation: derRepresentation)) - } - - public init(pemRepresentation: String) throws { - try self.init(impl: .init(pemRepresentation: pemRepresentation)) - } - - public var rawRepresentation: Data { - impl.rawRepresentation - } - - public var x963Representation: Data { - impl.x963Representation - } - - public var derRepresentation: Data { - impl.derRepresentation - } - - public var pemRepresentation: String { - impl.pemRepresentation - } - - typealias Impl = K1.PrivateKeyImpl - internal let impl: Impl - internal let publicKeyImpl: K1.PublicKeyImpl - - public typealias PublicKey = Feature.PublicKey - - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } - - init(impl: Impl) { - self.impl = impl - self.publicKeyImpl = impl.publicKey - } + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = Feature.PublicKey + + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } } diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKey.swift b/Sources/K1/K1/Keys/PublicKey/PublicKey.swift deleted file mode 100644 index b1a3a9f..0000000 --- a/Sources/K1/K1/Keys/PublicKey/PublicKey.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation - -public protocol K1PublicKeyProtocol: K1KeyPortable { - init(compressedRepresentation: some ContiguousBytes) throws - var compressedRepresentation: Data { get } -} - -extension K1 { - - struct PublicKeyImpl: Sendable, Hashable, K1PublicKeyProtocol { - - typealias Wrapped = FFI.PublicKey.Wrapped - internal let wrapped: Wrapped - - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - } - } -} - -// MARK: Init -extension K1.PublicKeyImpl { - - - /// `04 || X || Y` (65 bytes) - static let x963ByteCount = FFI.PublicKey.x963ByteCount - - /// `X || Y` (64 bytes) - static let rawByteCount = FFI.PublicKey.rawByteCount - - /// `02|03 || X` (33 bytes) - static let compressedByteCount = FFI.PublicKey.compressedByteCount - - /// `X || Y` (64 bytes) - init(rawRepresentation: some ContiguousBytes) throws { - try self.init( - wrapped: FFI.PublicKey.deserialize(rawRepresentation: rawRepresentation) - ) - } - - /// `04 || X || Y` (65 bytes) - init(x963Representation: some ContiguousBytes) throws { - try self.init( - wrapped: FFI.PublicKey.deserialize(x963Representation: x963Representation) - ) - } - - /// `DER` - init(derRepresentation: some RandomAccessCollection) throws { - let bytes = [UInt8](derRepresentation) - let parsed = try ASN1.SubjectPublicKeyInfo(asn1Encoded: bytes) - self = try .init(x963Representation: parsed.key) - } - - /// `02|03 || X` (33 bytes) - init(compressedRepresentation: some ContiguousBytes) throws { - try self.init( - wrapped: FFI.PublicKey.deserialize(compressedRepresentation: compressedRepresentation) - ) - } - - /// Creates a `secp256k1` key from a Privacy-Enhanced Mail (PEM) representation. - init(pemRepresentation: String) throws { - let pem = try ASN1.PEMDocument(pemString: pemRepresentation) - guard pem.type == Self.pemType else { - throw K1.Error.invalidPEMDocument - } - self = try .init(derRepresentation: pem.derBytes) - } - -} - -extension K1.PublicKeyImpl { - static let pemType = "PUBLIC KEY" -} - -// MARK: Serialize -extension K1.PublicKeyImpl { - - /// `X || Y` (64 bytes) - var rawRepresentation: Data { - Data(x963Representation.dropFirst()) - } - - - /// `04 || X || Y` (65 bytes) - var x963Representation: Data { - try! FFI.PublicKey.serialize(wrapped, format: .uncompressed) - } - - /// `02|03 || X` (33 bytes) - var compressedRepresentation: Data { - try! FFI.PublicKey.serialize(wrapped, format: .compressed) - } - - /// `DER` - var derRepresentation: Data { - let spki = ASN1.SubjectPublicKeyInfo( - algorithmIdentifier: .secp256k1, - key: Array(self.x963Representation) - ) - var serializer = ASN1.Serializer() - - // Serializing these keys can't throw - try! serializer.serialize(spki) - return Data(serializer.serializedBytes) - } - - /// A Privacy-Enhanced Mail (PEM) representation of the public key. - public var pemRepresentation: String { - let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) - return pemDocument.pemString - } -} - -// MARK: Equatable -extension K1.PublicKeyImpl { - 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) - } - } - } - } -} - -// MARK: Hashable -extension K1.PublicKeyImpl { - func hash(into hasher: inout Hasher) { - wrapped.withUnsafeBytes { - hasher.combine(bytes: $0) - } - } -} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift b/Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift new file mode 100644 index 0000000..94a5ee2 --- /dev/null +++ b/Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift @@ -0,0 +1,135 @@ +import Foundation + +// MARK: - K1PublicKeyProtocol +public protocol K1PublicKeyProtocol: K1KeyPortable { + init(compressedRepresentation: some ContiguousBytes) throws + var compressedRepresentation: Data { get } +} + +// MARK: - K1._PublicKeyImplementation +extension K1 { + struct _PublicKeyImplementation: Sendable, Hashable, K1PublicKeyProtocol { + typealias Wrapped = FFI.PublicKey.Wrapped + internal let wrapped: Wrapped + + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } +} + +// MARK: Init +extension K1._PublicKeyImplementation { + /// `04 || X || Y` (65 bytes) + static let x963ByteCount = FFI.PublicKey.x963ByteCount + + /// `X || Y` (64 bytes) + static let rawByteCount = FFI.PublicKey.rawByteCount + + /// `02|03 || X` (33 bytes) + static let compressedByteCount = FFI.PublicKey.compressedByteCount + + /// `X || Y` (64 bytes) + init(rawRepresentation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(rawRepresentation: rawRepresentation) + ) + } + + /// `04 || X || Y` (65 bytes) + init(x963Representation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(x963Representation: x963Representation) + ) + } + + /// `DER` + init(derRepresentation: some RandomAccessCollection) throws { + let bytes = [UInt8](derRepresentation) + let parsed = try ASN1.SubjectPublicKeyInfo(asn1Encoded: bytes) + self = try .init(x963Representation: parsed.key) + } + + /// `02|03 || X` (33 bytes) + init(compressedRepresentation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(compressedRepresentation: compressedRepresentation) + ) + } + + /// Creates a `secp256k1` key from a Privacy-Enhanced Mail (PEM) representation. + init(pemRepresentation: String) throws { + let pem = try ASN1.PEMDocument(pemString: pemRepresentation) + guard pem.type == Self.pemType else { + throw K1.Error.invalidPEMDocument + } + self = try .init(derRepresentation: pem.derBytes) + } +} + +extension K1._PublicKeyImplementation { + static let pemType = "PUBLIC KEY" +} + +// MARK: Serialize +extension K1._PublicKeyImplementation { + /// `X || Y` (64 bytes) + var rawRepresentation: Data { + Data(x963Representation.dropFirst()) + } + + /// `04 || X || Y` (65 bytes) + var x963Representation: Data { + try! FFI.PublicKey.serialize(wrapped, format: .uncompressed) + } + + /// `02|03 || X` (33 bytes) + var compressedRepresentation: Data { + try! FFI.PublicKey.serialize(wrapped, format: .compressed) + } + + /// `DER` + var derRepresentation: Data { + let spki = ASN1.SubjectPublicKeyInfo( + algorithmIdentifier: .secp256k1, + key: Array(self.x963Representation) + ) + var serializer = ASN1.Serializer() + + // Serializing these keys can't throw + try! serializer.serialize(spki) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the public key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) + return pemDocument.pemString + } +} + +// MARK: Equatable +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) + } + } + } + } +} + +// MARK: Hashable +extension K1._PublicKeyImplementation { + func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } +} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift b/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift index 24b5613..2a12e9e 100644 --- a/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift +++ b/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift @@ -1,58 +1,50 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-24. -// - import Foundation // TODO: Replace typealiases and this single existential to use existentials generated with GYB. public struct PublicKeyOf: Sendable, Hashable, K1PublicKeyProtocol { - - public init(rawRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(rawRepresentation: rawRepresentation)) - } - - public init(compressedRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) - } - - public init(x963Representation: some ContiguousBytes) throws { - try self.init(impl: .init(x963Representation: x963Representation)) - } - - public init(derRepresentation: some RandomAccessCollection) throws { - try self.init(impl: .init(derRepresentation: derRepresentation)) - } - - public init(pemRepresentation: String) throws { - try self.init(impl: .init(pemRepresentation: pemRepresentation)) - } - - public var rawRepresentation: Data { - impl.rawRepresentation - } - - public var x963Representation: Data { - impl.x963Representation - } - - public var derRepresentation: Data { - impl.derRepresentation - } - - public var compressedRepresentation: Data { - impl.compressedRepresentation - } - - public var pemRepresentation: String { - impl.pemRepresentation - } - - typealias Impl = K1.PublicKeyImpl - internal let impl: Impl - internal init(impl: Impl) { - self.impl = impl - } + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } } diff --git a/Sources/K1/K1/Schnorr/Schnorr+Signature.swift b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift index 4c5ca37..dec60db 100644 --- a/Sources/K1/K1/Schnorr/Schnorr+Signature.swift +++ b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift @@ -1,33 +1,26 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation +// MARK: - K1.Schnorr.Signature extension K1.Schnorr { - public struct Signature: Sendable, Hashable { - - typealias Wrapped = FFI.Schnorr.Wrapped - internal let wrapped: Wrapped - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - } - } + public struct Signature: Sendable, Hashable { + typealias Wrapped = FFI.Schnorr.Wrapped + internal let wrapped: Wrapped + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } } // MARK: Init extension K1.Schnorr.Signature { - public init(rawRepresentation: some DataProtocol) throws { - try self.init(wrapped: .init(bytes: [UInt8](rawRepresentation))) - } + public init(rawRepresentation: some DataProtocol) throws { + try self.init(wrapped: .init(bytes: [UInt8](rawRepresentation))) + } } // MARK: Serialize extension K1.Schnorr.Signature { - public var rawRepresentation: Data { - wrapped.rawRepresentation - } + public var rawRepresentation: Data { + wrapped.rawRepresentation + } } diff --git a/Sources/K1/K1/Schnorr/Schnorr.swift b/Sources/K1/K1/Schnorr/Schnorr.swift index c5122c5..b7d42d6 100644 --- a/Sources/K1/K1/Schnorr/Schnorr.swift +++ b/Sources/K1/K1/Schnorr/Schnorr.swift @@ -1,147 +1,136 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-24. -// - -import Foundation import protocol CryptoKit.Digest import struct CryptoKit.SHA256 +import Foundation +// MARK: - K1.Schnorr extension K1 { - - /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` and Schnorr signature scheme. - public enum Schnorr: K1Feature { - - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically Schnorr signatures. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures, - /// more specifically Schnorr signatures. - public typealias PublicKey = PublicKeyOf - } + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` and Schnorr signature scheme. + public enum Schnorr: K1Feature { + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically Schnorr signatures. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically Schnorr signatures. + public typealias PublicKey = PublicKeyOf + } } // MARK: Sign extension K1.Schnorr.PrivateKey { - public func signature( - for hashed: some DataProtocol, - options: K1.Schnorr.SigningOptions = .default - ) throws -> K1.Schnorr.Signature { - try K1.Schnorr.Signature( - wrapped: FFI.Schnorr.sign( - hashedMessage: [UInt8](hashed), - privateKey: impl.wrapped, - options: options - ) - ) - } - - public func signature( - for digest: some Digest, - options: K1.Schnorr.SigningOptions = .default - ) throws -> K1.Schnorr.Signature { - try signature( - for: [UInt8](digest), - options: options - ) - } - - /// SHA256 hashes `unhashed` before signing it. - public func signature( - forUnhashed unhashed: some DataProtocol, - options: K1.Schnorr.SigningOptions = .default - ) throws -> K1.Schnorr.Signature { - try signature( - for: SHA256.hash(data: unhashed), - options: options - ) - } + public func signature( + for hashed: some DataProtocol, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try K1.Schnorr.Signature( + wrapped: FFI.Schnorr.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) + } + + public func signature( + for digest: some Digest, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try signature( + for: [UInt8](digest), + options: options + ) + } + + /// SHA256 hashes `unhashed` before signing it. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } } // MARK: Validate extension K1.Schnorr.PublicKey { - - /// Verifies a 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 signature is valid for the given _hashed_ data. - public func isValidSignature( - _ signature: K1.Schnorr.Signature, - hashed: some DataProtocol - ) -> Bool { - do { - return try FFI.Schnorr.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed) - ) - } catch { - return false - } - } - - - /// Verifies a Schnorr signature on a digest over the `secp256k1` elliptic curve. - /// - Parameters: - /// - signature: The Schnorr signature to check against the given digest. - /// - digest: The digest covered by the signature. - /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. - public func isValidSignature( - _ signature: K1.Schnorr.Signature, - digest: some Digest - ) -> Bool { - isValidSignature( - signature, - hashed: [UInt8](digest) - ) - } - - /// Verifies a Schnorr signature on a block of data over the `secp256k1` elliptic curve. - /// - /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use - /// `isValidSignature(_:hashed:input)` - /// . - /// - Parameters: - /// - signature: The Schnorr signature to check against the block of data. - /// - unhashed: The block of data covered by the signature. - /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. - public func isValidSignature( - _ signature: K1.Schnorr.Signature, - unhashed: some DataProtocol - ) -> Bool { - isValidSignature( - signature, - digest: SHA256.hash(data: unhashed) - ) - } - + /// Verifies a 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 signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed: some DataProtocol + ) -> Bool { + do { + return try FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed) + ) + } catch { + return false + } + } + + /// Verifies a Schnorr signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + digest: some Digest + ) -> Bool { + isValidSignature( + signature, + hashed: [UInt8](digest) + ) + } + + /// Verifies a Schnorr signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)` + /// . + /// - Parameters: + /// - signature: The Schnorr signature to check against the block of data. + /// - unhashed: The block of data covered by the signature. + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + unhashed: some DataProtocol + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed) + ) + } } +// MARK: - K1.Schnorr.SigningOptions extension K1.Schnorr { - public struct SigningOptions: Sendable, Hashable { - public let auxilaryRandomData: AuxilaryRandomData? - public init(auxilaryRandomData: AuxilaryRandomData? = nil) { - self.auxilaryRandomData = auxilaryRandomData - } - } + public struct SigningOptions: Sendable, Hashable { + public let auxilaryRandomData: AuxilaryRandomData? + public init(auxilaryRandomData: AuxilaryRandomData? = nil) { + self.auxilaryRandomData = auxilaryRandomData + } + } } extension K1.Schnorr.SigningOptions { - - public static let `default` = Self() - - public struct AuxilaryRandomData: Sendable, Hashable { - public let aux: [UInt8] - - public init(aux: some DataProtocol) throws { - guard aux.count == Curve.Field.byteCount else { - throw K1.Error.failedToSchnorrSignDigestProvidedRandomnessInvalidLength - } - self.aux = [UInt8](aux) - } - } + public static let `default` = Self() + + public struct AuxilaryRandomData: Sendable, Hashable { + public let aux: [UInt8] + + public init(aux: some DataProtocol) throws { + guard aux.count == Curve.Field.byteCount else { + throw K1.Error.failedToSchnorrSignDigestProvidedRandomnessInvalidLength + } + self.aux = [UInt8](aux) + } + } } diff --git a/Sources/K1/Support/Extensions/Data+Extensions.swift b/Sources/K1/Support/Extensions/Data+Extensions.swift index bac5bb4..3cd3557 100644 --- a/Sources/K1/Support/Extensions/Data+Extensions.swift +++ b/Sources/K1/Support/Extensions/Data+Extensions.swift @@ -1,20 +1,10 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - - import Foundation - extension ContiguousBytes { - - @inlinable - var bytes: [UInt8] { - withUnsafeBytes { pointer in - Array(pointer) - } - } + @inlinable + var bytes: [UInt8] { + withUnsafeBytes { pointer in + Array(pointer) + } + } } diff --git a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift index 7860103..0b8fe79 100644 --- a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift +++ b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift @@ -1,98 +1,85 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation import secp256k1 +// MARK: - FFI.ECDH extension FFI { - - /// Just a namespace for `FFI ECDH` - enum ECDH {} + /// Just a namespace for `FFI ECDH` + enum ECDH {} } -// MARK: SerializeFunction +// MARK: - FFI.ECDH.SerializeFunction extension FFI.ECDH { - - enum SerializeFunction { - - /// Using the `libsecp256k1` default behaviour. - /// - /// SHA256 hashes the **compressed** shared point. - /// Accepts arbitrary data passed through hash function as well. - /// - case libsecp256kDefault(arbitraryData: Data?) - - /// Following the [ANSI X9.63][ansix963] standard - /// - /// No hash, returns `X` component of shared point only. - /// - /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 - case ansiX963 - - /// Following no standard at all. - /// - /// No hash, returns the whole shared point. - /// - case noHashWholePoint - - func hashfp() -> (Optional<@convention(c) (Optional>, Optional>, Optional>, Optional) -> Int32>) { - switch self { - case .libsecp256kDefault: return secp256k1_ecdh_hash_function_default - case .ansiX963: return ecdh_asn1_x963 - case .noHashWholePoint: return ecdh_unsafe_whole_point - } - } - - var outputByteCount: Int { - switch self { - case .libsecp256kDefault: return Curve.Field.byteCount - case .ansiX963: return Curve.Field.byteCount - case .noHashWholePoint: return K1.Format.uncompressed.length - } - } - } + enum SerializeFunction { + /// Using the `libsecp256k1` default behaviour. + /// + /// SHA256 hashes the **compressed** shared point. + /// Accepts arbitrary data passed through hash function as well. + /// + case libsecp256kDefault(arbitraryData: Data?) + + /// Following the [ANSI X9.63][ansix963] standard + /// + /// No hash, returns `X` component of shared point only. + /// + /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 + case ansiX963 + + /// Following no standard at all. + /// + /// No hash, returns the whole shared point. + /// + case noHashWholePoint + + func hashfp() -> (Optional< @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 + } + } + + var outputByteCount: Int { + switch self { + case .libsecp256kDefault: return Curve.Field.byteCount + case .ansiX963: return Curve.Field.byteCount + case .noHashWholePoint: return K1.Format.uncompressed.length + } + } + } } // MARK: ECDH extension FFI.ECDH { - static func keyExchange( - publicKey: FFI.PublicKey.Wrapped, - privateKey: FFI.PrivateKey.Wrapped, - serializeOutputFunction hashFp: SerializeFunction - ) throws -> Data { - - var sharedPublicPointBytes = [UInt8]( - repeating: 0, - count: hashFp.outputByteCount - ) - var arbitraryData: [UInt8]? = { - switch hashFp { - case let .libsecp256kDefault(arbitraryData?): return [UInt8](arbitraryData) - case .libsecp256kDefault(.none): return nil - case .ansiX963, .noHashWholePoint: return nil - } - }() - var publicKeyRaw = publicKey.raw - try FFI.call( - ifFailThrow: .failedToPerformDiffieHellmanKeyExchange - ) { context in - secp256k1_ecdh( - context, - &sharedPublicPointBytes, // output - &publicKeyRaw, // pubkey - privateKey.secureBytes.backing.bytes, // seckey - hashFp.hashfp(), // hashfp - &arbitraryData // arbitrary data pointer that is passed through to hashfp - ) - } - - return Data(sharedPublicPointBytes) - - } -} - + static func keyExchange( + publicKey: FFI.PublicKey.Wrapped, + privateKey: FFI.PrivateKey.Wrapped, + serializeOutputFunction hashFp: SerializeFunction + ) throws -> Data { + var sharedPublicPointBytes = [UInt8]( + repeating: 0, + count: hashFp.outputByteCount + ) + var arbitraryData: [UInt8]? = { + switch hashFp { + case let .libsecp256kDefault(arbitraryData?): return [UInt8](arbitraryData) + case .libsecp256kDefault(.none): return nil + case .ansiX963, .noHashWholePoint: return nil + } + }() + var publicKeyRaw = publicKey.raw + try FFI.call( + ifFailThrow: .failedToPerformDiffieHellmanKeyExchange + ) { context in + secp256k1_ecdh( + context, + &sharedPublicPointBytes, // output + &publicKeyRaw, // pubkey + privateKey.secureBytes.backing.bytes, // seckey + hashFp.hashfp(), // hashfp + &arbitraryData // arbitrary data pointer that is passed through to hashfp + ) + } + return Data(sharedPublicPointBytes) + } +} diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift index 8324f86..6447a49 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -1,102 +1,100 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation import secp256k1 +// MARK: - FFI.ECDSA extension FFI { - public enum ECDSA {} + public enum ECDSA {} } + extension FFI.ECDSA { - public enum Recovery {} - public enum NonRecovery {} + public enum Recovery {} + public enum NonRecovery {} } - +// MARK: - RawECDSASignature protocol RawECDSASignature { - init() + init() } + +// MARK: - secp256k1_ecdsa_recoverable_signature + RawECDSASignature extension secp256k1_ecdsa_recoverable_signature: RawECDSASignature {} + +// MARK: - secp256k1_ecdsa_signature + RawECDSASignature extension secp256k1_ecdsa_signature: RawECDSASignature {} +// MARK: - WrappedECDSASignature protocol WrappedECDSASignature { - associatedtype Raw: RawECDSASignature - init(raw: Raw) - var raw: Raw { get } - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 + associatedtype Raw: RawECDSASignature + init(raw: Raw) + var raw: Raw { get } + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 } // MARK: ECDSA Shared extension FFI.ECDSA { - - internal static func _sign( - message: [UInt8], - privateKey: FFI.PrivateKey.Wrapped, - options: K1.ECDSA.SigningOptions = .default - ) throws -> WrappedSignature where WrappedSignature: WrappedECDSASignature { - guard message.count == Curve.Field.byteCount else { - throw K1.Error.unableToSignMessageHasInvalidLength( - got: message.count, - expected: Curve.Field.byteCount - ) - } - - var raw = WrappedSignature.Raw() - - try FFI.call( - ifFailThrow: .failedToECDSASignDigest - ) { context in - WrappedSignature.sign()( - context, - &raw, - message, - privateKey.secureBytes.backing.bytes, - options.nonceFunction.function(), - options.arbitraryData - ) - } - - return .init(raw: raw) - - } + internal static func _sign( + message: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> WrappedSignature where WrappedSignature: WrappedECDSASignature { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToSignMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + + var raw = WrappedSignature.Raw() + + try FFI.call( + ifFailThrow: .failedToECDSASignDigest + ) { context in + WrappedSignature.sign()( + context, + &raw, + message, + privateKey.secureBytes.backing.bytes, + options.nonceFunction.function(), + options.arbitraryData + ) + } + + return .init(raw: raw) + } } + extension K1.ECDSA.SigningOptions { - fileprivate var arbitraryData: [UInt8]? { - switch self.nonceFunction { - case .random: return nil - case let .deterministic(arbitraryData): return arbitraryData?.arbitraryData - } - } - + fileprivate var arbitraryData: [UInt8]? { + switch self.nonceFunction { + case .random: return nil + case let .deterministic(arbitraryData): return arbitraryData?.arbitraryData + } + } } + extension K1.ECDSA.SigningOptions.NonceFunction { - fileprivate func function() -> Optional<@convention(c) (Optional>, Optional>, Optional>, Optional>, Optional, UInt32) -> Int32> { - switch self { - case .deterministic: - return secp256k1_nonce_function_rfc6979 - - case .random: - return { ( - nonce32: UnsafeMutablePointer?, // Out: pointer to a 32-byte array to be filled by the function. - msg: UnsafePointer?, // In: the 32-byte message hash being verified (will not be NULL) - key32: UnsafePointer?, // In: pointer to a 32-byte secret key (will not be NULL) - algo16: UnsafePointer?, // In: pointer to a 16-byte array describing the signature algorithm (will be NULL for ECDSA for compatibility). - arbitraryData: UnsafeMutableRawPointer?, // In: Arbitrary data pointer that is passed through. - attemptIndex: UInt32 // In: how many iterations we have tried to find a nonce. This will almost always be 0, but different attempt values are required to result in a different nonce. - ) -> Int32 /* Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. */ in - - SecureBytes(count: Curve.Field.byteCount).withUnsafeBytes { - nonce32?.assign(from: $0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count) - } - - // Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. - return 1 - } - } - } + fileprivate func function() -> Optional< @convention(c) (UnsafeMutablePointer?, UnsafePointer?, UnsafePointer?, UnsafePointer?, UnsafeMutableRawPointer?, UInt32) -> Int32> { + switch self { + case .deterministic: + return secp256k1_nonce_function_rfc6979 + + case .random: + return { ( + nonce32: UnsafeMutablePointer?, // Out: pointer to a 32-byte array to be filled by the function. + _: UnsafePointer?, // In: the 32-byte message hash being verified (will not be NULL) + _: UnsafePointer?, // In: pointer to a 32-byte secret key (will not be NULL) + _: UnsafePointer?, // In: pointer to a 16-byte array describing the signature algorithm (will be NULL for ECDSA for compatibility). + _: UnsafeMutableRawPointer?, // In: Arbitrary data pointer that is passed through. + _: UInt32 // In: how many iterations we have tried to find a nonce. This will almost always be 0, but different attempt values are required to result in a different nonce. + ) -> Int32 /* Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. */ in + + SecureBytes(count: Curve.Field.byteCount).withUnsafeBytes { + nonce32?.assign(from: $0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count) + } + + // Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. + return 1 + } + } + } } - 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 6cbd0a6..7bdd3e9 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,34 +1,30 @@ import Foundation import secp256k1 -// MARK: Wrapped +// MARK: - FFI.ECDSA.NonRecovery.Wrapped extension FFI.ECDSA.NonRecovery { - - struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { - typealias Raw = secp256k1_ecdsa_signature - let raw: Raw - init(raw: Raw) { - self.raw = raw - } - } + struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { + typealias Raw = secp256k1_ecdsa_signature + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } } - // MARK: Sign extension FFI.ECDSA.NonRecovery.Wrapped { - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign - } + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { + secp256k1_ecdsa_sign + } } // MARK: ContiguousBytes extension FFI.ECDSA.NonRecovery.Wrapped { - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - var rawData = raw.data - return try Swift.withUnsafeBytes(of: &rawData) { pointer in - try body(pointer) - } - } + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } } - - 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 1936376..d9e3674 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 @@ -3,172 +3,166 @@ import secp256k1 // MARK: Deserialize extension FFI.ECDSA.NonRecovery { - static let byteCount = 2 * Curve.Field.byteCount - - /// Compact aka `IEEE P1363` aka `R||S`. - static func from( - compactBytes: [UInt8] - ) throws -> Wrapped { - try Wrapped( - raw: Raw.nonRecoverableSignature(compactBytes: compactBytes) - ) - } - - static func from( - derRepresentation: [UInt8] - ) throws -> Wrapped { - try Wrapped( - raw: Raw.nonRecoverableSignature(derBytes: derRepresentation) - ) - } + static let byteCount = 2 * Curve.Field.byteCount + + /// Compact aka `IEEE P1363` aka `R||S`. + static func from( + compactBytes: [UInt8] + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(compactBytes: compactBytes) + ) + } + + static func from( + derRepresentation: [UInt8] + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(derBytes: derRepresentation) + ) + } } // MARK: Serialize extension FFI.ECDSA.NonRecovery { - static func compact(_ wrapped: Wrapped) throws -> Data { - var out = [UInt8](repeating: 0, count: Self.byteCount) - var rawSignature = wrapped.raw - try FFI.call(ifFailThrow: .failedToSerializeSignature) { context in - secp256k1_ecdsa_signature_serialize_compact(context, &out, &rawSignature) - } - return Data(out) - } - - static func der( - _ wrapped: Wrapped - ) throws -> Data { - var derMaxLength = 75 // in fact max is 73, but we can have some margin. - var derSignature = [UInt8](repeating: 0, count: derMaxLength) - var rawSignature = wrapped.raw - try FFI.call(ifFailThrow: .failedToSerializeDERSignature) { context in - secp256k1_ecdsa_signature_serialize_der( - context, - &derSignature, - &derMaxLength, - &rawSignature - ) - } - return Data(derSignature.prefix(derMaxLength)) - } + static func compact(_ wrapped: Wrapped) throws -> Data { + var out = [UInt8](repeating: 0, count: Self.byteCount) + var rawSignature = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializeSignature) { context in + secp256k1_ecdsa_signature_serialize_compact(context, &out, &rawSignature) + } + return Data(out) + } + + static func der( + _ wrapped: Wrapped + ) throws -> Data { + var derMaxLength = 75 // in fact max is 73, but we can have some margin. + var derSignature = [UInt8](repeating: 0, count: derMaxLength) + var rawSignature = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializeDERSignature) { context in + secp256k1_ecdsa_signature_serialize_der( + context, + &derSignature, + &derMaxLength, + &rawSignature + ) + } + return Data(derSignature.prefix(derMaxLength)) + } } // MARK: Recover extension FFI.ECDSA.NonRecovery { - - static func recoverPublicKey( - _ wrapped: Wrapped, - recoveryID: Int32, - message: [UInt8] - ) throws -> FFI.PublicKey.Wrapped { - guard message.count == Curve.Field.byteCount else { - throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) - } - let nonRecoverableCompact = try FFI.ECDSA.NonRecovery.compact(wrapped) - return try Self.recoverPublicKey( - nonRecoverableCompact: nonRecoverableCompact, - recoveryID: recoveryID, - message: message - ) - } - - static func recoverPublicKey( - nonRecoverableCompact: Data, - recoveryID: Int32, - message: [UInt8] - ) throws -> FFI.PublicKey.Wrapped { - guard message.count == Curve.Field.byteCount else { - throw K1.Error.unableToRecoverMessageHasInvalidLength( - got: message.count, - expected: Curve.Field.byteCount - ) - } - var compact = [UInt8](nonRecoverableCompact) - var recoverable = secp256k1_ecdsa_recoverable_signature() - try FFI.call(ifFailThrow: .failedToParseRecoverableSignatureFromCompact) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &recoverable, - &compact, - recoveryID - ) - } - var publicKeyRaw = secp256k1_pubkey() - try FFI.call(ifFailThrow: .failedToRecoverPublicKey) { context in - secp256k1_ecdsa_recover( - context, - &publicKeyRaw, - &recoverable, - message - ) - } - return FFI.PublicKey.Wrapped(raw: publicKeyRaw) - } -} + static func recoverPublicKey( + _ wrapped: Wrapped, + recoveryID: Int32, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) + } + let nonRecoverableCompact = try FFI.ECDSA.NonRecovery.compact(wrapped) + return try Self.recoverPublicKey( + nonRecoverableCompact: nonRecoverableCompact, + recoveryID: recoveryID, + message: message + ) + } + static func recoverPublicKey( + nonRecoverableCompact: Data, + recoveryID: Int32, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + var compact = [UInt8](nonRecoverableCompact) + var recoverable = secp256k1_ecdsa_recoverable_signature() + try FFI.call(ifFailThrow: .failedToParseRecoverableSignatureFromCompact) { context in + secp256k1_ecdsa_recoverable_signature_parse_compact( + context, + &recoverable, + &compact, + recoveryID + ) + } + var publicKeyRaw = secp256k1_pubkey() + try FFI.call(ifFailThrow: .failedToRecoverPublicKey) { context in + secp256k1_ecdsa_recover( + context, + &publicKeyRaw, + &recoverable, + message + ) + } + return FFI.PublicKey.Wrapped(raw: publicKeyRaw) + } +} // MARK: Validate extension FFI.ECDSA.NonRecovery { - static func isValid( - ecdsaSignature: FFI.ECDSA.NonRecovery.Wrapped, - publicKey: FFI.PublicKey.Wrapped, - message: [UInt8], - options: K1.ECDSA.ValidationOptions = .default - ) throws -> Bool { - try FFI.toC { ffi -> Bool in - var publicKeyRaw = publicKey.raw - var maybeMalleable = ecdsaSignature.raw - var normalized = secp256k1_ecdsa_signature() - - 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 acceptMalleableSignatures = options.malleabilityStrictness == .accepted - switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { - case (true, false, _): - // Signature is valid - return true - case (true, true, true): - // Signature was valid but malleable, since you specified to - // accept malleability => considering signature valid. - return true - case (true, true, false): - // Signature was valid, but not normalized which was required => - // considering signature invalid. - return false - case (false, _, _): - // Signature is invalid. - return false - } - } - } - + static func isValid( + ecdsaSignature: FFI.ECDSA.NonRecovery.Wrapped, + publicKey: FFI.PublicKey.Wrapped, + message: [UInt8], + options: K1.ECDSA.ValidationOptions = .default + ) throws -> Bool { + try FFI.toC { ffi -> Bool in + var publicKeyRaw = publicKey.raw + var maybeMalleable = ecdsaSignature.raw + var normalized = secp256k1_ecdsa_signature() + + 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 acceptMalleableSignatures = options.malleabilityStrictness == .accepted + switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { + case (true, false, _): + // Signature is valid + return true + case (true, true, true): + // Signature was valid but malleable, since you specified to + // accept malleability => considering signature valid. + return true + case (true, true, false): + // Signature was valid, but not normalized which was required => + // considering signature invalid. + return false + case (false, _, _): + // Signature is invalid. + return false + } + } + } } - // MARK: Sign extension FFI.ECDSA.NonRecovery { - - /// Produces a **non recoverable** ECDSA signature from a hashed `message` - static func sign( - hashedMessage: [UInt8], - privateKey: FFI.PrivateKey.Wrapped, - options: K1.ECDSA.SigningOptions = .default - ) throws -> FFI.ECDSA.NonRecovery.Wrapped { - - try FFI.ECDSA._sign( - message: hashedMessage, - privateKey: privateKey, - options: options - ) - } + /// Produces a **non recoverable** ECDSA signature from a hashed `message` + static func sign( + hashedMessage: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> FFI.ECDSA.NonRecovery.Wrapped { + try FFI.ECDSA._sign( + message: hashedMessage, + privateKey: privateKey, + options: options + ) + } } 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 40cbe06..9afa158 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,33 +1,30 @@ import Foundation import secp256k1 - -// MARK: ECDSA Recovery Wrapped +// MARK: - FFI.ECDSA.Recovery.Wrapped extension FFI.ECDSA.Recovery { - struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { - typealias Raw = secp256k1_ecdsa_recoverable_signature - let raw: Raw - init(raw: Raw) { - self.raw = raw - } - } + struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { + typealias Raw = secp256k1_ecdsa_recoverable_signature + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } } // MARK: Sign extension FFI.ECDSA.Recovery.Wrapped { - - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign_recoverable - } + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { + secp256k1_ecdsa_sign_recoverable + } } // MARK: ContiguousBytes extension FFI.ECDSA.Recovery.Wrapped { - - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - var rawData = raw.data - return try Swift.withUnsafeBytes(of: &rawData) { pointer in - try body(pointer) - } - } + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } } 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 63c7094..03ec3fe 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 @@ -3,120 +3,115 @@ import secp256k1 // MARK: Deserialize extension FFI.ECDSA.Recovery { - static let byteCount = FFI.ECDSA.NonRecovery.byteCount + 1 - - static func deserialize( - rawRepresentation: some DataProtocol - ) throws -> Wrapped { - try Wrapped(raw: Raw.recoverableSignature(rawRepresentation)) - } - - /// Compact aka `IEEE P1363` aka `R||S`. - static func deserialize( - compact rs: [UInt8], - recoveryID recid: Int32 - ) throws -> Wrapped { - var raw = Wrapped.Raw() - try FFI.call( - ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact - ) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &raw, - rs, - recid - ) - } - return .init(raw: raw) - } -} + static let byteCount = FFI.ECDSA.NonRecovery.byteCount + 1 + + static func deserialize( + rawRepresentation: some DataProtocol + ) throws -> Wrapped { + try Wrapped(raw: Raw.recoverableSignature(rawRepresentation)) + } + /// Compact aka `IEEE P1363` aka `R||S`. + static func deserialize( + compact rs: [UInt8], + recoveryID recid: Int32 + ) throws -> Wrapped { + var raw = Wrapped.Raw() + try FFI.call( + ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact + ) { context in + secp256k1_ecdsa_recoverable_signature_parse_compact( + context, + &raw, + rs, + recid + ) + } + return .init(raw: raw) + } +} // MARK: Serialize extension FFI.ECDSA.Recovery { - static func serializeCompact( - _ wrapped: Wrapped - ) throws -> (rs: [UInt8], recoveryID: Int32) { - var rs = [UInt8](repeating: 0, count: FFI.ECDSA.NonRecovery.byteCount) - var recoveryID: Int32 = 0 - var rawSignature = wrapped.raw - try FFI.call( - ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact - ) { context in - secp256k1_ecdsa_recoverable_signature_serialize_compact( - context, - &rs, - &recoveryID, - &rawSignature - ) - } - return (rs, recoveryID) - } + static func serializeCompact( + _ wrapped: Wrapped + ) throws -> (rs: [UInt8], recoveryID: Int32) { + var rs = [UInt8](repeating: 0, count: FFI.ECDSA.NonRecovery.byteCount) + var recoveryID: Int32 = 0 + var rawSignature = wrapped.raw + try FFI.call( + ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact + ) { context in + secp256k1_ecdsa_recoverable_signature_serialize_compact( + context, + &rs, + &recoveryID, + &rawSignature + ) + } + return (rs, recoveryID) + } } // MARK: Convert extension FFI.ECDSA.Recovery { - static func nonRecoverable( - _ wrapped: Wrapped - ) throws -> FFI.ECDSA.NonRecovery.Wrapped { - - - var nonRecoverable = secp256k1_ecdsa_signature() - var recoverable = wrapped.raw - - try FFI.call( - ifFailThrow: .failedToConvertRecoverableSignatureToNonRecoverable - ) { context in - secp256k1_ecdsa_recoverable_signature_convert( - context, - &nonRecoverable, - &recoverable - ) - } - - return .init(raw: nonRecoverable) - } + static func nonRecoverable( + _ wrapped: Wrapped + ) throws -> FFI.ECDSA.NonRecovery.Wrapped { + var nonRecoverable = secp256k1_ecdsa_signature() + var recoverable = wrapped.raw + + try FFI.call( + ifFailThrow: .failedToConvertRecoverableSignatureToNonRecoverable + ) { context in + secp256k1_ecdsa_recoverable_signature_convert( + context, + &nonRecoverable, + &recoverable + ) + } + + return .init(raw: nonRecoverable) + } } // MARK: Recover extension FFI.ECDSA.Recovery { - static func recover( - _ wrapped: Wrapped, - message: [UInt8] - ) throws -> FFI.PublicKey.Wrapped { - guard message.count == Curve.Field.byteCount else { - throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) - } - var rawSignature = wrapped.raw - var rawPublicKey = secp256k1_pubkey() - try FFI.call( - ifFailThrow: .failedToRecoverPublicKey - ) { context in - secp256k1_ecdsa_recover( - context, - &rawPublicKey, - &rawSignature, - message - ) - } - return FFI.PublicKey.Wrapped(raw: rawPublicKey) - } + static func recover( + _ wrapped: Wrapped, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) + } + var rawSignature = wrapped.raw + var rawPublicKey = secp256k1_pubkey() + try FFI.call( + ifFailThrow: .failedToRecoverPublicKey + ) { context in + secp256k1_ecdsa_recover( + context, + &rawPublicKey, + &rawSignature, + message + ) + } + return FFI.PublicKey.Wrapped(raw: rawPublicKey) + } } // MARK: Sign extension FFI.ECDSA.Recovery { - - /// Produces a **recoverable** ECDSA signature from a hashed `message` - static func sign( - hashedMessage: [UInt8], - privateKey: K1.PrivateKeyImpl.Wrapped, - options: K1.ECDSA.SigningOptions = .default - ) throws -> FFI.ECDSA.Recovery.Wrapped { - - try FFI.ECDSA._sign( - message: hashedMessage, - privateKey: privateKey, - options: options - ) - } + /// Produces a **recoverable** ECDSA signature from a hashed `message` + static func sign( + hashedMessage: [UInt8], + privateKey: K1._PrivateKeyImplementation.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> FFI.ECDSA.Recovery.Wrapped { + try FFI.ECDSA._sign( + message: hashedMessage, + privateKey: privateKey, + 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 192073a..d2ca3e8 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -1,91 +1,83 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-18. -// - import Foundation import secp256k1 +// MARK: - FFI.PrivateKey extension FFI { - enum PrivateKey { - struct Wrapped: @unchecked Sendable { - - let publicKey: FFI.PublicKey.Wrapped - internal let secureBytes: SecureBytes - - fileprivate init(secureBytes: SecureBytes) throws { - guard secureBytes.count == Curve.Field.byteCount else { - throw K1.Error.failedToInitializePrivateKeyIncorrectByteCount( - got: secureBytes.count, - expected: Curve.Field.byteCount - ) - } - - if secureBytes.allSatisfy({ $0 == .zero }) { - throw K1.Error.invalidPrivateKeyMustNotBeZero - } - - self.secureBytes = secureBytes - var secureBytes = secureBytes - self.publicKey = try secureBytes.withUnsafeMutableBytes { seckey in - var raw = secp256k1_pubkey() - - try FFI.call(ifFailThrow: .invalidPrivateKeyMustBeSmallerThanOrder) { context in - secp256k1_ec_pubkey_create(context, &raw, seckey.baseAddress!) - } - - return FFI.PublicKey.Wrapped(raw: raw) - } - } - } - } + enum PrivateKey { + struct Wrapped: @unchecked Sendable { + let publicKey: FFI.PublicKey.Wrapped + internal let secureBytes: SecureBytes + + fileprivate init(secureBytes: SecureBytes) throws { + guard secureBytes.count == Curve.Field.byteCount else { + throw K1.Error.failedToInitializePrivateKeyIncorrectByteCount( + got: secureBytes.count, + expected: Curve.Field.byteCount + ) + } + + if secureBytes.allSatisfy({ $0 == .zero }) { + throw K1.Error.invalidPrivateKeyMustNotBeZero + } + + self.secureBytes = secureBytes + var secureBytes = secureBytes + self.publicKey = try secureBytes.withUnsafeMutableBytes { seckey in + var raw = secp256k1_pubkey() + + try FFI.call(ifFailThrow: .invalidPrivateKeyMustBeSmallerThanOrder) { context in + secp256k1_ec_pubkey_create(context, &raw, seckey.baseAddress!) + } + + return FFI.PublicKey.Wrapped(raw: raw) + } + } + } + } } // MARK: Init extension FFI.PrivateKey.Wrapped { - - fileprivate init(bytes: [UInt8]) throws { - try self.init(secureBytes: .init(bytes: bytes)) - } - - init() { - func generateNew() -> SecureBytes { - var attempt = 0 - - while attempt < 100 { - defer { attempt += 1 } - do { - let secureBytes = SecureBytes(count: Curve.Field.byteCount) - let _ = try FFI.PrivateKey.Wrapped(secureBytes: secureBytes) - return secureBytes - } catch { - // Failure (due to unlikely scenario that the private key scalar > order of the curve) => retry - } - } - - // Probability of this happening is: - // n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - // (n / 2^256) ^ 100 = lim 0 - // I.e. will not happen. - fatalError( - """ - Failed to generate private key after #\(attempt) attempts. - You are the most unlucky person in the universe. - Or by Occam's razor: the person writing this code made some error. - """ - ) - } - try! self.init(secureBytes: generateNew()) - } + fileprivate init(bytes: [UInt8]) throws { + try self.init(secureBytes: .init(bytes: bytes)) + } + + init() { + func generateNew() -> SecureBytes { + var attempt = 0 + + while attempt < 100 { + defer { attempt += 1 } + do { + let secureBytes = SecureBytes(count: Curve.Field.byteCount) + let _ = try FFI.PrivateKey.Wrapped(secureBytes: secureBytes) + return secureBytes + } catch { + // Failure (due to unlikely scenario that the private key scalar > order of the curve) => retry + } + } + + // Probability of this happening is: + // n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + // (n / 2^256) ^ 100 = lim 0 + // I.e. will not happen. + fatalError( + """ + Failed to generate private key after #\(attempt) attempts. + You are the most unlucky person in the universe. + Or by Occam's razor: the person writing this code made some error. + """ + ) + } + try! self.init(secureBytes: generateNew()) + } } // MARK: Deserialize extension FFI.PrivateKey { - static func deserialize( - rawRepresentation: some ContiguousBytes - ) throws -> Wrapped { - try Wrapped(bytes: rawRepresentation.bytes) - } + static func deserialize( + rawRepresentation: some ContiguousBytes + ) throws -> Wrapped { + try Wrapped(bytes: rawRepresentation.bytes) + } } 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 2ff6230..c688978 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -1,109 +1,95 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-20. -// - import Foundation import secp256k1 // MARK: Deserialize extension FFI.PublicKey { - - /// `04 || X || Y` (65 bytes) - public static let x963ByteCount = 1 + (2 * Curve.Field.byteCount) - - /// `X || Y` (64 bytes) - public static let rawByteCount = 2 * Curve.Field.byteCount + /// `04 || X || Y` (65 bytes) + public static let x963ByteCount = 1 + (2 * Curve.Field.byteCount) + + /// `X || Y` (64 bytes) + public static let rawByteCount = 2 * Curve.Field.byteCount + + /// `02|03 || X` (33 bytes) + public static let compressedByteCount = 1 + Curve.Field.byteCount + + /// `04 || X || Y` (65 bytes) + static func deserialize( + x963Representation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.x963ByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfX963PublicKey(got: bufferPointer.count, expected: expected) + } + return try Self._deserialize(bytes: [UInt8](bufferPointer)) + } + } + + /// `X || Y` (64 bytes) + static func deserialize( + rawRepresentation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.rawByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfRawPublicKey(got: bufferPointer.count, expected: expected) + } + // We can simply prepend `04` and parse as x963Representation + do { + return try Self.deserialize(x963Representation: [0x04] + [UInt8](bufferPointer)) + } catch { + throw K1.Error.unableToDeserializePublicKeyFromRawRepresentation + } + } + } + + /// `02|03 || X` (33 bytes) + static func deserialize( + compressedRepresentation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.compressedByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfCompressedPublicKey(got: bufferPointer.count, expected: expected) + } + return try Self._deserialize(bytes: [UInt8](bufferPointer)) + } + } - - /// `02|03 || X` (33 bytes) - public static let compressedByteCount = 1 + Curve.Field.byteCount - - /// `04 || X || Y` (65 bytes) - static func deserialize( - x963Representation contiguousBytes: some ContiguousBytes - ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in - let expected = Self.x963ByteCount - guard bufferPointer.count == expected else { - throw K1.Error.incorrectByteCountOfX963PublicKey(got: bufferPointer.count, expected: expected) - } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) - } - } - - /// `X || Y` (64 bytes) - static func deserialize( - rawRepresentation contiguousBytes: some ContiguousBytes - ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in - let expected = Self.rawByteCount - guard bufferPointer.count == expected else { - throw K1.Error.incorrectByteCountOfRawPublicKey(got: bufferPointer.count, expected: expected) - } - // We can simply prepend `04` and parse as x963Representation - do { - return try Self.deserialize(x963Representation: [0x04] + [UInt8](bufferPointer)) - } catch { - throw K1.Error.unableToDeserializePublicKeyFromRawRepresentation - } - } - } - - /// `02|03 || X` (33 bytes) - static func deserialize( - compressedRepresentation contiguousBytes: some ContiguousBytes - ) throws -> Wrapped { - - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in - let expected = Self.compressedByteCount - guard bufferPointer.count == expected else { - throw K1.Error.incorrectByteCountOfCompressedPublicKey(got: bufferPointer.count, expected: expected) - } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) - } - - } - - private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { - var raw = secp256k1_pubkey() - try FFI.call( - ifFailThrow: .failedToDeserializePublicKey - ) { context in - secp256k1_ec_pubkey_parse( - context, - &raw, - bytes, - bytes.count - ) - - } - return .init(raw: raw) - } + private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { + var raw = secp256k1_pubkey() + try FFI.call( + ifFailThrow: .failedToDeserializePublicKey + ) { context in + secp256k1_ec_pubkey_parse( + context, + &raw, + bytes, + bytes.count + ) + } + return .init(raw: raw) + } } // MARK: Serialize extension FFI.PublicKey { - - static func serialize( - _ wrapped: Wrapped, - format: K1.Format - ) throws -> Data { - var byteCount = format.length - var out = [UInt8](repeating: 0x00, count: byteCount) - var publicKeyRaw = wrapped.raw - try FFI.call(ifFailThrow: .failedToSerializePublicKey) { context in - secp256k1_ec_pubkey_serialize( - context, - &out, - &byteCount, - &publicKeyRaw, - format.rawValue - ) - } - return Data(out.prefix(byteCount)) - } - + static func serialize( + _ wrapped: Wrapped, + format: K1.Format + ) throws -> Data { + var byteCount = format.length + var out = [UInt8](repeating: 0x00, count: byteCount) + var publicKeyRaw = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializePublicKey) { context in + secp256k1_ec_pubkey_serialize( + context, + &out, + &byteCount, + &publicKeyRaw, + 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 cb90a45..15728d7 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -1,50 +1,41 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation import secp256k1 +// MARK: - FFI.PublicKey extension FFI { - enum PublicKey {} + enum PublicKey {} } -// MARK: PublicKey Wrapped +// MARK: - FFI.PublicKey.Wrapped extension FFI.PublicKey { - - struct Wrapped: @unchecked Sendable, ContiguousBytes { - typealias Raw = secp256k1_pubkey - let raw: Raw - init(raw: Raw) { - self.raw = raw - } - } + struct Wrapped: @unchecked Sendable, ContiguousBytes { + typealias Raw = secp256k1_pubkey + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } } // MARK: ContiguousBytes extension FFI.PublicKey.Wrapped { - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - var rawData = raw.data - return try Swift.withUnsafeBytes(of: &rawData) { pointer in - try body(pointer) - } - } + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } } // 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) 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 + } + } } - diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 9f4fcd4..220e289 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -1,97 +1,85 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation import secp256k1 +// MARK: - FFI.Schnorr extension FFI { - enum Schnorr {} + enum Schnorr {} } // MARK: Schnorr Validate 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() - var publicKeyRaw = publicKey.raw - try FFI.call(ifFailThrow: .failedToSchnorrVerifyGettingXFromPubKey) { context in - secp256k1_xonly_pubkey_from_pubkey( - context, - &publicKeyX, - nil, - &publicKeyRaw - ) - } - - return ffi.validate { context in - secp256k1_schnorrsig_verify( - context, - signature.bytes, - message, - message.count, - &publicKeyX - ) - } - } - } + 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() + var publicKeyRaw = publicKey.raw + try FFI.call(ifFailThrow: .failedToSchnorrVerifyGettingXFromPubKey) { context in + secp256k1_xonly_pubkey_from_pubkey( + context, + &publicKeyX, + nil, + &publicKeyRaw + ) + } + + return ffi.validate { context in + secp256k1_schnorrsig_verify( + context, + signature.bytes, + message, + message.count, + &publicKeyX + ) + } + } + } } // MARK: Schnorr Sign extension FFI.Schnorr { - - static func sign( - hashedMessage message: [UInt8], - privateKey: FFI.PrivateKey.Wrapped, - options: K1.Schnorr.SigningOptions = .default - ) throws -> FFI.Schnorr.Wrapped { - - guard - message.count == Curve.Field.byteCount - else { - throw K1.Error.unableToSignMessageHasInvalidLength( - got: message.count, - expected: Curve.Field.byteCount - ) - } - - var signatureOut = [UInt8](repeating: 0, count: FFI.Schnorr.Wrapped.byteCount) - - var keyPair = secp256k1_keypair() - - try FFI.call( - ifFailThrow: .failedToInitializeKeyPairForSchnorrSigning - ) { context in - secp256k1_keypair_create( - context, - &keyPair, - privateKey.secureBytes.backing.bytes - ) - } - - - try FFI.call( - ifFailThrow: .failedToSchnorrSignDigest - ) { context in - secp256k1_schnorrsig_sign32( - context, - &signatureOut, - message, - &keyPair, - options.auxilaryRandomData?.aux - ) - } - - return try FFI.Schnorr.Wrapped(bytes: signatureOut) - } + static func sign( + hashedMessage message: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.Schnorr.SigningOptions = .default + ) throws -> FFI.Schnorr.Wrapped { + guard + message.count == Curve.Field.byteCount + else { + throw K1.Error.unableToSignMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + + var signatureOut = [UInt8](repeating: 0, count: FFI.Schnorr.Wrapped.byteCount) + + var keyPair = secp256k1_keypair() + + try FFI.call( + ifFailThrow: .failedToInitializeKeyPairForSchnorrSigning + ) { context in + secp256k1_keypair_create( + context, + &keyPair, + privateKey.secureBytes.backing.bytes + ) + } + + try FFI.call( + ifFailThrow: .failedToSchnorrSignDigest + ) { context in + secp256k1_schnorrsig_sign32( + context, + &signatureOut, + message, + &keyPair, + options.auxilaryRandomData?.aux + ) + } + return try FFI.Schnorr.Wrapped(bytes: signatureOut) + } } diff --git a/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift b/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift index 0967bed..c1b4214 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift @@ -1,47 +1,41 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-20. -// - import Foundation +// MARK: - FFI.Schnorr.Wrapped extension FFI.Schnorr { - struct Wrapped: @unchecked Sendable, Hashable { - static let byteCount = 2 * Curve.Field.byteCount - - internal let bytes: [UInt8] - - init(bytes: [UInt8]) throws { - guard bytes.count == Self.byteCount else { - throw K1.Error.failedToInitSchnorrSignatureInvalidByteCount( - got: bytes.count, - expected: Self.byteCount - ) - } - self.bytes = bytes - } - } + struct Wrapped: @unchecked Sendable, Hashable { + static let byteCount = 2 * Curve.Field.byteCount + + internal let bytes: [UInt8] + + init(bytes: [UInt8]) throws { + guard bytes.count == Self.byteCount else { + throw K1.Error.failedToInitSchnorrSignatureInvalidByteCount( + got: bytes.count, + expected: Self.byteCount + ) + } + self.bytes = bytes + } + } } // MARK: Serialization extension FFI.Schnorr.Wrapped { - var rawRepresentation: Data { - Data(bytes) - } + var rawRepresentation: Data { + Data(bytes) + } } // MARK: Equatable extension FFI.Schnorr.Wrapped { - static func == (lhs: FFI.Schnorr.Wrapped, rhs: FFI.Schnorr.Wrapped) -> Bool { - lhs.bytes == rhs.bytes - } + static func == (lhs: FFI.Schnorr.Wrapped, rhs: FFI.Schnorr.Wrapped) -> Bool { + lhs.bytes == rhs.bytes + } } // MARK: Hashable extension FFI.Schnorr.Wrapped { - func hash(into hasher: inout Hasher) { - hasher.combine(bytes) - } + func hash(into hasher: inout Hasher) { + hasher.combine(bytes) + } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Call.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift index 44ef514..435350f 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Call.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -1,74 +1,64 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-28. -// - - import CryptoKit import secp256k1 +// MARK: - FFI final class FFI { - - let context: OpaquePointer - init() throws { - guard - /* "Create a secp256k1 context object." */ - let context = secp256k1_context_create(Context.sign.rawValue | Context.verify.rawValue) - else { - throw K1.Error.failedToCreateContextForSecp256k1 - } - - self.context = context - - } - - deinit { - secp256k1_context_destroy(context) - } + let context: OpaquePointer + init() throws { + guard + /* "Create a secp256k1 context object." */ + let context = secp256k1_context_create(Context.sign.rawValue | Context.verify.rawValue) + else { + throw K1.Error.failedToCreateContextForSecp256k1 + } + + self.context = context + } + + deinit { + secp256k1_context_destroy(context) + } } extension FFI { - - static func toC( - _ closure: (FFI) throws -> T - ) throws -> T { - let ffi = try FFI() - return try closure(ffi) - } - - /// Returns `true` iff result code is `1` - func validate( - _ method: (OpaquePointer) -> Int32 - ) -> Bool { - method(context) == 1 - } - - func callWithResultCode( - _ method: (OpaquePointer) -> Int32 - ) -> Int { - let result = method(context) - return Int(result) - } - - func call( - ifFailThrow error: K1.Error, - _ method: (OpaquePointer) -> Int32 - ) throws { - let result = callWithResultCode(method) - let successCode = 1 - guard result == successCode else { - throw error - } - } - - static func call( - ifFailThrow error: K1.Error, - _ method: (OpaquePointer) -> Int32 - ) throws { - try toC { ffi in - try ffi.call(ifFailThrow: error, method) - } - } + static func toC( + _ closure: (FFI) throws -> T + ) throws -> T { + let ffi = try FFI() + return try closure(ffi) + } + + /// Returns `true` iff result code is `1` + func validate( + _ method: (OpaquePointer) -> Int32 + ) -> Bool { + method(context) == 1 + } + + func callWithResultCode( + _ method: (OpaquePointer) -> Int32 + ) -> Int { + let result = method(context) + return Int(result) + } + + func call( + ifFailThrow error: K1.Error, + _ method: (OpaquePointer) -> Int32 + ) throws { + let result = callWithResultCode(method) + let successCode = 1 + guard result == successCode else { + throw error + } + } + + static func call( + ifFailThrow error: K1.Error, + _ method: (OpaquePointer) -> Int32 + ) throws { + try toC { ffi in + try ffi.call(ifFailThrow: error, method) + } + } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Context.swift b/Sources/K1/Support/FFI/Internals/FFI+Context.swift index 980c9bf..5c6e6db 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Context.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Context.swift @@ -1,40 +1,30 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - // FFI to C import secp256k1 +// MARK: - FFI.Context extension FFI { - - /// Bridging type used for underlying libsecp256k1 methods: - /// - `secp256k1_context_create` - /// - `secp256k1_context_preallocated_size` - /// - `secp256k1_context_preallocated_create` - enum Context: UInt32 { - case none, sign, verify - } - + /// Bridging type used for underlying libsecp256k1 methods: + /// - `secp256k1_context_create` + /// - `secp256k1_context_preallocated_size` + /// - `secp256k1_context_preallocated_create` + enum Context: UInt32 { + case none, sign, verify + } } - // MARK: Context extension FFI.Context { - - /// Bridging value used by libsecp256k1 methods that requires info about - /// how the context is used, e.g. for signing or verification (validate). - var rawValue: UInt32 { - let value: Int32 + /// Bridging value used by libsecp256k1 methods that requires info about + /// how the context is used, e.g. for signing or verification (validate). + var rawValue: UInt32 { + let value: Int32 - switch self { - case .none: value = SECP256K1_CONTEXT_NONE - case .sign: value = SECP256K1_CONTEXT_SIGN - case .verify: value = SECP256K1_CONTEXT_VERIFY - } + switch self { + case .none: value = SECP256K1_CONTEXT_NONE + case .sign: value = SECP256K1_CONTEXT_SIGN + case .verify: value = SECP256K1_CONTEXT_VERIFY + } - return UInt32(value) - } + return UInt32(value) + } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Format.swift b/Sources/K1/Support/FFI/Internals/FFI+Format.swift index 7634cd5..f8c91a9 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Format.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Format.swift @@ -1,53 +1,44 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-17. -// - import Foundation import secp256k1 // MARK: Format extension K1.Format { - - /// Bridging value used by libsecp256k1 key specifying the format - /// of the imported key, i.e. how many bytes. - public var rawValue: UInt32 { - let value: Int32 - switch self { - case .compressed: value = SECP256K1_EC_COMPRESSED - case .uncompressed: value = SECP256K1_EC_UNCOMPRESSED - } - - return UInt32(value) - } + /// Bridging value used by libsecp256k1 key specifying the format + /// of the imported key, i.e. how many bytes. + public var rawValue: UInt32 { + let value: Int32 + switch self { + case .compressed: value = SECP256K1_EC_COMPRESSED + case .uncompressed: value = SECP256K1_EC_UNCOMPRESSED + } + + return UInt32(value) + } } +// MARK: - K1.Format extension K1 { - - // Bridging type for: `secp256k1_ec_pubkey_serialize` - public enum Format: UInt32, CaseIterable { - case compressed, uncompressed - } + // Bridging type for: `secp256k1_ec_pubkey_serialize` + public enum Format: UInt32, CaseIterable { + case compressed, uncompressed + } } extension K1.Format { - - var length: Int { - switch self { - case .compressed: return 33 - case .uncompressed: return 65 - } - } - - internal init(byteCount: Int) throws { - if byteCount == Self.uncompressed.length { - self = .uncompressed - } else if byteCount == Self.compressed.length { - self = .compressed - } else { - fatalError("invalid byte count: \(byteCount)") - } - } + var length: Int { + switch self { + case .compressed: return 33 + case .uncompressed: return 65 + } + } + + internal init(byteCount: Int) throws { + if byteCount == Self.uncompressed.length { + self = .uncompressed + } else if byteCount == Self.compressed.length { + self = .compressed + } else { + fatalError("invalid byte count: \(byteCount)") + } + } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index d329378..8790a56 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -1,70 +1,61 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-17. -// - import Foundation import secp256k1 +// MARK: - Raw enum Raw {} extension Raw { - - static func recoverableSignature( - _ rawRepresentation: some DataProtocol - ) throws -> secp256k1_ecdsa_recoverable_signature { - let expected = K1.ECDSA.Recoverable.Signature.Compact.byteCount - guard - rawRepresentation.count == expected - else { - throw K1.Error.incorrectByteCountOfRawRecoverableSignature( - got: rawRepresentation.count, - expected: expected - ) - } - var raw = secp256k1_ecdsa_recoverable_signature() - withUnsafeMutableBytes(of: &raw.data) { pointer in - pointer.copyBytes( - from: rawRepresentation.prefix(pointer.count) - ) - } - return raw - } - - static func nonRecoverableSignature( - compactBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - - var raw = secp256k1_ecdsa_signature() - - try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in - secp256k1_ecdsa_signature_parse_compact( - context, - &raw, - compactBytes - ) - } - - return raw - } - - static func nonRecoverableSignature( - derBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - - var raw = secp256k1_ecdsa_signature() - - try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in - secp256k1_ecdsa_signature_parse_der( - context, - &raw, - derBytes, - derBytes.count - ) - } - - return raw - } + static func recoverableSignature( + _ rawRepresentation: some DataProtocol + ) throws -> secp256k1_ecdsa_recoverable_signature { + let expected = K1.ECDSA.Recoverable.Signature.Compact.byteCount + guard + rawRepresentation.count == expected + else { + throw K1.Error.incorrectByteCountOfRawRecoverableSignature( + got: rawRepresentation.count, + expected: expected + ) + } + var raw = secp256k1_ecdsa_recoverable_signature() + withUnsafeMutableBytes(of: &raw.data) { pointer in + pointer.copyBytes( + from: rawRepresentation.prefix(pointer.count) + ) + } + return raw + } + + static func nonRecoverableSignature( + compactBytes: [UInt8] + ) throws -> secp256k1_ecdsa_signature { + var raw = secp256k1_ecdsa_signature() + + try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in + secp256k1_ecdsa_signature_parse_compact( + context, + &raw, + compactBytes + ) + } + + return raw + } + + static func nonRecoverableSignature( + derBytes: [UInt8] + ) throws -> secp256k1_ecdsa_signature { + var raw = secp256k1_ecdsa_signature() + + try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in + secp256k1_ecdsa_signature_parse_der( + context, + &raw, + derBytes, + derBytes.count + ) + } + + return raw + } } diff --git a/Sources/K1/Support/Misc/Curve.swift b/Sources/K1/Support/Misc/Curve.swift index ac2bac9..c765b5e 100644 --- a/Sources/K1/Support/Misc/Curve.swift +++ b/Sources/K1/Support/Misc/Curve.swift @@ -1,24 +1,16 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation // MARK: - Curve - /// Details about the elliptic curve `secp256k1`. enum Curve {} -// MARK: - FiniteField +// MARK: Curve.Field extension Curve { - /// The finite field of the secp256k1 curve. - enum Field {} + /// The finite field of the secp256k1 curve. + enum Field {} } extension Curve.Field { - /// Finite field members are 256 bits large, i.e. 32 bytes. - static let byteCount = 32 + /// Finite field members are 256 bits large, i.e. 32 bytes. + static let byteCount = 32 } diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index d2ca959..7f261f7 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -1,57 +1,49 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-17. -// - import Foundation extension K1 { - enum Error: Sendable, Swift.Error, Hashable { - case invalidPEMDocument - case invalidPrivateKeyMustNotBeZero - case invalidPrivateKeyMustBeSmallerThanOrder - case failedToProduceSharedSecret - case failedToComputePublicKeyFromPrivateKey - case failedToPerformDiffieHellmanKeyExchange - case failedToInitializePrivateKeyIncorrectByteCount(got: Int, expected: Int) - case failedToCreateContextForSecp256k1 - case failedToDeserializePublicKey - case failedToSerializePublicKey - case failedToSchnorrVerifyGettingXFromPubKey - case incorrectByteCountOfX963PrivateKey(got: Int, expected: Int) - - /// The public key encoded in the x963 privateKey representation does not match the derived public key from the private key. - case invalidPrivateX963RepresentationPublicKeyDiscrepancy - - case incorrectByteCountOfX963PublicKey(got: Int, expected: Int) - case incorrectByteCountOfRawPublicKey(got: Int, expected: Int) - case unableToDeserializePublicKeyFromRawRepresentation - case incorrectByteCountOfCompressedPublicKey(got: Int, expected: Int) - case failedSignatureToConvertRecoverableSignatureToCompact - case failedToConvertRecoverableSignatureToNonRecoverable - case failedToRecoverPublicKey - case unableToSignMessageHasInvalidLength(got: Int, expected: Int) - case failedToInitializeKeyPairForSchnorrSigning - case failedToSchnorrSignDigestProvidedRandomnessInvalidLength - case failedToSchnorrSignDigest - case failedToInitSchnorrSignatureInvalidByteCount(got: Int, expected: Int) - case failedToSchnorrSignMessageInvalidLength - case incorrectByteCountOfArbitraryDataForNonceFunction - case invalidByteCount - case failedToECDSASignDigest - case unableToRecoverMessageHasInvalidLength(got: Int, expected: Int) - case failedToComparePublicKeys - case failedToSerializeDERSignature - case failedToSerializeSignature - case failedToParseRecoverableSignatureFromCompact - case incorrectByteCountOfRawSignature - case incorrectByteCountOfRawRecoverableSignature(got: Int, expected: Int) - case failedToParseNonRecoverableSignatureFromCompactRepresentation - case failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: Int, expected: Int) - case failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount(got: Int, expected: Int) - case invalidRecoveryID(got: Int) - } -} + enum Error: Sendable, Swift.Error, Hashable { + case invalidPEMDocument + case invalidPrivateKeyMustNotBeZero + case invalidPrivateKeyMustBeSmallerThanOrder + case failedToProduceSharedSecret + case failedToComputePublicKeyFromPrivateKey + case failedToPerformDiffieHellmanKeyExchange + case failedToInitializePrivateKeyIncorrectByteCount(got: Int, expected: Int) + case failedToCreateContextForSecp256k1 + case failedToDeserializePublicKey + case failedToSerializePublicKey + case failedToSchnorrVerifyGettingXFromPubKey + case incorrectByteCountOfX963PrivateKey(got: Int, expected: Int) + /// The public key encoded in the x963 privateKey representation does not match the derived public key from the private key. + case invalidPrivateX963RepresentationPublicKeyDiscrepancy + + case incorrectByteCountOfX963PublicKey(got: Int, expected: Int) + case incorrectByteCountOfRawPublicKey(got: Int, expected: Int) + case unableToDeserializePublicKeyFromRawRepresentation + case incorrectByteCountOfCompressedPublicKey(got: Int, expected: Int) + case failedSignatureToConvertRecoverableSignatureToCompact + case failedToConvertRecoverableSignatureToNonRecoverable + case failedToRecoverPublicKey + case unableToSignMessageHasInvalidLength(got: Int, expected: Int) + case failedToInitializeKeyPairForSchnorrSigning + case failedToSchnorrSignDigestProvidedRandomnessInvalidLength + case failedToSchnorrSignDigest + case failedToInitSchnorrSignatureInvalidByteCount(got: Int, expected: Int) + case failedToSchnorrSignMessageInvalidLength + case incorrectByteCountOfArbitraryDataForNonceFunction + case invalidByteCount + case failedToECDSASignDigest + case unableToRecoverMessageHasInvalidLength(got: Int, expected: Int) + case failedToComparePublicKeys + case failedToSerializeDERSignature + case failedToSerializeSignature + case failedToParseRecoverableSignatureFromCompact + case incorrectByteCountOfRawSignature + case incorrectByteCountOfRawRecoverableSignature(got: Int, expected: Int) + case failedToParseNonRecoverableSignatureFromCompactRepresentation + case failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: Int, expected: Int) + case failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount(got: Int, expected: Int) + case invalidRecoveryID(got: Int) + } +} diff --git a/Sources/K1/Support/Misc/SharedSecret.swift b/Sources/K1/Support/Misc/SharedSecret.swift index 30c0b1f..f129f48 100644 --- a/Sources/K1/Support/Misc/SharedSecret.swift +++ b/Sources/K1/Support/Misc/SharedSecret.swift @@ -1,34 +1,27 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-17. -// - import Foundation // MUST match https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L34 import struct CryptoKit.SharedSecret +// MARK: - __SharedSecret /// A Key Agreement Result /// A SharedSecret has to go through a Key Derivation Function before being able to use by a symmetric key operation. struct __SharedSecret { - var ss: SecureBytes - - internal init(ss: SecureBytes) { - self.ss = ss - } -} + var ss: SecureBytes + internal init(ss: SecureBytes) { + self.ss = ss + } +} extension CryptoKit.SharedSecret { - init(data: Data) throws { - let __sharedSecret = __SharedSecret(ss: .init(bytes: data)) - let sharedSecret = unsafeBitCast(__sharedSecret, to: SharedSecret.self) - guard sharedSecret.withUnsafeBytes({ Data($0).count == data.count }) else { - throw K1.Error.failedToProduceSharedSecret - } - - self = sharedSecret - } + init(data: Data) throws { + let __sharedSecret = __SharedSecret(ss: .init(bytes: data)) + let sharedSecret = unsafeBitCast(__sharedSecret, to: SharedSecret.self) + guard sharedSecret.withUnsafeBytes({ Data($0).count == data.count }) else { + throw K1.Error.failedToProduceSharedSecret + } + + self = sharedSecret + } } diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift index 145e796..79c71e0 100644 --- a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift @@ -1,80 +1,71 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-03. -// - import Foundation @testable import K1 import XCTest +// MARK: - ECDHTests final class ECDHTests: XCTestCase { - - func testECDHX963() throws { - let alice = K1.KeyAgreement.PrivateKey() - let bob = K1.KeyAgreement.PrivateKey() - XCTAssertNotEqual(alice, bob) - let ab = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) - let ba = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) - ab.withUnsafeBytes { - XCTAssertEqual(Data($0).count, 32) - } - XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") - } - - func testECDHLibsecp256k1() throws { - let alice = K1.KeyAgreement.PrivateKey() - let bob = K1.KeyAgreement.PrivateKey() - XCTAssertNotEqual(alice, bob) - let ab = try alice.ecdh(with: bob.publicKey) - let ba = try bob.ecdh(with: alice.publicKey) - ab.withUnsafeBytes { - XCTAssertEqual(Data($0).count, 32) - } - XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") - } - - func testECDHPoint() throws { - let alice = K1.KeyAgreement.PrivateKey() - let bob = K1.KeyAgreement.PrivateKey() - XCTAssertNotEqual(alice, bob) - let ab = try alice.ecdhPoint(with: bob.publicKey) - let ba = try bob.ecdhPoint(with: alice.publicKey) - XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") - XCTAssertEqual(ab.count, 65) - } - - /// Test vectors from: https://crypto.stackexchange.com/q/57695 - func test_crypto_stackexchange_vector() throws { - let privateKey1 = try K1.KeyAgreement.PrivateKey(hex: "82fc9947e878fc7ed01c6c310688603f0a41c8e8704e5b990e8388343b0fd465") - let privateKey2 = try K1.KeyAgreement.PrivateKey(hex: "5f706787ac72c1080275c1f398640fb07e9da0b124ae9734b28b8d0f01eda586") - - let libsecp256k1 = try privateKey1.ecdh(with: privateKey2.publicKey) - let ans1X963 = try privateKey1.sharedSecretFromKeyAgreement(with: privateKey2.publicKey).withUnsafeBytes({ Data($0) }) - - libsecp256k1.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, "5935d0476af9df2998efb60383adf2ff23bc928322cfbb738fca88e49d557d7e") - } - - XCTAssertEqual(ans1X963.hex, "3a17fe5fa33c4f2c7e61799a65061214913f39bfcbee178ab351493d5ee17b2f") - - } - - /// Assert we do not introduce any regression bugs for the custom ECDh `ecdhPoint` - func testECDHCustom() throws { - let alice = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xAA, count: 32)) - let bob = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xBB, count: 32)) - let ab = try alice.ecdhPoint(with: bob.publicKey) - let ba = try bob.ecdhPoint(with: alice.publicKey) - XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") - XCTAssertEqual(ab.hex, "041d3e7279da3f845c4246087cdd3dd42bea3dea7245ceaf75609d8eb0a4e89c4e8e7a7c012045a2eae87463012468d7aae911b8a1140e240c828c96d9b19bd8e7") - - } + func testECDHX963() throws { + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) + let ab = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) + let ba = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) + ab.withUnsafeBytes { + XCTAssertEqual(Data($0).count, 32) + } + XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") + } + + func testECDHLibsecp256k1() throws { + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) + let ab = try alice.ecdh(with: bob.publicKey) + let ba = try bob.ecdh(with: alice.publicKey) + ab.withUnsafeBytes { + XCTAssertEqual(Data($0).count, 32) + } + XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") + } + + func testECDHPoint() throws { + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) + let ab = try alice.ecdhPoint(with: bob.publicKey) + let ba = try bob.ecdhPoint(with: alice.publicKey) + XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") + XCTAssertEqual(ab.count, 65) + } + + /// Test vectors from: https://crypto.stackexchange.com/q/57695 + func test_crypto_stackexchange_vector() throws { + let privateKey1 = try K1.KeyAgreement.PrivateKey(hex: "82fc9947e878fc7ed01c6c310688603f0a41c8e8704e5b990e8388343b0fd465") + let privateKey2 = try K1.KeyAgreement.PrivateKey(hex: "5f706787ac72c1080275c1f398640fb07e9da0b124ae9734b28b8d0f01eda586") + + let libsecp256k1 = try privateKey1.ecdh(with: privateKey2.publicKey) + let ans1X963 = try privateKey1.sharedSecretFromKeyAgreement(with: privateKey2.publicKey).withUnsafeBytes { Data($0) } + + libsecp256k1.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, "5935d0476af9df2998efb60383adf2ff23bc928322cfbb738fca88e49d557d7e") + } + + XCTAssertEqual(ans1X963.hex, "3a17fe5fa33c4f2c7e61799a65061214913f39bfcbee178ab351493d5ee17b2f") + } + + /// Assert we do not introduce any regression bugs for the custom ECDh `ecdhPoint` + func testECDHCustom() throws { + let alice = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xAA, count: 32)) + let bob = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xBB, count: 32)) + let ab = try alice.ecdhPoint(with: bob.publicKey) + let ba = try bob.ecdhPoint(with: alice.publicKey) + XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") + XCTAssertEqual(ab.hex, "041d3e7279da3f845c4246087cdd3dd42bea3dea7245ceaf75609d8eb0a4e89c4e8e7a7c012045a2eae87463012468d7aae911b8a1140e240c828c96d9b19bd8e7") + } } extension K1.KeyAgreement.PrivateKey { - init(hex: String) throws { - self = try Self(rawRepresentation: Data(hex: hex)) - } + init(hex: String) throws { + self = try Self(rawRepresentation: Data(hex: hex)) + } } diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHWycheproofTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHWycheproofTests.swift new file mode 100644 index 0000000..c3c5d28 --- /dev/null +++ b/Tests/K1Tests/TestCases/ECDH/ECDHWycheproofTests.swift @@ -0,0 +1,128 @@ +// swiftformat:disable strip + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// From: https://github.com/apple/swift-crypto/blob/main/Tests/CryptoTests/ECDH/secpECDH_Runner.swift +// Commit: 794901c991bf3fa0431ba3c0927ba078799c6911 + +// swiftformat:enable strip + +import Foundation +@testable import K1 +import XCTest + +// MARK: - ECDHWycheproofTests +final class ECDHWycheproofTests: XCTestCase { + func testECDHWycheproof() throws { + let _ = try testSuite( + jsonName: "wycheproof_ecdh_ASN1x963", + testFunction: { (group: ECDHTestGroup) in + testGroup(group: group) + } + ) + } +} + +private extension ECDHWycheproofTests { + func testGroup( + group: ECDHTestGroup, + skipIfContainsAnyFlag flagsForUnsupportedTestVectors: [String] = ["InvalidAsn", "CompressedPoint", "InvalidPublic", "UnnamedCurve"], + file: StaticString = #file, line: UInt = #line + ) -> ResultOfTestGroup { + func padKeyIfNecessary(vector: String, file: StaticString = #file, line: UInt = #line) throws -> [UInt8] { + // There are a few edge cases here. + // + // First, our raw bytes function requires the + // input buffer to be exactly as long as the curve size. + // + // Second, Wycheproof inputs may be too short or too long with + // leading zeros. + let curveSize = 32 // K1.Curve.Field.byteCount + var privateBytes = [UInt8](repeating: 0, count: curveSize) + + let hexFromVector = (vector.count % 2 == 0) ? vector : "0\(vector)" + let privateKeyVector = try! Array(hex: hexFromVector) + + // Input is too long (i.e. we have leading zeros) + if privateKeyVector.count > curveSize { + privateBytes = privateKeyVector.suffix(curveSize) + } else if privateKeyVector.count == curveSize { + privateBytes = privateKeyVector + } else { + // Input is too short + privateBytes.replaceSubrange((privateBytes.count - privateKeyVector.count) ..< privateBytes.count, with: privateKeyVector) + } + + return privateBytes + } + + var idsOfOmittedTests = [Int]() + var numberOfTestsRun = 0 + + for testVector in group.tests { + if !Set(testVector.flags).isDisjoint(with: Set(flagsForUnsupportedTestVectors)) { + idsOfOmittedTests.append(testVector.tcId) + continue + } + numberOfTestsRun += 1 + do { + let publicKey = try K1.KeyAgreement.PublicKey(derRepresentation: Data(hex: testVector.publicKey)) + var privateBytes = [UInt8]() + privateBytes = try padKeyIfNecessary(vector: testVector.privateKey) + let privateKey = try K1.KeyAgreement.PrivateKey(rawRepresentation: privateBytes) + + /// ANS1 X9.63 serialization of shared secret, returning a `CryptoKit.SharedSecret` + let sharedPublicKeyPoint = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + let got = sharedPublicKeyPoint.withUnsafeBytes { + Data($0) + } + XCTAssertEqual(got.hex, testVector.shared, file: file, line: line) + } catch { + if testVector.result != "invalid" { + XCTFail("Failed with error: \(String(describing: error)), test vector: \(String(describing: testVector))") + } + } + } + + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: idsOfOmittedTests) + } +} + +// MARK: - ECDHTestGroup +struct ECDHTestGroup: Codable { + let curve: String + let tests: [ECDHTestVector] +} + +// MARK: - ECDHTestVector +struct ECDHTestVector: Codable { + let comment: String + let publicKey: String + let privateKey: String + let shared: String + let result: String + let tcId: Int + let flags: [String] + + enum CodingKeys: String, CodingKey { + case publicKey = "public" + case privateKey = "private" + case comment + case shared + case result + case tcId + case flags + } +} diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift deleted file mode 100644 index 0698e03..0000000 --- a/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift +++ /dev/null @@ -1,122 +0,0 @@ -// From: https://github.com/apple/swift-crypto/blob/main/Tests/CryptoTests/ECDH/secpECDH_Runner.swift -// Commit: 794901c991bf3fa0431ba3c0927ba078799c6911 - -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftCrypto open source project -// -// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.md for the list of SwiftCrypto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import Foundation -import XCTest -@testable import K1 - -final class ECDHWychoproofTests: XCTestCase { - - func testECDHWycheproof() throws { - let result = try testSuite( - jsonName: "wycheproof_ecdh_ASN1x963", - testFunction: { (group: ECDHTestGroup) in - testGroup(group: group) - }) - - print("☑️ \(String(describing: result))") - } -} - -private extension ECDHWychoproofTests { - func testGroup( - group: ECDHTestGroup, - skipIfContainsAnyFlag flagsForUnsupportedTestVectors: [String] = ["InvalidAsn", "CompressedPoint", "InvalidPublic", "UnnamedCurve"], - file: StaticString = #file, line: UInt = #line - ) -> ResultOfTestGroup { - func padKeyIfNecessary(vector: String, file: StaticString = #file, line: UInt = #line) throws -> [UInt8] { - // There are a few edge cases here. - // - // First, our raw bytes function requires the - // input buffer to be exactly as long as the curve size. - // - // Second, Wycheproof inputs may be too short or too long with - // leading zeros. - let curveSize = 32 //K1.Curve.Field.byteCount - var privateBytes = [UInt8](repeating: 0, count: curveSize) - - let hexFromVector = (vector.count % 2 == 0) ? vector : "0\(vector)" - let privateKeyVector = try! Array(hex: hexFromVector) - - // Input is too long (i.e. we have leading zeros) - if privateKeyVector.count > curveSize { - privateBytes = privateKeyVector.suffix(curveSize) - } else if privateKeyVector.count == curveSize { - privateBytes = privateKeyVector - } else { - // Input is too short - privateBytes.replaceSubrange((privateBytes.count - privateKeyVector.count) ..< privateBytes.count, with: privateKeyVector) - } - - return privateBytes - } - - var idsOfOmittedTests = Array() - var numberOfTestsRun = 0 - - for testVector in group.tests { - if !Set(testVector.flags).isDisjoint(with: Set(flagsForUnsupportedTestVectors)) { - idsOfOmittedTests.append(testVector.tcId) - continue - } - numberOfTestsRun += 1 - do { - let publicKey = try K1.KeyAgreement.PublicKey(derRepresentation: Data(hex: testVector.publicKey)) - var privateBytes = [UInt8]() - privateBytes = try padKeyIfNecessary(vector: testVector.privateKey) - let privateKey = try K1.KeyAgreement.PrivateKey(rawRepresentation: privateBytes) - - /// ANS1 X9.63 serialization of shared secret, returning a `CryptoKit.SharedSecret` - let sharedPublicKeyPoint = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) - let got = sharedPublicKeyPoint.withUnsafeBytes { - Data($0) - } - XCTAssertEqual(got.hex, testVector.shared, file: file, line: line) - } catch { - if testVector.result != "invalid" { - XCTFail("Failed with error: \(String(describing: error)), test vector: \(String(describing: testVector))") - } - } - } - - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests:idsOfOmittedTests) - } -} - -struct ECDHTestGroup: Codable { - let curve: String - let tests: [ECDHTestVector] -} - -struct ECDHTestVector: Codable { - let comment: String - let publicKey: String - let privateKey: String - let shared: String - let result: String - let tcId: Int - let flags: [String] - - enum CodingKeys: String, CodingKey { - case publicKey = "public" - case privateKey = "private" - case comment - case shared - case result - case tcId - case flags - } -} diff --git a/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift b/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift index af223b0..af76aa6 100644 --- a/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift @@ -1,127 +1,122 @@ +import CryptoKit import Foundation @testable import K1 -import CryptoKit import XCTest +// MARK: - ECDHX963Suite private struct ECDHX963Suite: Decodable { - let origin: String - let author: String - let description: String - let numberOfTests: Int - let algorithm: String - let generatedOn: String // data - let vectors: [Vector] + let origin: String + let author: String + let description: String + let numberOfTests: Int + let algorithm: String + let generatedOn: String // data + let vectors: [Vector] } +// MARK: - Vector private struct Vector: Decodable { - let alicePrivateKey: String - let bobPrivateKey: String - let alicePublicKeyUncompressed: String - let bobPublicKeyUncompressed: String - let outcomes: [Outcome] - - struct Outcome: Decodable { - enum ECDHVariant: String, Decodable { - case asn1X963 = "ASN1X963" - case libsecp256k1 = "Bitcoin" - } - let ecdhVariant: ECDHVariant - let ecdhSharedKey: String - let derivedKeys: [DerivedKeys] - struct DerivedKeys: Decodable { - - /// Used by both `x963` and `HKDF` - let info: String - - /// `x963` KDF output - let x963: String - - /// Salt for `HDKF` - let salt: String - - let hkdf: String - } - } -} + let alicePrivateKey: String + let bobPrivateKey: String + let alicePublicKeyUncompressed: String + let bobPublicKeyUncompressed: String + let outcomes: [Outcome] + + struct Outcome: Decodable { + enum ECDHVariant: String, Decodable { + case asn1X963 = "ASN1X963" + case libsecp256k1 = "Bitcoin" + } + let ecdhVariant: ECDHVariant + let ecdhSharedKey: String + let derivedKeys: [DerivedKeys] + struct DerivedKeys: Decodable { + /// Used by both `x963` and `HKDF` + let info: String + /// `x963` KDF output + let x963: String + /// Salt for `HDKF` + let salt: String + + let hkdf: String + } + } +} + +// MARK: - TwoVariantsOfECDHWithKDFTests final class TwoVariantsOfECDHWithKDFTests: XCTestCase { - override func setUp() { - super.setUp() - continueAfterFailure = false - } - func testTwoVariantsOfECDHWithKDF_vectors() throws { - let fileURL = Bundle.module.url(forResource: "cyon_ecdh_two_variants_with_kdf", withExtension: ".json") - let data = try Data(contentsOf: fileURL!) - let suite = try JSONDecoder().decode(ECDHX963Suite.self, from: data) - try suite.vectors.forEach(doTest) - } + override func setUp() { + super.setUp() + continueAfterFailure = false + } + + func testTwoVariantsOfECDHWithKDF_vectors() throws { + let fileURL = Bundle.module.url(forResource: "cyon_ecdh_two_variants_with_kdf", withExtension: ".json") + let data = try Data(contentsOf: fileURL!) + let suite = try JSONDecoder().decode(ECDHX963Suite.self, from: data) + try suite.vectors.forEach(doTest) + } } extension TwoVariantsOfECDHWithKDFTests { - - - fileprivate func doTest(_ vector: Vector) throws { - let outputByteCount = 32 - let hash = SHA256.self - - let alice = try K1.KeyAgreement.PrivateKey(hex: vector.alicePrivateKey) - let bob = try K1.KeyAgreement.PrivateKey(hex: vector.bobPrivateKey) - XCTAssertEqual(alice.publicKey.x963Representation.hex, vector.alicePublicKeyUncompressed) - XCTAssertEqual(bob.publicKey.x963Representation.hex, vector.bobPublicKeyUncompressed) - - for outcome in vector.outcomes { - switch outcome.ecdhVariant { - case .asn1X963: - let sharedSecretAB = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) - let sharedSecretBA = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) - - XCTAssertEqual(sharedSecretAB, sharedSecretBA) - sharedSecretAB.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, outcome.ecdhSharedKey, "Wrong ECDH secret, mismatched expected from vector.") - } - - for derivedKeys in outcome.derivedKeys { - let info = try XCTUnwrap(derivedKeys.info.data(using: .utf8)) - let salt = try Data(hex: derivedKeys.salt) - let x963 = sharedSecretBA.x963DerivedSymmetricKey(using: hash, sharedInfo: info, outputByteCount: outputByteCount) - x963.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, derivedKeys.x963, "Wrong X963 KDF result, mismatched expected from vector.") - } - let hkdf = sharedSecretBA.hkdfDerivedSymmetricKey(using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) - hkdf.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, derivedKeys.hkdf, "Wrong HKDF result, mismatched expected from vector.") - } - } - - case .libsecp256k1: - let sharedSecretAB = try alice.ecdh(with: bob.publicKey) - let sharedSecretBA = try bob.ecdh(with: alice.publicKey) - - XCTAssertEqual(sharedSecretAB, sharedSecretBA) - sharedSecretAB.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, outcome.ecdhSharedKey, "Wrong ECDH secret, mismatched expected from vector.") - } - - for derivedKeys in outcome.derivedKeys { - let info = try XCTUnwrap(derivedKeys.info.data(using: .utf8)) - let salt = try Data(hex: derivedKeys.salt) - let x963 = sharedSecretAB.x963DerivedSymmetricKey(using: hash, sharedInfo: info, outputByteCount: outputByteCount) - x963.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, derivedKeys.x963, "Wrong X963 KDF result, mismatched expected from vector.") - } - let hkdf = sharedSecretAB.hkdfDerivedSymmetricKey(using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) - hkdf.withUnsafeBytes { - XCTAssertEqual(Data($0).hex, derivedKeys.hkdf, "Wrong HKDF result, mismatched expected from vector.") - } - } - } - - } - - } - - - + private func doTest(_ vector: Vector) throws { + let outputByteCount = 32 + let hash = SHA256.self + + let alice = try K1.KeyAgreement.PrivateKey(hex: vector.alicePrivateKey) + let bob = try K1.KeyAgreement.PrivateKey(hex: vector.bobPrivateKey) + XCTAssertEqual(alice.publicKey.x963Representation.hex, vector.alicePublicKeyUncompressed) + XCTAssertEqual(bob.publicKey.x963Representation.hex, vector.bobPublicKeyUncompressed) + + for outcome in vector.outcomes { + switch outcome.ecdhVariant { + case .asn1X963: + let sharedSecretAB = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) + let sharedSecretBA = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) + + XCTAssertEqual(sharedSecretAB, sharedSecretBA) + sharedSecretAB.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, outcome.ecdhSharedKey, "Wrong ECDH secret, mismatched expected from vector.") + } + + for derivedKeys in outcome.derivedKeys { + let info = try XCTUnwrap(derivedKeys.info.data(using: .utf8)) + let salt = try Data(hex: derivedKeys.salt) + let x963 = sharedSecretBA.x963DerivedSymmetricKey(using: hash, sharedInfo: info, outputByteCount: outputByteCount) + x963.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, derivedKeys.x963, "Wrong X963 KDF result, mismatched expected from vector.") + } + let hkdf = sharedSecretBA.hkdfDerivedSymmetricKey(using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) + hkdf.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, derivedKeys.hkdf, "Wrong HKDF result, mismatched expected from vector.") + } + } + + case .libsecp256k1: + let sharedSecretAB = try alice.ecdh(with: bob.publicKey) + let sharedSecretBA = try bob.ecdh(with: alice.publicKey) + + XCTAssertEqual(sharedSecretAB, sharedSecretBA) + sharedSecretAB.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, outcome.ecdhSharedKey, "Wrong ECDH secret, mismatched expected from vector.") + } + + for derivedKeys in outcome.derivedKeys { + let info = try XCTUnwrap(derivedKeys.info.data(using: .utf8)) + let salt = try Data(hex: derivedKeys.salt) + let x963 = sharedSecretAB.x963DerivedSymmetricKey(using: hash, sharedInfo: info, outputByteCount: outputByteCount) + x963.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, derivedKeys.x963, "Wrong X963 KDF result, mismatched expected from vector.") + } + let hkdf = sharedSecretAB.hkdfDerivedSymmetricKey(using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) + hkdf.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, derivedKeys.hkdf, "Wrong HKDF result, mismatched expected from vector.") + } + } + } + } + } } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift index ce4d8bc..5fcb488 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift @@ -1,21 +1,13 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-03. -// - import Foundation import K1 import XCTest final class ECDSARecoverableSignatureRoundtripTests: XCTestCase { - func testECDSARecoverable() throws { - let alice = K1.ECDSA.Recoverable.PrivateKey() - let message = "Send Bob 3 BTC".data(using: .utf8)! - let signature = try alice.signature(forUnhashed: message) - let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) - XCTAssertTrue(isSignatureValid, "Signature should be valid.") - } - + func testECDSARecoverable() throws { + let alice = K1.ECDSA.Recoverable.PrivateKey() + let message = "Send Bob 3 BTC".data(using: .utf8)! + let signature = try alice.signature(forUnhashed: message) + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) + XCTAssertTrue(isSignatureValid, "Signature should be valid.") + } } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift index c2d1ccd..3b4b285 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift @@ -1,47 +1,40 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-03. -// - +import CryptoKit import Foundation import K1 import XCTest -import CryptoKit final class ECDSASignatureTests: XCTestCase { - func testECDSADeterministic() throws { - let alice = K1.ECDSA.NonRecoverable.PrivateKey() - let message = "Send Bob 3 BTC".data(using: .utf8)! - - let signature = try alice.signature(forUnhashed: message) - - let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) - XCTAssertTrue(isSignatureValid, "Signature should be valid.") - } + func testECDSADeterministic() throws { + let alice = K1.ECDSA.NonRecoverable.PrivateKey() + let message = "Send Bob 3 BTC".data(using: .utf8)! + + let signature = try alice.signature(forUnhashed: message) + + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) + XCTAssertTrue(isSignatureValid, "Signature should be valid.") + } + + func testECDSARandom() throws { + continueAfterFailure = false + let alice = K1.ECDSA.NonRecoverable.PrivateKey() + let message = "Send Bob 3 BTC".data(using: .utf8)! + + let requestedNumberOfSignatures = 1000 + var signatures = Set() + + for i in 0 ..< requestedNumberOfSignatures { + let signature = try alice.signature( + forUnhashed: message, + options: .init(nonceFunction: .random) + ) - func testECDSARandom() throws { - continueAfterFailure = false - let alice = K1.ECDSA.NonRecoverable.PrivateKey() - let message = "Send Bob 3 BTC".data(using: .utf8)! - - let requestedNumberOfSignatures = 1000 - var signatures = Set() - - for i in 0.., - file: StaticString = #file, - line: UInt = #line - ) throws -> ResultOfTestGroup { - var numberOfTestsRun = 0 - for vector in group.tests { - let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: Data(hex: vector.privateKey)) - let publicKey: K1.ECDSA.NonRecoverable.PublicKey = privateKey.publicKey - - let expectedSignature = try vector.expectedSignature() - let messageDigest = try vector.messageDigest() - XCTAssertTrue(publicKey.isValidSignature(expectedSignature, digest: messageDigest)) - - let signatureFromMessage = try privateKey.signature(for: messageDigest) - XCTAssertEqual(signatureFromMessage, expectedSignature) + func doTestGroup( + group: ECDSATestGroup, + file: StaticString = #file, + line: UInt = #line + ) throws -> ResultOfTestGroup { + var numberOfTestsRun = 0 + for vector in group.tests { + let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: Data(hex: vector.privateKey)) + let publicKey: K1.ECDSA.NonRecoverable.PublicKey = privateKey.publicKey - let signatureRandom = try privateKey.signature( - for: messageDigest, - options: .init(nonceFunction: .random) - ) + let expectedSignature = try vector.expectedSignature() + let messageDigest = try vector.messageDigest() + XCTAssertTrue(publicKey.isValidSignature(expectedSignature, digest: messageDigest)) - XCTAssertNotEqual(signatureRandom, expectedSignature) - XCTAssertTrue(publicKey.isValidSignature(signatureRandom, digest: messageDigest)) + let signatureFromMessage = try privateKey.signature(for: messageDigest) + XCTAssertEqual(signatureFromMessage, expectedSignature) - let privateKeyRecoverable = try K1.ECDSA.Recoverable.PrivateKey(rawRepresentation: privateKey.rawRepresentation) - let signatureRecoverableFromMessage = try privateKeyRecoverable.signature(for: messageDigest) - try XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) - let recid = try signatureRecoverableFromMessage.compact().recoveryID - XCTAssertEqual(signatureRecoverableFromMessage.rawRepresentation.hex, expectedSignature.rawRepresentation.hex + "\(Data([UInt8(recid.rawValue)]).hex)") - numberOfTestsRun += 1 - } - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) - } -} + let signatureRandom = try privateKey.signature( + for: messageDigest, + options: .init(nonceFunction: .random) + ) + XCTAssertNotEqual(signatureRandom, expectedSignature) + XCTAssertTrue(publicKey.isValidSignature(signatureRandom, digest: messageDigest)) -private struct SignatureTrezorTestVector: SignatureTestVector { - - typealias MessageDigest = SHA256.Digest - typealias Signature = K1.ECDSA.NonRecoverable.Signature - - let msg: String - let privateKey: String - let expected: Expected - struct Expected: Codable { - let k: String - let r: String - let s: String - let der: String - } - let tcId: Int - - func messageDigest() throws -> MessageDigest { - let messageToHash = msg.data(using: .utf8)! - return SHA256.hash(data: messageToHash) - } - func expectedSignature() throws -> Signature { - let derData = try Data(hex: expected.der) - let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) - try XCTAssertEqual(signature.derRepresentation().hex, expected.der) - try XCTAssertEqual( - signature.compactRepresentation().hex, - [ - expected.r, - expected.s - ].joined(separator: "") - ) - - try XCTAssertEqual( - K1.ECDSA.NonRecoverable.Signature(compactRepresentation: Data(hex: expected.r + expected.s)), - signature - ) - - return signature - } - + let privateKeyRecoverable = try K1.ECDSA.Recoverable.PrivateKey(rawRepresentation: privateKey.rawRepresentation) + let signatureRecoverableFromMessage = try privateKeyRecoverable.signature(for: messageDigest) + try XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) + let recid = try signatureRecoverableFromMessage.compact().recoveryID + XCTAssertEqual(signatureRecoverableFromMessage.rawRepresentation.hex, expectedSignature.rawRepresentation.hex + "\(Data([UInt8(recid.rawValue)]).hex)") + numberOfTestsRun += 1 + } + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) + } } +// MARK: - SignatureTrezorTestVector +private struct SignatureTrezorTestVector: SignatureTestVector { + typealias MessageDigest = SHA256.Digest + typealias Signature = K1.ECDSA.NonRecoverable.Signature + + let msg: String + let privateKey: String + let expected: Expected + struct Expected: Codable { + let k: String + let r: String + let s: String + let der: String + } + + let tcId: Int + + func messageDigest() throws -> MessageDigest { + let messageToHash = msg.data(using: .utf8)! + return SHA256.hash(data: messageToHash) + } + + func expectedSignature() throws -> Signature { + let derData = try Data(hex: expected.der) + let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) + try XCTAssertEqual(signature.derRepresentation().hex, expected.der) + try XCTAssertEqual( + signature.compactRepresentation().hex, + [ + expected.r, + expected.s, + ].joined(separator: "") + ) + + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(compactRepresentation: Data(hex: expected.r + expected.s)), + signature + ) + + return signature + } +} diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift index 0a3ddcb..3681653 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift @@ -1,5 +1,4 @@ -// FROM: https://github.com/apple/swift-crypto/blob/main/Tests/CryptoTests/Signatures/ECDSA/ECDSASignatureTests.swift -// commit: 53da7b3706ae6a2bd621becbb201f3d8e24039d6 +// swiftformat:disable strip //===----------------------------------------------------------------------===// // @@ -14,54 +13,59 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation -import XCTest + +// FROM: https://github.com/apple/swift-crypto/blob/main/Tests/CryptoTests/Signatures/ECDSA/ECDSASignatureTests.swift +// commit: 53da7b3706ae6a2bd621becbb201f3d8e24039d6 + +// swiftformat:enable strip + import CryptoKit +import Foundation @testable import K1 +import XCTest +// MARK: - ECDSA_Wycheproof_ASN_DER_EncodedSignaturesTests final class ECDSA_Wycheproof_ASN_DER_EncodedSignaturesTests: XCTestCase { - - func testWycheProofSecp256k1_DER() throws { - let result: TestResult = try testSuite( - /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ - jsonName: "wycheproof_ecdsa_verify_der", - testFunction: { (group: ECDSAWycheTestGroup) in - - try doTestGroup( - group: group, - signatureValidationMode: .init(malleabilityStrictness: .accepted), - hashFunction: SHA256.self, - skipIfContainsFlags: .init(["MissingZero", "BER"]) - ) - - }) - print("☑️ Test result: \(String(describing: result))") - } + func testWycheProofSecp256k1_DER() throws { + let _: TestResult = try testSuite( + /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ + jsonName: "wycheproof_ecdsa_verify_der", + testFunction: { (group: ECDSAWycheTestGroup) in + + try doTestGroup( + group: group, + signatureValidationMode: .init(malleabilityStrictness: .accepted), + hashFunction: SHA256.self, + skipIfContainsFlags: .init(["MissingZero", "BER"]) + ) + } + ) + } } +// MARK: - SignatureWycheproofDERTestVector private struct SignatureWycheproofDERTestVector: WycheproofTestVector { - - typealias MessageDigest = SHA256.Digest - typealias Signature = K1.ECDSA.NonRecoverable.Signature - - let comment: String - let msg: String - let sig: String - let result: String - let flags: [String] - let tcId: Int - - func messageDigest() throws -> MessageDigest { - let msg = try Data(hex: msg) - return SHA256.hash(data: msg) - } - func expectedSignature() throws -> Signature { - let derData = try Data(hex: sig) - let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) - if self.result == "valid" { - try XCTAssertEqual(sig, signature.derRepresentation().hex) - } - return signature - } - + typealias MessageDigest = SHA256.Digest + typealias Signature = K1.ECDSA.NonRecoverable.Signature + + let comment: String + let msg: String + let sig: String + let result: String + let flags: [String] + let tcId: Int + + func messageDigest() throws -> MessageDigest { + let msg = try Data(hex: msg) + return SHA256.hash(data: msg) + } + + func expectedSignature() throws -> Signature { + let derData = try Data(hex: sig) + let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) + if self.result == "valid" { + try XCTAssertEqual(sig, signature.derRepresentation().hex) + } + return signature + } } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift index 04c2a3c..24eb38a 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift @@ -1,56 +1,51 @@ -import Foundation -import XCTest import CryptoKit +import Foundation @testable import K1 +import XCTest +// MARK: - ECDSA_Wycheproof_IEEE_P1364_RS_EncodedSignaturesTests final class ECDSA_Wycheproof_IEEE_P1364_RS_EncodedSignaturesTests: XCTestCase { - - func testWycheProofSecp256k1_P1364_RS() throws { - let result: TestResult = - try testSuite( - /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ - jsonName: "wycheproof_ecdsa_verify_p1363", - testFunction: { (group: ECDSAWycheTestGroup) in - try doTestGroup( - group: group, - signatureValidationMode: .init(malleabilityStrictness: .accepted), - hashFunction: SHA256.self, - skipIfContainsFlags: .init(["MissingZero", "BER", "SigSize"]), - skipIfContainsComment: ["r too large"] - ) - }) - - print("☑️ Test result: \(String(describing: result))") - } + func testWycheProofSecp256k1_P1364_RS() throws { + let _: TestResult = + try testSuite( + /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ + jsonName: "wycheproof_ecdsa_verify_p1363", + testFunction: { (group: ECDSAWycheTestGroup) in + try doTestGroup( + group: group, + signatureValidationMode: .init(malleabilityStrictness: .accepted), + hashFunction: SHA256.self, + skipIfContainsFlags: .init(["MissingZero", "BER", "SigSize"]), + skipIfContainsComment: ["r too large"] + ) + } + ) + } } - - +// MARK: - SignatureWycheproofP1364TestVector private struct SignatureWycheproofP1364TestVector: WycheproofTestVector { - - typealias MessageDigest = SHA256.Digest - typealias Signature = K1.ECDSA.NonRecoverable.Signature - - let comment: String - let msg: String - let sig: String - let result: String - let flags: [String] - let tcId: Int - - func messageDigest() throws -> MessageDigest { - let msg = try Data(hex: msg) - return SHA256.hash(data: msg) - } - func expectedSignature() throws -> Signature { - let raw = try Data(hex: sig) - let signature = try Signature(compactRepresentation: raw) - if self.result == "valid" { - try XCTAssertEqual(sig, signature.compactRepresentation().hex) - } - return signature - } - -} + typealias MessageDigest = SHA256.Digest + typealias Signature = K1.ECDSA.NonRecoverable.Signature + let comment: String + let msg: String + let sig: String + let result: String + let flags: [String] + let tcId: Int + func messageDigest() throws -> MessageDigest { + let msg = try Data(hex: msg) + return SHA256.hash(data: msg) + } + + func expectedSignature() throws -> Signature { + let raw = try Data(hex: sig) + let signature = try Signature(compactRepresentation: raw) + if self.result == "valid" { + try XCTAssertEqual(sig, signature.compactRepresentation().hex) + } + return signature + } +} diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift index c416a2e..bcbc5c7 100644 --- a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift @@ -1,54 +1,47 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-21. -// import Foundation @testable import K1 import XCTest - +// MARK: - PrivateKeyEncodingTests final class PrivateKeyEncodingTests: XCTestCase { - - func testRawRoundtrip() throws { - try doTest( - serialize: \.rawRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(rawRepresentation:) - ) - } - - func testx963Roundtrip() throws { - try doTest( - serialize: \.x963Representation, - deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(x963Representation:) - ) - } - - func testDERRoundtrip() throws { - try doTest( - serialize: \.derRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(derRepresentation:) - ) - } - - func testPEMRoundtrip() throws { - try doTest( - serialize: \.pemRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(pemRepresentation:) - ) - } + func testRawRoundtrip() throws { + try doTest( + serialize: \.rawRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(rawRepresentation:) + ) + } + + func testx963Roundtrip() throws { + try doTest( + serialize: \.x963Representation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(x963Representation:) + ) + } + + func testDERRoundtrip() throws { + try doTest( + serialize: \.derRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(derRepresentation:) + ) + } + + func testPEMRoundtrip() throws { + try doTest( + serialize: \.pemRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(pemRepresentation:) + ) + } } private extension PrivateKeyEncodingTests { - func doTest( - serialize: KeyPath, - deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PrivateKey - ) throws { - try doTestSerializationRoundtrip( - original: K1.ECDSA.NonRecoverable.PrivateKey(), - serialize: serialize, - deserialize: deserialize - ) - } + func doTest( + serialize: KeyPath, + deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PrivateKey + ) throws { + try doTestSerializationRoundtrip( + original: K1.ECDSA.NonRecoverable.PrivateKey(), + serialize: serialize, + deserialize: deserialize + ) + } } diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift index 9f93c98..76a114c 100644 --- a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift @@ -1,23 +1,14 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-04. -// - import Foundation import K1 import XCTest final class PrivateKeyGenerationTests: XCTestCase { + func testGenerationWorks() throws { + XCTAssertNoThrow(K1.ECDSA.NonRecoverable.PrivateKey()) + } - func testGenerationWorks() throws { - XCTAssertNoThrow(K1.ECDSA.NonRecoverable.PrivateKey()) - } - - func testRandom() throws { - // The probability of two keys being identical is approximately: 1/2^256 - XCTAssertNotEqual(K1.ECDSA.NonRecoverable.PrivateKey(), K1.ECDSA.NonRecoverable.PrivateKey()) - } - + func testRandom() throws { + // The probability of two keys being identical is approximately: 1/2^256 + XCTAssertNotEqual(K1.ECDSA.NonRecoverable.PrivateKey(), K1.ECDSA.NonRecoverable.PrivateKey()) + } } diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift index f9d08f3..1063083 100644 --- a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift @@ -1,63 +1,54 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-04. -// - import Foundation @testable import K1 import XCTest final class PrivateKeyImportTests: XCTestCase { + func testAssertImportingPrivateKeyWithTooFewBytesThrowsError() throws { + let raw = try Data(hex: "deadbeef") + try assert( + K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 4, expected: 32) + ) + } + + func testAssertImportingPrivateKeyWithTooManyBytesThrowsError() throws { + let raw = Data(repeating: 0xBA, count: 33) + try assert( + K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 33, expected: 32) + ) + } + + func testAssertImportingPrivateKeyZeroThrowsError() throws { + let raw = Data(repeating: 0x00, count: 32) + try assert( + K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.invalidPrivateKeyMustNotBeZero + ) + } + + func testAssertImportingPrivateKeyCurveOrderThrowsError() throws { + let raw = try Data(hex: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") + try assert( + K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder + ) + } + + func testAssertImportingPrivateKeyLargerThanCurveOrderThrowsError() throws { + let raw = Data(repeating: 0xFF, count: 32) + try assert( + K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder + ) + } - func testAssertImportingPrivateKeyWithTooFewBytesThrowsError() throws { - let raw = try Data(hex: "deadbeef") - assert( - try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), - throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 4, expected: 32) - ) - } - - func testAssertImportingPrivateKeyWithTooManyBytesThrowsError() throws { - let raw = Data(repeating: 0xba, count: 33) - assert( - try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), - throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 33, expected: 32) - ) - } - - func testAssertImportingPrivateKeyZeroThrowsError() throws { - let raw = Data(repeating: 0x00, count: 32) - assert( - try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), - throws: K1.Error.invalidPrivateKeyMustNotBeZero - ) - } - - func testAssertImportingPrivateKeyCurveOrderThrowsError() throws { - let raw = try Data(hex: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") - assert( - try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), - throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder - ) - } - - func testAssertImportingPrivateKeyLargerThanCurveOrderThrowsError() throws { - let raw = Data(repeating: 0xff, count: 32) - assert( - try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), - throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder - ) - } - - func testAssertPublicKeyOfImportedPrivateKey1() throws { - let privateKeyRaw = try Data(hex: "0000000000000000000000000000000000000000000000000000000000000001") - let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: privateKeyRaw) - // Easily verified by: https://bitaddress.org/ - // Pretty well known key pair - let expectedPublicKey = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: Data(hex: "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")) - XCTAssertEqual(privateKey.publicKey, expectedPublicKey) - } - + func testAssertPublicKeyOfImportedPrivateKey1() throws { + let privateKeyRaw = try Data(hex: "0000000000000000000000000000000000000000000000000000000000000001") + let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: privateKeyRaw) + // Easily verified by: https://bitaddress.org/ + // Pretty well known key pair + let expectedPublicKey = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: Data(hex: "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")) + XCTAssertEqual(privateKey.publicKey, expectedPublicKey) + } } diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift index 3337af5..82710df 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift @@ -1,91 +1,82 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-21. -// - import Foundation @testable import K1 import XCTest - +// MARK: - PublicKeyEncodingTests final class PublicKeyEncodingTests: XCTestCase { - - func test_pubkey_raw_is_x963_minus_prefix() throws { - let privateKey = K1.ECDSA.NonRecoverable.PrivateKey() - let publicKey = privateKey.publicKey - - XCTAssertEqual(publicKey.rawRepresentation.hex, Data(publicKey.x963Representation.dropFirst()).hex) - } - - func testRawRoundtrip() throws { - try doTest( - serialize: \.rawRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(rawRepresentation:) - ) - } - - func testCompressedRoundtrip() throws { - try doTest( - serialize: \.compressedRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(compressedRepresentation:) - ) - } - - func testx963Roundtrip() throws { - try doTest( - serialize: \.x963Representation, - deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(x963Representation:) - ) - } - - func testDERRoundtrip() throws { - try doTest( - serialize: \.derRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(derRepresentation:) - ) - } - - func testPEMRoundtrip() throws { - try doTest( - serialize: \.pemRepresentation, - deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(pemRepresentation:) - ) - } + func test_pubkey_raw_is_x963_minus_prefix() throws { + let privateKey = K1.ECDSA.NonRecoverable.PrivateKey() + let publicKey = privateKey.publicKey + + XCTAssertEqual(publicKey.rawRepresentation.hex, Data(publicKey.x963Representation.dropFirst()).hex) + } + + func testRawRoundtrip() throws { + try doTest( + serialize: \.rawRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(rawRepresentation:) + ) + } + + func testCompressedRoundtrip() throws { + try doTest( + serialize: \.compressedRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(compressedRepresentation:) + ) + } + + func testx963Roundtrip() throws { + try doTest( + serialize: \.x963Representation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(x963Representation:) + ) + } + + func testDERRoundtrip() throws { + try doTest( + serialize: \.derRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(derRepresentation:) + ) + } + + func testPEMRoundtrip() throws { + try doTest( + serialize: \.pemRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(pemRepresentation:) + ) + } } private extension PublicKeyEncodingTests { - func doTest( - serialize: KeyPath, - deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PublicKey - ) throws { - try doTestSerializationRoundtrip( - original: K1.ECDSA.NonRecoverable.PublicKey.generateNew(), - serialize: serialize, - deserialize: deserialize - ) - } + func doTest( + serialize: KeyPath, + deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PublicKey + ) throws { + try doTestSerializationRoundtrip( + original: K1.ECDSA.NonRecoverable.PublicKey.generateNew(), + serialize: serialize, + deserialize: deserialize + ) + } } extension K1.ECDSA.NonRecoverable.PublicKey { - static func generateNew() -> Self { - K1.ECDSA.NonRecoverable.PrivateKey().publicKey - } + static func generateNew() -> Self { + K1.ECDSA.NonRecoverable.PrivateKey().publicKey + } } public func doTestSerializationRoundtrip( - original makeOriginal: @autoclosure () -> T, - serialize: KeyPath, - deserialize: (Enc) throws -> T + original makeOriginal: @autoclosure () -> T, + serialize: KeyPath, + deserialize: (Enc) throws -> T ) throws where T: Equatable, Enc: Equatable { - for _ in 0 ..< 100 { - let original = makeOriginal() - let serialized = original[keyPath: serialize] - let deserialized = try deserialize(serialized) - XCTAssertEqual(deserialized, original) - let reserialized = deserialized[keyPath: serialize] - XCTAssertEqual(reserialized, serialized) - - } + for _ in 0 ..< 100 { + let original = makeOriginal() + let serialized = original[keyPath: serialize] + let deserialized = try deserialize(serialized) + XCTAssertEqual(deserialized, original) + let reserialized = deserialized[keyPath: serialize] + XCTAssertEqual(reserialized, serialized) + } } diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift index 929d6cf..7363284 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift @@ -1,71 +1,64 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-04. -// - import Foundation @testable import K1 import XCTest final class PublicKeyImportTests: XCTestCase { - func testAssertImportingPublicKeyWithTooFewBytesThrowsError() throws { - let raw = try Data(hex: "deadbeef") - assert( - try K1.ECDSA.NonRecoverable.PublicKey.init(x963Representation: raw), - throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 4, expected: 65) - ) - } - - func testAssertImportingPublicKeyWithTooManyBytesThrowsError() throws { - let raw = Data(repeating: 0xde, count: 66) - assert( - try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), - throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 66, expected: 65) - ) - } - - func testAssertImportingInvalidUncompressedPublicKeyThrowsError() throws { - let raw = Data(repeating: 0x04, count: 65) - assert( - try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), - throws: K1.Error.failedToDeserializePublicKey - ) - } - - func testAssertImportingInvalidCompressedPublicKeyThrowsError() throws { - let raw = Data(repeating: 0x03, count: 33) - assert( - try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw), - throws: K1.Error.failedToDeserializePublicKey - ) - } - - func testAssertImportValidPublicKeyWorks() throws { - let raw = Data(repeating: 0x02, count: 33) - let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) - XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") - XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") - } - - func test_compress_pubkey() throws { - let raw = Data(repeating: 0x02, count: 33) - let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) - XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") - XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") - } - - func testNotOnCurve() throws { - /// Public key from `ecdh_secp256k1_test.json` in Wycheproof - /// Vector id: 185 - /// With "comment" : "point is not on curve" - /// DER => raw - let raw = try Data(hex: "040000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") - - assert( - try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), - throws: K1.Error.failedToDeserializePublicKey - ) - } + func testAssertImportingPublicKeyWithTooFewBytesThrowsError() throws { + let raw = try Data(hex: "deadbeef") + try assert( + K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 4, expected: 65) + ) + } + + func testAssertImportingPublicKeyWithTooManyBytesThrowsError() throws { + let raw = Data(repeating: 0xDE, count: 66) + try assert( + K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 66, expected: 65) + ) + } + + func testAssertImportingInvalidUncompressedPublicKeyThrowsError() throws { + let raw = Data(repeating: 0x04, count: 65) + try assert( + K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.failedToDeserializePublicKey + ) + } + + func testAssertImportingInvalidCompressedPublicKeyThrowsError() throws { + let raw = Data(repeating: 0x03, count: 33) + try assert( + K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw), + throws: K1.Error.failedToDeserializePublicKey + ) + } + + func testAssertImportValidPublicKeyWorks() throws { + let raw = Data(repeating: 0x02, count: 33) + let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) + XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") + XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") + } + + func test_compress_pubkey() throws { + let raw = Data(repeating: 0x02, count: 33) + let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) + XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") + XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") + } + + func testNotOnCurve() throws { + /// Public key from `ecdh_secp256k1_test.json` in Wycheproof + /// Vector id: 185 + /// With "comment" : "point is not on curve" + /// DER => raw + let raw = try Data(hex: "040000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") + + try assert( + K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.failedToDeserializePublicKey + ) + } } diff --git a/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift b/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift index dbe5abf..48969d1 100644 --- a/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift +++ b/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift @@ -3,86 +3,82 @@ import K1 import XCTest final class PerformanceTests: XCTestCase { - - func testPerformance() { - let message = [UInt8](repeating: 0xab, count: 32) - measure { - do { - for _ in 0 ..< 10 { - let schnorrPrivateKey = K1.Schnorr.PrivateKey() - let schnorrPublicKey = schnorrPrivateKey.publicKey - - try XCTAssertEqual( - K1.Schnorr.PublicKey(compressedRepresentation: schnorrPublicKey.compressedRepresentation), - schnorrPublicKey - ) - try XCTAssertEqual( - K1.Schnorr.PublicKey(x963Representation: schnorrPublicKey.x963Representation), - schnorrPublicKey - ) - - let ecdsaPrivateKey = K1.ECDSA.Recoverable.PrivateKey() - let ecdsaPublicKey = ecdsaPrivateKey.publicKey - - try XCTAssertEqual( - K1.ECDSA.Recoverable.PublicKey(compressedRepresentation: ecdsaPublicKey.compressedRepresentation), - ecdsaPublicKey - ) - try XCTAssertEqual( - K1.ECDSA.Recoverable.PublicKey(x963Representation: ecdsaPublicKey.x963Representation), - ecdsaPublicKey - ) - - let ecdsa = try ecdsaPrivateKey.signature(for: message) - XCTAssertTrue( - ecdsaPublicKey.isValidSignature( - ecdsa, - hashed: message - ) - ) - try XCTAssertEqual( - K1.ECDSA.Recoverable.Signature(compact: ecdsa.compact()), - ecdsa - ) - try XCTAssertEqual( - K1.ECDSA.NonRecoverable.Signature(compactRepresentation: ecdsa.nonRecoverable().compactRepresentation()), - ecdsa.nonRecoverable() - ) - try XCTAssertEqual( - K1.ECDSA.NonRecoverable.Signature(derRepresentation: ecdsa.nonRecoverable().derRepresentation()), - ecdsa.nonRecoverable() - ) - - let schnorr = try schnorrPrivateKey.signature(for: message) - XCTAssertTrue( - schnorrPublicKey.isValidSignature( - schnorr, - hashed: message - ) - ) - try XCTAssertEqual( - K1.Schnorr.Signature(rawRepresentation: schnorr.rawRepresentation), - schnorr - ) - - let alicePrivateKey = try K1.KeyAgreement.PrivateKey(x963Representation: ecdsaPrivateKey.x963Representation) - let alicePublicKey = alicePrivateKey.publicKey - let bobPrivateKey = K1.KeyAgreement.PrivateKey() - let bobPublicKey = bobPrivateKey.publicKey + func testPerformance() { + let message = [UInt8](repeating: 0xAB, count: 32) + measure { + do { + for _ in 0 ..< 10 { + let schnorrPrivateKey = K1.Schnorr.PrivateKey() + let schnorrPublicKey = schnorrPrivateKey.publicKey - var ab = try alicePrivateKey.sharedSecretFromKeyAgreement(with: bobPublicKey) - var ba = try bobPrivateKey.sharedSecretFromKeyAgreement(with: alicePublicKey) - XCTAssertEqual(ab, ba) - ab = try alicePrivateKey.ecdh(with: bobPublicKey) - ba = try bobPrivateKey.ecdh(with: alicePublicKey) - XCTAssertEqual(ab, ba) - } - } catch { - XCTFail("abort") - } - } - - } + try XCTAssertEqual( + K1.Schnorr.PublicKey(compressedRepresentation: schnorrPublicKey.compressedRepresentation), + schnorrPublicKey + ) + try XCTAssertEqual( + K1.Schnorr.PublicKey(x963Representation: schnorrPublicKey.x963Representation), + schnorrPublicKey + ) -} + let ecdsaPrivateKey = K1.ECDSA.Recoverable.PrivateKey() + let ecdsaPublicKey = ecdsaPrivateKey.publicKey + + try XCTAssertEqual( + K1.ECDSA.Recoverable.PublicKey(compressedRepresentation: ecdsaPublicKey.compressedRepresentation), + ecdsaPublicKey + ) + try XCTAssertEqual( + K1.ECDSA.Recoverable.PublicKey(x963Representation: ecdsaPublicKey.x963Representation), + ecdsaPublicKey + ) + + let ecdsa = try ecdsaPrivateKey.signature(for: message) + XCTAssertTrue( + ecdsaPublicKey.isValidSignature( + ecdsa, + hashed: message + ) + ) + try XCTAssertEqual( + K1.ECDSA.Recoverable.Signature(compact: ecdsa.compact()), + ecdsa + ) + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(compactRepresentation: ecdsa.nonRecoverable().compactRepresentation()), + ecdsa.nonRecoverable() + ) + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(derRepresentation: ecdsa.nonRecoverable().derRepresentation()), + ecdsa.nonRecoverable() + ) + let schnorr = try schnorrPrivateKey.signature(for: message) + XCTAssertTrue( + schnorrPublicKey.isValidSignature( + schnorr, + hashed: message + ) + ) + try XCTAssertEqual( + K1.Schnorr.Signature(rawRepresentation: schnorr.rawRepresentation), + schnorr + ) + + let alicePrivateKey = try K1.KeyAgreement.PrivateKey(x963Representation: ecdsaPrivateKey.x963Representation) + let alicePublicKey = alicePrivateKey.publicKey + let bobPrivateKey = K1.KeyAgreement.PrivateKey() + let bobPublicKey = bobPrivateKey.publicKey + + var ab = try alicePrivateKey.sharedSecretFromKeyAgreement(with: bobPublicKey) + var ba = try bobPrivateKey.sharedSecretFromKeyAgreement(with: alicePublicKey) + XCTAssertEqual(ab, ba) + ab = try alicePrivateKey.ecdh(with: bobPublicKey) + ba = try bobPrivateKey.ecdh(with: alicePublicKey) + XCTAssertEqual(ab, ba) + } + } catch { + XCTFail("abort") + } + } + } +} diff --git a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift index 35a7c23..c67b421 100644 --- a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift +++ b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift @@ -1,145 +1,134 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-10-25. -// - import Foundation @testable import K1 import XCTest - +// MARK: - ECDASignaturePublicKeyRecoveryTests /// Test vectors: /// https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { - - override func setUp() { - super.setUp() - continueAfterFailure = false - } - - func test_recovery_test_vectors() throws { - let result: TestResult = try testSuite( - jsonName: "warta_cyon_publickey_recovery", - testFunction: { (group: RecoveryTestGroup) in - try doTestGroup(group: group) - } - ) - - print("☑️ Test result: \(String(describing: result))") - } - - func test_conversionRoundtrips() throws { - let recoverySignatureHex = "acf9e195e094f2f40eb619b9878817ff951b9b11fac37cf0d7290098bbefb574f8606281a2231a3fc781045f2ea4df086936263bbfa8d15ca17fe70e0c3d6e5601" - let recoverableSigRaw = try Data(hex: recoverySignatureHex) - let recoverableSig = try K1.ECDSA.Recoverable.Signature(rawRepresentation: recoverableSigRaw) - XCTAssertEqual(recoverableSig.rawRepresentation.hex, recoverySignatureHex) - - let compactRSV = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f801" - try XCTAssertEqual(recoverableSig.compact().serialize(format: .rsv).hex, compactRSV) - let compactVRS = "0174b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" - try XCTAssertEqual(recoverableSig.compact().serialize(format: .vrs).hex, compactVRS) - - try XCTAssertEqual( - recoverableSig.rawRepresentation.hex, - K1.ECDSA.Recoverable.Signature(compact: .init(rawRepresentation: Data(hex: compactVRS), format: .vrs)).rawRepresentation.hex - ) - - let compactRecoverableSig = try recoverableSig.compact() - - let compactRecoverableSigRSHex = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" - let recid = try K1.ECDSA.Recoverable.Signature.RecoveryID(recid: 1) - XCTAssertEqual(compactRecoverableSig.compact.hex, compactRecoverableSigRSHex) - XCTAssertEqual(compactRecoverableSig.recoveryID, recid) - - let compactRecoverableSigRS = try Data(hex: compactRecoverableSigRSHex) - try XCTAssertEqual(K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSigRS, recoveryID: recid), K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSig)) - try XCTAssertEqual(K1.ECDSA.Recoverable.Signature.Compact.init(compact: compactRecoverableSigRS, recoveryID: recid), compactRecoverableSig) - - let nonRecoverable = try K1.ECDSA.NonRecoverable.Signature(compactRepresentation: compactRecoverableSig.compact) - - try XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) - let nonRecovDer = try nonRecoverable.derRepresentation() - let nonRecoveryDERHex = "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" - XCTAssertEqual(nonRecovDer.hex, nonRecoveryDERHex) - - try XCTAssertEqual(K1.ECDSA.NonRecoverable.Signature(derRepresentation: Data(hex: nonRecoveryDERHex)), nonRecoverable) - - - - } + override func setUp() { + super.setUp() + continueAfterFailure = false + } + + func test_recovery_test_vectors() throws { + let _: TestResult = try testSuite( + jsonName: "warta_cyon_publickey_recovery", + testFunction: { (group: RecoveryTestGroup) in + try doTestGroup(group: group) + } + ) + } + + func test_conversionRoundtrips() throws { + let recoverySignatureHex = "acf9e195e094f2f40eb619b9878817ff951b9b11fac37cf0d7290098bbefb574f8606281a2231a3fc781045f2ea4df086936263bbfa8d15ca17fe70e0c3d6e5601" + let recoverableSigRaw = try Data(hex: recoverySignatureHex) + let recoverableSig = try K1.ECDSA.Recoverable.Signature(rawRepresentation: recoverableSigRaw) + XCTAssertEqual(recoverableSig.rawRepresentation.hex, recoverySignatureHex) + + let compactRSV = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f801" + try XCTAssertEqual(recoverableSig.compact().serialize(format: .rsv).hex, compactRSV) + let compactVRS = "0174b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + try XCTAssertEqual(recoverableSig.compact().serialize(format: .vrs).hex, compactVRS) + + try XCTAssertEqual( + recoverableSig.rawRepresentation.hex, + K1.ECDSA.Recoverable.Signature(compact: .init(rawRepresentation: Data(hex: compactVRS), format: .vrs)).rawRepresentation.hex + ) + + let compactRecoverableSig = try recoverableSig.compact() + + let compactRecoverableSigRSHex = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + let recid = try K1.ECDSA.Recoverable.Signature.RecoveryID(recid: 1) + XCTAssertEqual(compactRecoverableSig.compact.hex, compactRecoverableSigRSHex) + XCTAssertEqual(compactRecoverableSig.recoveryID, recid) + + let compactRecoverableSigRS = try Data(hex: compactRecoverableSigRSHex) + try XCTAssertEqual(K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSigRS, recoveryID: recid), K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSig)) + try XCTAssertEqual(K1.ECDSA.Recoverable.Signature.Compact(compact: compactRecoverableSigRS, recoveryID: recid), compactRecoverableSig) + + let nonRecoverable = try K1.ECDSA.NonRecoverable.Signature(compactRepresentation: compactRecoverableSig.compact) + + try XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) + let nonRecovDer = try nonRecoverable.derRepresentation() + let nonRecoveryDERHex = "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + XCTAssertEqual(nonRecovDer.hex, nonRecoveryDERHex) + + try XCTAssertEqual(K1.ECDSA.NonRecoverable.Signature(derRepresentation: Data(hex: nonRecoveryDERHex)), nonRecoverable) + } } private extension ECDASignaturePublicKeyRecoveryTests { - func doTestGroup( - group: RecoveryTestGroup, - file: StaticString = #file, - line: UInt = #line - ) throws -> ResultOfTestGroup { - var numberOfTestsRun = 0 - for vector in group.tests { - let publicKeyUncompressed = try [UInt8](hex: vector.publicKeyUncompressed) - let expectedPublicKey = try K1.ECDSA.Recoverable.PublicKey.init(x963Representation: publicKeyUncompressed) - - XCTAssertEqual( - try Data(hex: vector.publicKeyCompressed), - expectedPublicKey.compressedRepresentation - ) - - - let recoverableSig = try vector.recoverableSignature() - try XCTAssertEqual(recoverableSig.compact().recoveryID, vector.recoveryID) - - let hashedMessage = try Data(hex: vector.hashMessage) - XCTAssertTrue(expectedPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) - try XCTAssertEqual(vector.recoveryID, recoverableSig.compact().recoveryID) - - let recoveredPublicKey = try recoverableSig.recoverPublicKey( - message: hashedMessage - ) - - XCTAssertEqual(expectedPublicKey, recoveredPublicKey) - - XCTAssertTrue(recoveredPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) - - let recoveredWithID = try recoverableSig.nonRecoverable().recoverPublicKey( - recoveryID: vector.recoveryID, - message: hashedMessage - ) - try XCTAssertEqual(expectedPublicKey, .init(x963Representation: recoveredWithID.x963Representation)) - - numberOfTestsRun += 1 - } - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) - } + func doTestGroup( + group: RecoveryTestGroup, + file: StaticString = #file, + line: UInt = #line + ) throws -> ResultOfTestGroup { + var numberOfTestsRun = 0 + for vector in group.tests { + let publicKeyUncompressed = try [UInt8](hex: vector.publicKeyUncompressed) + let expectedPublicKey = try K1.ECDSA.Recoverable.PublicKey(x963Representation: publicKeyUncompressed) + + XCTAssertEqual( + try Data(hex: vector.publicKeyCompressed), + expectedPublicKey.compressedRepresentation + ) + + let recoverableSig = try vector.recoverableSignature() + try XCTAssertEqual(recoverableSig.compact().recoveryID, vector.recoveryID) + + let hashedMessage = try Data(hex: vector.hashMessage) + XCTAssertTrue(expectedPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) + try XCTAssertEqual(vector.recoveryID, recoverableSig.compact().recoveryID) + + let recoveredPublicKey = try recoverableSig.recoverPublicKey( + message: hashedMessage + ) + + XCTAssertEqual(expectedPublicKey, recoveredPublicKey) + + XCTAssertTrue(recoveredPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) + + let recoveredWithID = try recoverableSig.nonRecoverable().recoverPublicKey( + recoveryID: vector.recoveryID, + message: hashedMessage + ) + try XCTAssertEqual(expectedPublicKey, .init(x963Representation: recoveredWithID.x963Representation)) + + numberOfTestsRun += 1 + } + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) + } } +// MARK: - RecoveryTestGroup private struct RecoveryTestGroup: Decodable { - let tests: [RecoveryTestVector] + let tests: [RecoveryTestVector] } +// MARK: - IncorrectByteCount struct IncorrectByteCount: Swift.Error {} + +// MARK: - RecoveryTestVector struct RecoveryTestVector: Decodable, Equatable { - let recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID - let message: String - let hashMessage: String - private let signature: String - - - func recoverableSignature() throws -> K1.ECDSA.Recoverable.Signature { - try K1.ECDSA.Recoverable.Signature( - rawRepresentation: Data(hex: signature) - ) - } - - let publicKeyUncompressed: String - let publicKeyCompressed: String + let recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID + let message: String + let hashMessage: String + private let signature: String + + func recoverableSignature() throws -> K1.ECDSA.Recoverable.Signature { + try K1.ECDSA.Recoverable.Signature( + rawRepresentation: Data(hex: signature) + ) + } + + let publicKeyUncompressed: String + let publicKeyCompressed: String } +// MARK: - K1.ECDSA.Recoverable.Signature.RecoveryID + ExpressibleByIntegerLiteral extension K1.ECDSA.Recoverable.Signature.RecoveryID: ExpressibleByIntegerLiteral { - public init(integerLiteral value: UInt8) { - self.init(rawValue: value)! - } + public init(integerLiteral value: UInt8) { + self.init(rawValue: value)! + } } - diff --git a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift index 67b1bff..2ffb926 100644 --- a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift +++ b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift @@ -1,156 +1,152 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-01. -// - -import XCTest import Foundation @testable import K1 +import XCTest +// MARK: - SchnorrTestGroup struct SchnorrTestGroup: Codable { - let tests: [V] + let tests: [V] } +// MARK: - SchnorrTestVector protocol SchnorrTestVector: Codable { - var tcId: Int { get } - var isValid: Bool { get } - - var messageHex: String { get } - var publicKeyHex: String { get } - var signatureCompact: String { get } - - var comment: String? { get } - var flags: [String]? { get } + var tcId: Int { get } + var isValid: Bool { get } + + var messageHex: String { get } + var publicKeyHex: String { get } + var signatureCompact: String { get } + + var comment: String? { get } + var flags: [String]? { get } } + extension SchnorrTestVector { - var comment: String? { nil } - var flags: [String]? { nil } + var comment: String? { nil } + var flags: [String]? { nil } } +// MARK: - SchnorrTestVerifyVector struct SchnorrTestVerifyVector: SchnorrTestVector { - let isValid: Bool - let messageHex: String - let tcId: Int - let publicKeyHex: String - let signatureCompact: String - let comment: String? - let flags: [String]? + let isValid: Bool + let messageHex: String + let tcId: Int + let publicKeyHex: String + let signatureCompact: String + let comment: String? + let flags: [String]? } +// MARK: - SchnorrTestSignVector struct SchnorrTestSignVector: SchnorrTestVector { - let isValid: Bool - let messageHex: String - let auxDataHex: String - let tcId: Int - let publicKeyHex: String - let privateKeyHex: String - let signatureCompact: String - let comment: String? + let isValid: Bool + let messageHex: String + let auxDataHex: String + let tcId: Int + let publicKeyHex: String + let privateKeyHex: String + let signatureCompact: String + let comment: String? } +// MARK: - SchnorrSignatureBitcoinCoreTests final class SchnorrSignatureBitcoinCoreTests: XCTestCase { - - override func setUp() { - super.setUp() - continueAfterFailure = false - } - - func testSchnorrSignBitcoinVectors() throws { - let result: TestResult = try testSuite( - /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ - jsonName: "bip340_schnorr_sign", - testFunction: { (group: SchnorrTestGroup) in - var numberOfTestsRun = 0 - try group.tests.forEach(doTestSchnorrSign) - numberOfTestsRun += 1 - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) - }) - - print("☑️ Test result: \(String(describing: result))") - } - - func testSchnorrVerifyBitcoinVectors() throws { - let result: TestResult = - try testSuite( - /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ - jsonName: "bip340_schnorr_verify", - testFunction: { (group: SchnorrTestGroup) in - var numberOfTestsRun = 0 - try group.tests.forEach(doTestSchnorrVerify) - numberOfTestsRun += 1 - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) - }) - - print("☑️ Test result: \(String(describing: result))") - } + override func setUp() { + super.setUp() + continueAfterFailure = false + } + + func testSchnorrSignBitcoinVectors() throws { + let _: TestResult = try testSuite( + /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ + jsonName: "bip340_schnorr_sign", + testFunction: { (group: SchnorrTestGroup) in + var numberOfTestsRun = 0 + try group.tests.forEach(doTestSchnorrSign) + numberOfTestsRun += 1 + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) + } + ) + } + + func testSchnorrVerifyBitcoinVectors() throws { + let _: TestResult = + try testSuite( + /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ + jsonName: "bip340_schnorr_verify", + testFunction: { (group: SchnorrTestGroup) in + var numberOfTestsRun = 0 + try group.tests.forEach(doTestSchnorrVerify) + numberOfTestsRun += 1 + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) + } + ) + } } private extension SchnorrSignatureBitcoinCoreTests { - func doTestSchnorrSign(vector: SchnorrTestSignVector) throws { - - let privateKey = try K1.Schnorr.PrivateKey( - rawRepresentation: Data(hex: vector.privateKeyHex) - ) - - let publicKey = privateKey.publicKey - let expectedPublicKey = try K1.Schnorr.PublicKey(compressedRepresentation: Data(hex: vector.publicKeyHex)) - XCTAssertEqual(publicKey, expectedPublicKey) - - XCTAssertEqual( - publicKey.compressedRepresentation, - expectedPublicKey.compressedRepresentation - ) - - let message = try Data(hex: vector.messageHex) - let signature = try privateKey.signature( - for: message, - options: .init(auxilaryRandomData: .init(aux: Data(hex: vector.auxDataHex))) - ) - - let expectedSig = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) - - XCTAssertEqual(signature.rawRepresentation.hex, vector.signatureCompact) - - XCTAssertEqual( - publicKey.isValidSignature(expectedSig, hashed: message), - vector.isValid - ) - } - - func doTestSchnorrVerify(vector: SchnorrTestVector) throws { - func parsePublicKey() throws -> K1.Schnorr.PublicKey { - try .init(compressedRepresentation: Data(hex: vector.publicKeyHex)) - } - guard !vector.invalidPublicKey else { - XCTAssertThrowsError(try parsePublicKey(), "") { anyError in - if let error = anyError as? K1.Error { - XCTAssertEqual(error, K1.Error.failedToDeserializePublicKey) - } else { - XCTFail("Failed to cast error") - } - } - return - } - let publicKey = try parsePublicKey() - - let signature = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) - - let validSignature = try publicKey.isValidSignature( - signature, hashed: Data(hex: vector.messageHex) - ) - - XCTAssertEqual(validSignature, vector.isValid) - } + func doTestSchnorrSign(vector: SchnorrTestSignVector) throws { + let privateKey = try K1.Schnorr.PrivateKey( + rawRepresentation: Data(hex: vector.privateKeyHex) + ) + + let publicKey = privateKey.publicKey + let expectedPublicKey = try K1.Schnorr.PublicKey(compressedRepresentation: Data(hex: vector.publicKeyHex)) + XCTAssertEqual(publicKey, expectedPublicKey) + + XCTAssertEqual( + publicKey.compressedRepresentation, + expectedPublicKey.compressedRepresentation + ) + + let message = try Data(hex: vector.messageHex) + let signature = try privateKey.signature( + for: message, + options: .init(auxilaryRandomData: .init(aux: Data(hex: vector.auxDataHex))) + ) + + let expectedSig = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) + + XCTAssertEqual(signature.rawRepresentation.hex, vector.signatureCompact) + + XCTAssertEqual( + publicKey.isValidSignature(expectedSig, hashed: message), + vector.isValid + ) + } + + func doTestSchnorrVerify(vector: SchnorrTestVector) throws { + func parsePublicKey() throws -> K1.Schnorr.PublicKey { + try .init(compressedRepresentation: Data(hex: vector.publicKeyHex)) + } + guard !vector.invalidPublicKey else { + XCTAssertThrowsError(try parsePublicKey(), "") { anyError in + if let error = anyError as? K1.Error { + XCTAssertEqual(error, K1.Error.failedToDeserializePublicKey) + } else { + XCTFail("Failed to cast error") + } + } + return + } + let publicKey = try parsePublicKey() + + let signature = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) + + let validSignature = try publicKey.isValidSignature( + signature, hashed: Data(hex: vector.messageHex) + ) + + XCTAssertEqual(validSignature, vector.isValid) + } } extension SchnorrTestVector { - func hasFlag(_ flag: String) -> Bool { - guard let flags = flags else { return false } - return flags.contains(flag) - } - var invalidPublicKey: Bool { - hasFlag("InvalidPublicKey") - } + func hasFlag(_ flag: String) -> Bool { + guard let flags = flags else { return false } + return flags.contains(flag) + } + + var invalidPublicKey: Bool { + hasFlag("InvalidPublicKey") + } } diff --git a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift index e401a37..a9fcdaa 100644 --- a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift +++ b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift @@ -1,23 +1,14 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-03. -// - +import CryptoKit import Foundation import K1 import XCTest -import CryptoKit final class SchnorrSignatureTests: XCTestCase { - - func testSchnorr() throws { - let alice = K1.Schnorr.PrivateKey() - let message = "Send Bob 3 BTC".data(using: .utf8)! - let signature = try alice.signature(forUnhashed: message) - let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) - XCTAssertTrue(isSignatureValid, "Signature should be valid.") - } - + func testSchnorr() throws { + let alice = K1.Schnorr.PrivateKey() + let message = "Send Bob 3 BTC".data(using: .utf8)! + let signature = try alice.signature(forUnhashed: message) + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) + XCTAssertTrue(isSignatureValid, "Signature should be valid.") + } } diff --git a/Tests/K1Tests/Util/ECSignature.swift b/Tests/K1Tests/Util/ECSignature.swift index df0c297..b0ef872 100644 --- a/Tests/K1Tests/Util/ECSignature.swift +++ b/Tests/K1Tests/Util/ECSignature.swift @@ -1,13 +1,11 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-19. -// - import Foundation import K1 +// MARK: - ECSignature public protocol ECSignature {} + +// MARK: - K1.ECDSA.NonRecoverable.Signature + ECSignature extension K1.ECDSA.NonRecoverable.Signature: ECSignature {} + +// MARK: - K1.ECDSA.Recoverable.Signature + ECSignature extension K1.ECDSA.Recoverable.Signature: ECSignature {} diff --git a/Tests/K1Tests/Util/Wycheproof.swift b/Tests/K1Tests/Util/Wycheproof.swift index 568d83f..0ad0375 100644 --- a/Tests/K1Tests/Util/Wycheproof.swift +++ b/Tests/K1Tests/Util/Wycheproof.swift @@ -1,5 +1,4 @@ -// From: https://github.com/apple/swift-crypto/blob/main/Tests/_CryptoExtrasTests/Utils/Wycheproof.swift -// Commit: 794901c991bf3fa0431ba3c0927ba078799c6911 +// swiftformat:disable strip //===----------------------------------------------------------------------===// // @@ -14,208 +13,216 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import XCTest -import CryptoKit +// From: https://github.com/apple/swift-crypto/blob/main/Tests/_CryptoExtrasTests/Utils/Wycheproof.swift +// Commit: 794901c991bf3fa0431ba3c0927ba078799c6911 + +// swiftformat:enable strip + +import CryptoKit @testable import K1 +import XCTest +// MARK: - TestSuite struct TestSuite: Decodable { - let algorithm: String - let numberOfTests: UInt32 - let testGroups: [T] + let algorithm: String + let numberOfTests: UInt32 + let testGroups: [T] } +// MARK: - TestResult struct TestResult { - let numberOfTestsInSource: Int - - /// Might be less than `numberOfTestsInSource` if some tests were omitted. - let numberOfTestsRun: Int - - let idsOfOmittedTests: [Int] + let numberOfTestsInSource: Int + + /// Might be less than `numberOfTestsInSource` if some tests were omitted. + let numberOfTestsRun: Int + + let idsOfOmittedTests: [Int] } extension XCTestCase { - func testSuite( - jsonName: String, - file: StaticString = #file, - line: UInt = #line, - testFunction: (T) throws -> ResultOfTestGroup - ) throws -> TestResult { - let fileURL = Bundle.module.url(forResource: jsonName, withExtension: ".json") - let data = try Data(contentsOf: fileURL!) - - let decoder = JSONDecoder() - let wpTest = try decoder.decode(TestSuite.self, from: data) - var numberOfTestsRun = 0 - var idsOfOmittedTests = Array() - for group in wpTest.testGroups { - let resultOfGroup = try testFunction(group) - numberOfTestsRun += resultOfGroup.numberOfTestsRun - idsOfOmittedTests.append(contentsOf: resultOfGroup.idsOmittedTests) - } - return .init( - numberOfTestsInSource: Int(wpTest.numberOfTests), - numberOfTestsRun: numberOfTestsRun, - idsOfOmittedTests: idsOfOmittedTests - ) - } - + func testSuite( + jsonName: String, + file: StaticString = #file, + line: UInt = #line, + testFunction: (T) throws -> ResultOfTestGroup + ) throws -> TestResult { + let fileURL = Bundle.module.url(forResource: jsonName, withExtension: ".json") + let data = try Data(contentsOf: fileURL!) + + let decoder = JSONDecoder() + let wpTest = try decoder.decode(TestSuite.self, from: data) + var numberOfTestsRun = 0 + var idsOfOmittedTests = [Int]() + for group in wpTest.testGroups { + let resultOfGroup = try testFunction(group) + numberOfTestsRun += resultOfGroup.numberOfTestsRun + idsOfOmittedTests.append(contentsOf: resultOfGroup.idsOmittedTests) + } + return .init( + numberOfTestsInSource: Int(wpTest.numberOfTests), + numberOfTestsRun: numberOfTestsRun, + idsOfOmittedTests: idsOfOmittedTests + ) + } } + +// MARK: - ResultOfTestGroup struct ResultOfTestGroup { - let numberOfTestsRun: Int - let idsOmittedTests: [Int] + let numberOfTestsRun: Int + let idsOmittedTests: [Int] } extension XCTestCase { - - func doTestGroup( - group: ECDSAWycheTestGroup, - signatureValidationMode: K1.ECDSA.ValidationOptions = .default, - hashFunction: HF.Type, - skipIfContainsFlags: [String] = [], - skipIfContainsComment: [String] = [], - file: StaticString = #file, - line: UInt = #line - ) throws -> ResultOfTestGroup { - guard group.key.curve == "secp256k1" else { - let errorMessage = "Key in test group is on wrong EC curve: \(group.key.curve), expected 'secp256k1'" - throw ECDSASignatureTestError(description: errorMessage) - } - let keyBytes = try Array(hex: group.key.uncompressed) - let key = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: keyBytes) - - let keyFromDER = try K1.ECDSA.NonRecoverable.PublicKey(derRepresentation: Data(hex: group.keyDer)) - XCTAssertEqual(key.derRepresentation.hex, group.keyDer) - XCTAssertEqual(keyFromDER, key) - - let keyFromPEM = try K1.ECDSA.NonRecoverable.PublicKey(pemRepresentation: group.keyPem) - XCTAssertEqual(key.pemRepresentation, group.keyPem) - XCTAssertEqual(keyFromPEM, key) - - - let compactXRaw = try Data(hex: group.key.wx) - let compactYRaw = try Data(hex: group.key.wy) - func ensure32Bytes(_ compactComponent: Data) throws -> Data { - if compactComponent.count == Curve.Field.byteCount { - return compactComponent - } - - var compactComponent = [UInt8](compactComponent) - while compactComponent.count < Curve.Field.byteCount { - compactComponent = [0x00] + compactComponent - } - while compactComponent.count > Curve.Field.byteCount { - guard compactComponent.first == 0x00 else { - throw BadKeyComponent() - } - compactComponent = [UInt8](compactComponent.dropFirst()) - } - return Data(compactComponent) - } - let xOnly = try ensure32Bytes(compactXRaw) - let yOnly = try ensure32Bytes(compactYRaw) - - let fromRaw = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: xOnly + yOnly) - XCTAssertEqual(fromRaw, key) - - var numberOfTestsRun = 0 - var idsOfOmittedTests = Array() - outerloop: for testVector in group.tests { - let testVectorFlags = Set(testVector.flags) - if testVector.msg == "" || !testVectorFlags.isDisjoint(with: Set(skipIfContainsFlags)) { - idsOfOmittedTests.append(testVector.tcId) - continue - } - - for comment in skipIfContainsComment { - if testVector.comment.contains(comment) { - idsOfOmittedTests.append(testVector.tcId) - continue outerloop - } - } - - numberOfTestsRun += 1 - var isValid = false - do { - let signature = try testVector.expectedSignature() - let messageDigest = try testVector.messageDigest() - - isValid = key.isValidSignature( - signature, - digest: messageDigest, - options: signatureValidationMode - ) - } catch { - let expectedFailure = testVector.result == "invalid" || testVector.result == "acceptable" - let errorMessage = String(describing: error) - if !expectedFailure { - print("❌ Test ID: \(testVector.tcId) is valid, but failed \(errorMessage).") - } - XCTAssert(expectedFailure, "Test ID: \(testVector.tcId) is valid, but failed \(errorMessage).", file: file, line: line) - continue - } - - switch testVector.result { - case "valid": - if !isValid { - print("❌ Test vector is valid, but is rejected \(testVector.tcId)") - } - XCTAssert(isValid, "Test vector is valid, but is rejected \(testVector.tcId)", file: file, line: line) - case "acceptable": - XCTAssert(isValid, "'acceptable' test vector, expected isValid to be `true`, but was not, tcID: \(testVector.tcId)" ,file: file, line: line) - case "invalid": - if isValid { - print("❌ Test ID: \(testVector.tcId) is valid (we expected 'invalid'), but failed.") - } - XCTAssert(!isValid, "Test ID: \(testVector.tcId) is valid (we expected 'invalid'), but failed.", file: file, line: line) - default: - XCTFail("Unhandled test vector", file: file, line: line) - } - } - return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: idsOfOmittedTests) - } + func doTestGroup( + group: ECDSAWycheTestGroup, + signatureValidationMode: K1.ECDSA.ValidationOptions = .default, + hashFunction: HF.Type, + skipIfContainsFlags: [String] = [], + skipIfContainsComment: [String] = [], + file: StaticString = #file, + line: UInt = #line + ) throws -> ResultOfTestGroup { + guard group.key.curve == "secp256k1" else { + let errorMessage = "Key in test group is on wrong EC curve: \(group.key.curve), expected 'secp256k1'" + throw ECDSASignatureTestError(description: errorMessage) + } + let keyBytes = try Array(hex: group.key.uncompressed) + let key = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: keyBytes) + + let keyFromDER = try K1.ECDSA.NonRecoverable.PublicKey(derRepresentation: Data(hex: group.keyDer)) + XCTAssertEqual(key.derRepresentation.hex, group.keyDer) + XCTAssertEqual(keyFromDER, key) + + let keyFromPEM = try K1.ECDSA.NonRecoverable.PublicKey(pemRepresentation: group.keyPem) + XCTAssertEqual(key.pemRepresentation, group.keyPem) + XCTAssertEqual(keyFromPEM, key) + + let compactXRaw = try Data(hex: group.key.wx) + let compactYRaw = try Data(hex: group.key.wy) + func ensure32Bytes(_ compactComponent: Data) throws -> Data { + if compactComponent.count == Curve.Field.byteCount { + return compactComponent + } + + var compactComponent = [UInt8](compactComponent) + while compactComponent.count < Curve.Field.byteCount { + compactComponent = [0x00] + compactComponent + } + while compactComponent.count > Curve.Field.byteCount { + guard compactComponent.first == 0x00 else { + throw BadKeyComponent() + } + compactComponent = [UInt8](compactComponent.dropFirst()) + } + return Data(compactComponent) + } + let xOnly = try ensure32Bytes(compactXRaw) + let yOnly = try ensure32Bytes(compactYRaw) + + let fromRaw = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: xOnly + yOnly) + XCTAssertEqual(fromRaw, key) + + var numberOfTestsRun = 0 + var idsOfOmittedTests = [Int]() + outerloop: for testVector in group.tests { + let testVectorFlags = Set(testVector.flags) + if testVector.msg == "" || !testVectorFlags.isDisjoint(with: Set(skipIfContainsFlags)) { + idsOfOmittedTests.append(testVector.tcId) + continue + } + + for comment in skipIfContainsComment { + if testVector.comment.contains(comment) { + idsOfOmittedTests.append(testVector.tcId) + continue outerloop + } + } + + numberOfTestsRun += 1 + var isValid = false + do { + let signature = try testVector.expectedSignature() + let messageDigest = try testVector.messageDigest() + + isValid = key.isValidSignature( + signature, + digest: messageDigest, + options: signatureValidationMode + ) + } catch { + let expectedFailure = testVector.result == "invalid" || testVector.result == "acceptable" + let errorMessage = String(describing: error) + if !expectedFailure { + print("❌ Test ID: \(testVector.tcId) is valid, but failed \(errorMessage).") + } + XCTAssert(expectedFailure, "Test ID: \(testVector.tcId) is valid, but failed \(errorMessage).", file: file, line: line) + continue + } + + switch testVector.result { + case "valid": + if !isValid { + print("❌ Test vector is valid, but is rejected \(testVector.tcId)") + } + XCTAssert(isValid, "Test vector is valid, but is rejected \(testVector.tcId)", file: file, line: line) + case "acceptable": + XCTAssert(isValid, "'acceptable' test vector, expected isValid to be `true`, but was not, tcID: \(testVector.tcId)", file: file, line: line) + case "invalid": + if isValid { + print("❌ Test ID: \(testVector.tcId) is valid (we expected 'invalid'), but failed.") + } + XCTAssert(!isValid, "Test ID: \(testVector.tcId) is valid (we expected 'invalid'), but failed.", file: file, line: line) + default: + XCTFail("Unhandled test vector", file: file, line: line) + } + } + return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: idsOfOmittedTests) + } } - - +// MARK: - ECDSATestGroup struct ECDSATestGroup: Codable { - let tests: [TV] + let tests: [TV] } - +// MARK: - ECDSAWycheTestGroup struct ECDSAWycheTestGroup: Codable { - let tests: [TV] - let key: ECDSAKey - let keyDer: String - let keyPem: String + let tests: [TV] + let key: ECDSAKey + let keyDer: String + let keyPem: String } - +// MARK: - ECDSAKey struct ECDSAKey: Codable { - let uncompressed: String - let wx: String - let wy: String - let curve: String + let uncompressed: String + let wx: String + let wy: String + let curve: String } - +// MARK: - SignatureTestVector protocol SignatureTestVector: Codable { - associatedtype MessageDigest: Digest - associatedtype Signature: ECSignature - func messageDigest() throws -> MessageDigest - func expectedSignature() throws -> Signature + associatedtype MessageDigest: Digest + associatedtype Signature: ECSignature + func messageDigest() throws -> MessageDigest + func expectedSignature() throws -> Signature } + +// MARK: - WycheproofTestVector protocol WycheproofTestVector: SignatureTestVector where Signature == K1.ECDSA.NonRecoverable.Signature { - var flags: [String] { get } - var tcId: Int { get } - var result: String { get } - var msg: String { get } - var comment: String { get } + var flags: [String] { get } + var tcId: Int { get } + var result: String { get } + var msg: String { get } + var comment: String { get } } - - +// MARK: - ECDSASignatureTestError struct ECDSASignatureTestError: Swift.Error, CustomStringConvertible { - let description: String + let description: String } + +// MARK: - BadKeyComponent struct BadKeyComponent: Swift.Error {} diff --git a/Tests/K1Tests/Util/XCTestUtils.swift b/Tests/K1Tests/Util/XCTestUtils.swift index 7958fbd..fa13186 100644 --- a/Tests/K1Tests/Util/XCTestUtils.swift +++ b/Tests/K1Tests/Util/XCTestUtils.swift @@ -1,17 +1,17 @@ import XCTest extension XCTestCase { - func assert( - _ fn: @autoclosure () throws -> T, - throws expectedError: E, - file: StaticString = #file, line: UInt = #line - ) { - XCTAssertThrowsError(try fn()) { anyError in - guard let error = anyError as? E else { - XCTFail("Incorrect type of error, got '\(type(of: anyError))' but expected: \(E.self)") - return - } - XCTAssertEqual(error, expectedError) - } - } + func assert( + _ fn: @autoclosure () throws -> T, + throws expectedError: E, + file: StaticString = #file, line: UInt = #line + ) { + XCTAssertThrowsError(try fn()) { anyError in + guard let error = anyError as? E else { + XCTFail("Incorrect type of error, got '\(type(of: anyError))' but expected: \(E.self)") + return + } + XCTAssertEqual(error, expectedError) + } + } } diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..9b0e4ab --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -o errexit # exit out if any command fails +set -o nounset # exit out if any used variable is unset + +echo "Installing CLI tools (if missing)" +if ! command -v grealpath &> /dev/null; then + brew install coreutils +fi +if ! command -v swiftformat &> /dev/null; then + brew install swiftformat +fi + +DIR=$(cd "$(dirname "$BASH_SOURCE")" && pwd) + +pushd "$DIR/.." > /dev/null + +echo "Symlinking Git hooks" +GIT_HOOKS_DIR=.git/hooks +mkdir -p "$GIT_HOOKS_DIR" +ln -sf "$(grealpath --relative-to=.git/hooks scripts)/pre-commit" "$GIT_HOOKS_DIR" + +echo "Done ✅" + +popd > /dev/null diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index daf5136..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - - -set -eu -LIB="Sources/secp256k1/libsecp256k1" -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$here"/.. -rm -rf $LIB - -git submodule init -git submodule update diff --git a/scripts/git-format-staged b/scripts/git-format-staged new file mode 100755 index 0000000..5ac45bf --- /dev/null +++ b/scripts/git-format-staged @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# +# Git command to transform staged files according to a command that accepts file +# content on stdin and produces output on stdout. This command is useful in +# combination with `git add -p` which allows you to stage specific changes in +# a file. This command runs a formatter on the file with staged changes while +# ignoring unstaged changes. +# +# Usage: git-format-staged [OPTION]... [FILE]... +# Example: git-format-staged --formatter 'prettier --stdin-filepath "{}"' '*.js' +# +# Tested with Python 3.10 and Python 2.7. +# +# Original author: Jesse Hallett + +from __future__ import print_function +import argparse +from fnmatch import fnmatch +from gettext import gettext as _ +import os +import re +import subprocess +import sys + +# The string $VERSION is replaced during the publish process. +VERSION = '$VERSION' +PROG = sys.argv[0] + +def info(msg): + print(msg, file=sys.stdout) + +def warn(msg): + print('{}: warning: {}'.format(PROG, msg), file=sys.stderr) + +def fatal(msg): + print('{}: error: {}'.format(PROG, msg), file=sys.stderr) + exit(1) + +def format_staged_files(file_patterns, formatter, git_root, update_working_tree=True, write=True): + try: + output = subprocess.check_output([ + 'git', 'diff-index', + '--cached', + '--diff-filter=AM', # select only file additions and modifications + '--no-renames', + 'HEAD' + ]) + for line in output.splitlines(): + entry = parse_diff(line.decode('utf-8')) + entry_path = normalize_path(entry['src_path'], relative_to=git_root) + if entry['dst_mode'] == '120000': + # Do not process symlinks + continue + if not (matches_some_path(file_patterns, entry_path)): + continue + if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write): + info('Reformatted {} with {}'.format(entry['src_path'], formatter)) + except Exception as err: + fatal(str(err)) + +# Run formatter on file in the git index. Creates a new git object with the +# result, and replaces the content of the file in the index with that object. +# Returns hash of the new object if formatting produced any changes. +def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=True): + orig_hash = diff_entry['dst_hash'] + new_hash = format_object(formatter, orig_hash, diff_entry['src_path']) + + # If the new hash is the same then the formatter did not make any changes. + if not write or new_hash == orig_hash: + return None + + # If the content of the new object is empty then the formatter did not + # produce any output. We want to abort instead of replacing the file with an + # empty one. + if object_is_empty(new_hash): + return None + + replace_file_in_index(diff_entry, new_hash) + + if update_working_tree: + try: + patch_working_file(diff_entry['src_path'], orig_hash, new_hash) + except Exception as err: + # Errors patching working tree files are not fatal + warn(str(err)) + + return new_hash + +file_path_placeholder = re.compile('\{\}') + +# Run formatter on a git blob identified by its hash. Writes output to a new git +# blob, and returns the hash of the new blob. +def format_object(formatter, object_hash, file_path): + get_content = subprocess.Popen( + ['git', 'cat-file', '-p', object_hash], + stdout=subprocess.PIPE + ) + format_content = subprocess.Popen( + re.sub(file_path_placeholder, file_path, formatter), + shell=True, + stdin=get_content.stdout, + stdout=subprocess.PIPE + ) + write_object = subprocess.Popen( + ['git', 'hash-object', '-w', '--stdin'], + stdin=format_content.stdout, + stdout=subprocess.PIPE + ) + + get_content.stdout.close() + format_content.stdout.close() + + if get_content.wait() != 0: + raise ValueError('unable to read file content from object database: ' + object_hash) + + if format_content.wait() != 0: + raise Exception('formatter exited with non-zero status') # TODO: capture stderr from format command + + new_hash, err = write_object.communicate() + + if write_object.returncode != 0: + raise Exception('unable to write formatted content to object database') + + return new_hash.decode('utf-8').rstrip() + +def object_is_empty(object_hash): + get_content = subprocess.Popen( + ['git', 'cat-file', '-p', object_hash], + stdout=subprocess.PIPE + ) + content, err = get_content.communicate() + + if get_content.returncode != 0: + raise Exception('unable to verify content of formatted object') + + return not content + +def replace_file_in_index(diff_entry, new_object_hash): + subprocess.check_call(['git', 'update-index', + '--cacheinfo', '{},{},{}'.format( + diff_entry['dst_mode'], + new_object_hash, + diff_entry['src_path'] + )]) + +def patch_working_file(path, orig_object_hash, new_object_hash): + patch = subprocess.check_output( + ['git', 'diff', '--color=never', orig_object_hash, new_object_hash] + ) + + # Substitute object hashes in patch header with path to working tree file + patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(new_object_hash.encode(), path.encode()) + + apply_patch = subprocess.Popen( + ['git', 'apply', '-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + output, err = apply_patch.communicate(input=patch_b) + + if apply_patch.returncode != 0: + raise Exception('could not apply formatting changes to working tree file {}'.format(path)) + +# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path? +diff_pat = re.compile('^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$') + +# Parse output from `git diff-index` +def parse_diff(diff): + m = diff_pat.match(diff) + if not m: + raise ValueError('Failed to parse diff-index line: ' + diff) + return { + 'src_mode': unless_zeroed(m.group(1)), + 'dst_mode': unless_zeroed(m.group(2)), + 'src_hash': unless_zeroed(m.group(3)), + 'dst_hash': unless_zeroed(m.group(4)), + 'status': m.group(5), + 'score': int(m.group(6)) if m.group(6) else None, + 'src_path': m.group(7), + 'dst_path': m.group(8) + } + +zeroed_pat = re.compile('^0+$') + +# Returns the argument unless the argument is a string of zeroes, in which case +# returns `None` +def unless_zeroed(s): + return s if not zeroed_pat.match(s) else None + +def get_git_root(): + return subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'] + ).decode('utf-8').rstrip() + +def normalize_path(p, relative_to=None): + return os.path.abspath( + os.path.join(relative_to, p) if relative_to else p + ) + +def matches_some_path(patterns, target): + is_match = False + for signed_pattern in patterns: + (is_pattern_positive, pattern) = from_signed_pattern(signed_pattern) + if fnmatch(target, normalize_path(pattern)): + is_match = is_pattern_positive + return is_match + +# Checks for a '!' as the first character of a pattern, returns the rest of the +# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern). +# For example: +# from_signed_pattern('!pat') == (False, 'pat') +# from_signed_pattern('pat') == (True, 'pat') +def from_signed_pattern(pattern): + if pattern[0] == '!': + return (False, pattern[1:]) + else: + return (True, pattern) + +class CustomArgumentParser(argparse.ArgumentParser): + def parse_args(self, args=None, namespace=None): + args, argv = self.parse_known_args(args, namespace) + if argv: + msg = argparse._( + 'unrecognized arguments: %s. Do you need to quote your formatter command?' + ) + self.error(msg % ' '.join(argv)) + return args + +if __name__ == '__main__': + parser = CustomArgumentParser( + description='Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.', + epilog='Example: %(prog)s --formatter "prettier --stdin-filepath \'{}\'" "src/*.js" "test/*.js"' + ) + parser.add_argument( + '--formatter', '-f', + required=True, + help='Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin-filepath \'{}\'")' + ) + parser.add_argument( + '--no-update-working-tree', + action='store_true', + help='By default formatting changes made to staged file content will also be applied to working tree files via a patch. This option disables that behavior, leaving working tree files untouched.' + ) + parser.add_argument( + '--no-write', + action='store_true', + help='Prevents %(prog)s from modifying staged or working tree files. You can use this option to check staged changes with a linter instead of formatting. With this option stdout from the formatter command is ignored. Example: %(prog)s --no-write -f "eslint --stdin --stdin-filename \'{}\' >&2" "*.js"' + ) + parser.add_argument( + '--version', + action='version', + version='%(prog)s version {}'.format(VERSION), + help='Display version of %(prog)s' + ) + parser.add_argument( + 'files', + nargs='+', + help='Patterns that specify files to format. The formatter will only transform staged files that are given here. Patterns may be literal file paths, or globs which will be tested against staged file paths using Python\'s fnmatch function. For example "src/*.js" will match all files with a .js extension in src/ and its subdirectories. Patterns may be negated to exclude files using a "!" character. Patterns are evaluated left-to-right. (Example: "main.js" "src/*.js" "test/*.js" "!test/todo/*")' + ) + args = parser.parse_args() + files = vars(args)['files'] + format_staged_files( + file_patterns=files, + formatter=vars(args)['formatter'], + git_root=get_git_root(), + update_working_tree=not vars(args)['no_update_working_tree'], + write=not vars(args)['no_write'] + ) \ No newline at end of file diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..8e46702 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,5 @@ +#!/bin/bash + +DIR="$(dirname "$(readlink -f "$BASH_SOURCE")")" + +"$DIR/swiftformat-staged" diff --git a/scripts/swiftformat-staged b/scripts/swiftformat-staged new file mode 100755 index 0000000..e38f7d6 --- /dev/null +++ b/scripts/swiftformat-staged @@ -0,0 +1,5 @@ +#!/bin/bash + +DIR="$(dirname "$(readlink -f "$BASH_SOURCE")")" + +"$DIR/git-format-staged" --formatter "swiftformat stdin --stdinpath '{}'" "*.swift"