Skip to content

Commit

Permalink
Ecdh custom test (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sajjon committed Mar 16, 2023
1 parent 27805f1 commit b1f043e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 21 deletions.
50 changes: 47 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
30 changes: 23 additions & 7 deletions Tests/K1Tests/TestCases/ECDH/ECDHTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,36 @@ 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()

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
Expand All @@ -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 {
Expand Down

0 comments on commit b1f043e

Please sign in to comment.