From b1f043e3169b7027f7d7b399177ea34ed335ff87 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Thu, 16 Mar 2023 12:27:32 +0100 Subject: [PATCH] Ecdh custom test (#8) --- README.md | 50 +++++++++++++++++-- .../PrivateKey/PrivateKey+Bridge+To+C.swift | 22 ++++---- Tests/K1Tests/TestCases/ECDH/ECDHTests.swift | 30 ++++++++--- 3 files changed, 81 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d7ef4eb..fd047e5 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,57 @@ It is worth noting that some Schnorr implementations are incompatible with [BIP3 ## ECDH +This library vendors three different EC Diffie-Hellman (ECDH) key exchange functions: +1. `ASN1 x9.63` - No hash, return only the `X` coordinate of the point - `sharedSecretFromKeyAgreement -> SharedSecret` +2. `libsecp256k1` - SHA-256 hash the compressed point - `ecdh -> Data` +3. Custom - No hash, return point uncompressed - `ecdhPoint -> Data` + ```swift let alice = try K1.PrivateKey.generateNew() let bob = try K1.PrivateKey.generateNew() +``` + +### `ASN1 x9.63` ECDH +Returning only the `X` coordinate of the point, following [ANSI X9.63][x963] standards, embedded in a [`CryptoKit.SharedSecret`][ckss], which is useful since you can use `CryptoKit` key derivation functions on this SharedSecret, e.g. [`x963DerivedSymmetricKey`](https://developer.apple.com/documentation/cryptokit/sharedsecret/x963derivedsymmetrickey(using:sharedinfo:outputbytecount:)) or [`hkdfDerivedSymmetricKey`](https://developer.apple.com/documentation/cryptokit/sharedsecret/hkdfderivedsymmetrickey(using:salt:sharedinfo:outputbytecount:)). + +You can retrieve the `X` coordinate as raw data using `withUnsafeBytes` if you need to. + +```swift +let ab: CryptoKit.SharedSecret = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) +let ba: : CryptoKit.SharedSecret = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) + +assert(ab == ba) // pass + +ab.withUnsafeBytes { + assert(Data($0).count == 32) // pass +} +``` + +### `libsecp256k1` ECDH -let ab = try alice.sharedSecret(with: bob.publicKey) -let ba = try bob.sharedSecret(with: alice.publicKey) -assert(ab == ba, "Alice and Bob should be able to agree on the same secret") +Using `libsecp256k1` default behaviour, returning a SHA-256 hash of the **compressed** point. + +```swift +let ab: Data = try alice.ecdh(with: bob.publicKey) +let ba: Data = try bob.ecdh(with: alice.publicKey) +assert(ab == ba) // pass + +assert(ab.count == 32) // pass +``` + +### Custom ECDH + +Returns an entire uncompresed EC point, without hashing it. Might be useful if you wanna construct your own cryptographic functions, e.g. some custom ECIES. + +```swift +let ab: Data = try alice.ecdhPoint(with: bob.publicKey) +let ba: Data = try bob.ecdhPoint(with: alice.publicKey) +assert(ab == ba) // pass + +assert(ab.count == 65) // pass ``` + # Alternatives - [GigaBitcoin/secp256k1.swift](https://github.com/GigaBitcoin/secp256k1.swift) (also using `libsecp256k1`, ⚠️ possibly unsafe, ✅ Schnorr support) @@ -70,3 +112,5 @@ To clone the dependency [libsecp256k1][lib], using commit [427bc3cdcfbc747780704 [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki [lib]: https://github.com/bitcoin-core/secp256k1 +[x963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 +[ckss]: https://developer.apple.com/documentation/cryptokit/sharedsecret diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift index aef6a7c..6b27eec 100644 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift +++ b/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift @@ -548,16 +548,14 @@ extension K1.PrivateKey { return sharedSecretData } - - /// 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. SHA-256 hash the compressed point - /// 2. No hash, return point uncompressed - /// 3. No hash, return only the `X` coordinate of the point <- this function + /// 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. @@ -591,9 +589,9 @@ extension K1.PrivateKey { /// /// This is one of three ECDH functions, this library vendors, all three versions /// uses different serialization of the shared EC Point, specifically: - /// 1. SHA-256 hash the compressed point <- this function - /// 2. No hash, return point uncompressed - /// 3. No hash, return only the `X` coordinate of the point. + /// 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 @@ -611,14 +609,15 @@ extension K1.PrivateKey { try _ecdh(publicKey: publicKey, serializeOutputFunction: .libsecp256kDefault) } + /// 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. SHA-256 hash the compressed point - /// 2. No hash, return point uncompressed <- this function - /// 3. No hash, return only the `X` coordinate of the point. + /// 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 @@ -627,6 +626,7 @@ extension K1.PrivateKey { public func ecdhPoint(with publicKey: K1.PublicKey) throws -> Data { try _ecdh(publicKey: publicKey, serializeOutputFunction: .noHashWholePoint) } + } // MUST match https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L34 diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift index afad938..7f47e51 100644 --- a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift @@ -11,15 +11,18 @@ import XCTest final class ECDHTests: XCTestCase { - func testECDHPoint() throws { + func testECDHX963() throws { let alice = try K1.PrivateKey.generateNew() let bob = try K1.PrivateKey.generateNew() - let ab = try alice.ecdhPoint(with: bob.publicKey) - let ba = try bob.ecdhPoint(with: alice.publicKey) + 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 = try K1.PrivateKey.generateNew() let bob = try K1.PrivateKey.generateNew() @@ -27,15 +30,17 @@ final class ECDHTests: XCTestCase { let ab = try alice.ecdh(with: bob.publicKey) let ba = try bob.ecdh(with: alice.publicKey) XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") + XCTAssertEqual(ab.count, 32) } - func testECDHX963() throws { + func testECDHPoint() throws { let alice = try K1.PrivateKey.generateNew() let bob = try K1.PrivateKey.generateNew() - let ab = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) - let ba = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) + 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 @@ -50,6 +55,17 @@ final class ECDHTests: XCTestCase { XCTAssertEqual(ans1X963.hex, "3a17fe5fa33c4f2c7e61799a65061214913f39bfcbee178ab351493d5ee17b2f") } + + /// Assert we do not introduce any regression bugs for the custom ECDh `ecdhPoint` + func testECDHCustom() throws { + let alice = try K1.PrivateKey.import(rawRepresentation: Data(repeating: 0xAA, count: 32)) + let bob = try K1.PrivateKey.import(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.PrivateKey {