From d3df9f50555f68c4780efeecba56f041b741e2ed Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Wed, 14 Feb 2018 16:24:29 +0100 Subject: [PATCH] Libsecp256k1 backend (#2) * Add safe hex, endianness and bytes conversion tools * comment typo endianess -> endianness * Add libsecp256k1 - private and public keygen and serialization + tests --- .gitignore | 5 +- eth_keys.nimble | 3 +- src/backend_libsecp256k1/libsecp256k1.nim | 53 +++++++++ src/datatypes.nim | 7 +- src/datatypes_interface.nim | 73 ++++++------- src/private/lowlevel_types.nim | 101 ++++++++++++++++++ tests/all_tests.nim | 4 +- tests/config.nim | 16 +-- tests/test_hex_bytes_conversion.nim | 23 ++++ tests/test_private_public_key_consistency.nim | 16 +++ 10 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 src/backend_libsecp256k1/libsecp256k1.nim create mode 100644 src/private/lowlevel_types.nim create mode 100644 tests/test_hex_bytes_conversion.nim create mode 100644 tests/test_private_public_key_consistency.nim diff --git a/.gitignore b/.gitignore index 8160cf0..471d0fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ nimcache/ # Executables shall be put in an ignored build/ directory +# Ignore dynamic, static libs and libtool archive files build/ *.so -*.dylib \ No newline at end of file +*.dylib +*.a +*.la \ No newline at end of file diff --git a/eth_keys.nimble b/eth_keys.nimble index c2ac065..06b7105 100644 --- a/eth_keys.nimble +++ b/eth_keys.nimble @@ -6,7 +6,8 @@ license = "MIT" srcDir = "src" ### Dependencies -requires "nim >= 0.17.2", "keccak_tiny >= 0.1.0", "ttmath >= 0.1.0", "nimSHA2" + +requires "nim >= 0.17.2", "keccak_tiny >= 0.1.0", "ttmath >= 0.1.0", "nimSHA2", "secp256k1" proc test(name: string, lang: string = "cpp") = if not dirExists "build": diff --git a/src/backend_libsecp256k1/libsecp256k1.nim b/src/backend_libsecp256k1/libsecp256k1.nim new file mode 100644 index 0000000..33160e1 --- /dev/null +++ b/src/backend_libsecp256k1/libsecp256k1.nim @@ -0,0 +1,53 @@ +import ../datatypes +import secp256k1 + +const SECP256K1_CONTEXT_ALL = SECP256K1_CONTEXT_VERIFY or SECP256K1_CONTEXT_SIGN + +let ctx = secp256k1_context_create(SECP256K1_CONTEXT_ALL) + +{.experimental.} +proc `=destroy`(ctx: ptr secp256k1_context) = + if not ctx.isNil: + ctx.secp256k1_context_destroy + +type Serialized_PubKey = ByteArrayBE[65] + # header 0x04 (uncompressed) + 128 hex char + +proc asPtrPubKey(key: PublicKey): ptr secp256k1_pubkey = + cast[ptr secp256k1_pubkey](unsafeAddr key.raw_key) + +proc asPtrCuchar(key: PrivateKey): ptr cuchar = + cast[ptr cuchar](unsafeAddr key.raw_key) + +proc asPtrCuchar(key: Serialized_PubKey): ptr cuchar = + cast[ptr cuchar](unsafeAddr key) + +proc private_key_to_public_key*(key: PrivateKey): PublicKey {.noInit.}= + + let valid:bool = bool secp256k1_ec_pubkey_create( + ctx, + result.asPtrPubKey, + key.asPtrCuchar + ) + + if not valid: + raise newException(ValueError, "Private key is invalid") + +proc serialize*(key: PublicKey): string = + + var + tmp{.noInit.}: Serialized_PubKey + tmp_len: csize = 65 + + # Proc always return 1 + discard secp256k1_ec_pubkey_serialize( + ctx, + tmp.asPtrCuchar, + addr tmp_len, + key.asPtrPubKey, + SECP256K1_EC_UNCOMPRESSED + ) + + assert tmp_len == 65 # header 0x04 (uncompressed) + 128 hex char + + result = tmp.toHex diff --git a/src/datatypes.nim b/src/datatypes.nim index 44e5e6b..1cfb5e5 100644 --- a/src/datatypes.nim +++ b/src/datatypes.nim @@ -1,14 +1,15 @@ # Copyright (c) 2018 Status Research & Development GmbH # Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). -import strutils, ttmath +import ttmath, ./private/lowlevel_types +export lowlevel_types type PublicKey* = object - raw_key*: array[2, UInt256] + raw_key*: ByteArrayBE[64] PrivateKey* = object - raw_key*: UInt256 + raw_key*: ByteArrayBE[32] public_key*: PublicKey BaseKey* = PrivateKey|PublicKey diff --git a/src/datatypes_interface.nim b/src/datatypes_interface.nim index 3df6fe0..767689b 100644 --- a/src/datatypes_interface.nim +++ b/src/datatypes_interface.nim @@ -7,57 +7,50 @@ # Note: for now only a native pure Nim backend is supported # In the future alternative, proven crypto backend will be added like libsecpk1 -import ./private/hex, ./datatypes -import keccak_tiny, ttmath +import ./datatypes, + ttmath +# import keccak_tiny -import ./backend_native/ecdsa +when defined(backend_native): + import ./backend_native/ecdsa +else: + import ./backend_libsecp256k1/libsecp256k1 + export libsecp256k1.serialize # ################################ # Initialization -proc initPublicKey*(hexString: string): PublicKey = - assert hexString.len == 128 - result.raw_key[0] = hexToUInt256(hexString[0..<64]) - result.raw_key[1] = hexToUInt256(hexString[64..<128]) - -proc initPrivateKey*(hexString: string): PrivateKey = - assert hexString.len == 64 - result.raw_key = hexToUInt256(hexString) +proc initPrivateKey*(hexString: string): PrivateKey {.noInit.}= + result.raw_key = hexToByteArrayBE[32](hexString) result.public_key = private_key_to_public_key(result) -# ################################ -# Hex -proc toHex*(key: PrivateKey): string = - result = key.raw_key.toHex +# proc initPublicKey*(hexString: string): PublicKey {.noInit, noSideEffect.}= +# result.raw_key = hexToByteArrayBE[64](hexString) -proc toHex*(key: PublicKey): string = - result = key.raw_key[0].toHex - result.add key.raw_key[1].toHex +# # ################################ +# # Public key interface +# proc recover_pubkey_from_msg_hash*(message_hash: Hash[256], sig: Signature): PublicKey {.inline.}= +# ecdsa_raw_recover(message_hash, sig) -# ################################ -# Public key interface -proc recover_pubkey_from_msg_hash*(message_hash: Hash[256], sig: Signature): PublicKey {.inline.}= - ecdsa_raw_recover(message_hash, sig) +# proc recover_pubkey_from_msg*(message: string, sig: Signature): PublicKey {.inline.}= +# let message_hash = keccak_256(message) +# result = recover_pubkey_from_msg_hash(message_hash, sig) -proc recover_pubkey_from_msg*(message: string, sig: Signature): PublicKey {.inline.}= - let message_hash = keccak_256(message) - result = recover_pubkey_from_msg_hash(message_hash, sig) +# proc verify_msg_hash*(key: PublicKey, message_hash: Hash[256], sig: Signature): bool {.inline.}= +# key == ecdsa_raw_recover(message_hash, sig) -proc verify_msg_hash*(key: PublicKey, message_hash: Hash[256], sig: Signature): bool {.inline.}= - key == ecdsa_raw_recover(message_hash, sig) +# proc verify_msg*(key: PublicKey, message: string, sig: Signature): bool {.inline.} = +# let message_hash = keccak_256(message) +# key == ecdsa_raw_recover(message_hash, sig) -proc verify_msg*(key: PublicKey, message: string, sig: Signature): bool {.inline.} = - let message_hash = keccak_256(message) - key == ecdsa_raw_recover(message_hash, sig) +# # ################################ +# # Private key interface +# proc sign_msg_hash*(key: PrivateKey, message_hash: Hash[256]): Signature {.inline.}= +# ecdsa_raw_sign(message_hash, key) -# ################################ -# Private key interface -proc sign_msg_hash*(key: PrivateKey, message_hash: Hash[256]): Signature {.inline.}= - ecdsa_raw_sign(message_hash, key) +# proc sign_msg*(key: PrivateKey, message: string): Signature {.inline.} = +# let message_hash = keccak_256(message) +# ecdsa_raw_sign(message_hash, key) -proc sign_msg*(key: PrivateKey, message: string): Signature {.inline.} = - let message_hash = keccak_256(message) - ecdsa_raw_sign(message_hash, key) - -# ################################ -# Signature interface is a duplicate of the public key interface \ No newline at end of file +# # ################################ +# # Signature interface is a duplicate of the public key interface \ No newline at end of file diff --git a/src/private/lowlevel_types.nim b/src/private/lowlevel_types.nim new file mode 100644 index 0000000..f41c20b --- /dev/null +++ b/src/private/lowlevel_types.nim @@ -0,0 +1,101 @@ +# Copyright (c) 2018 Status Research & Development GmbH +# Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). + +import ttmath, strutils, strutils + +# Note on endianness: +# - UInt256 uses host endianness +# - Libsecp256k1, Ethereum EVM expect Big Endian +# https://github.com/ethereum/evmjit/issues/91 +# - Keccak expects least-significant byte first: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +# Appendix B.1 p37 and outputs a hash with the same endianness as input +# http://www.dianacoman.com/2018/02/08/eucrypt-chapter-9-byte-order-and-bit-disorder-in-keccak/ +# https://www.reddit.com/r/crypto/comments/6287my/explanations_on_the_keccaksha3_paddingbyte/ +# Note: Since Nim's Keccak-Tiny only accepts string as input, endianness does not matter. + +type ByteArrayBE*[N: static[int]] = distinct array[N, byte] + ## A byte array that stores bytes in big-endian order + +proc `[]`*[N: static[int], I: Ordinal](ba: ByteArrayBE[N], i: I): byte {.noSideEffect.}= + (array[N,byte])(ba)[i] + +proc `[]=`*[N: static[int], I: Ordinal](ba: var ByteArrayBE[N], i: I, val: byte) {.noSideEffect.}= + (array[N,byte])(ba)[i] = val + +proc readUint256BE*(ba: ByteArrayBE[32]): UInt256 {.noSideEffect.}= + ## Convert a big-endian array of Bytes to an UInt256 (in native host endianness) + const N = 32 + for i in 0 ..< N: + result = result shl 8 or ba[i].u256 + +proc toByteArrayBE*(num: UInt256): ByteArrayBE[32] {.noSideEffect, noInit.}= + ## Convert an UInt256 (in native host endianness) to a big-endian byte array + const N = 32 + for i in 0 ..< N: + result[i] = byte getUInt(num shr uint((N-1-i) * 8)) + +proc readHexChar(c: char): byte {.noSideEffect.}= + ## Converts an hex char to a byte + case c + of '0'..'9': result = byte(ord(c) - ord('0')) + of 'a'..'f': result = byte(ord(c) - ord('a') + 10) + of 'A'..'F': result = byte(ord(c) - ord('A') + 10) + else: + raise newException(ValueError, $c & "is not a hexademical character") + +proc hexToByteArrayBE*[N: static[int]](hexStr: string): ByteArrayBE[N] {.noSideEffect, noInit.}= + ## Read an hex string and store it in a Byte Array in Big-Endian order + var i = 0 + if hexStr[i] == '0' and (hexStr[i+1] == 'x' or hexStr[i+1] == 'X'): + inc(i, 2) # Ignore 0x and 0X prefix + + assert hexStr.len - i == 2*N + + while i < N: + result[i] = hexStr[2*i].readHexChar shl 4 or hexStr[2*i+1].readHexChar + inc(i) + +proc hexToUInt256*(hexStr: string): UInt256 {.noSideEffect.}= + ## Read an hex string and store it in a UInt256 + const N = 32 + + var i = 0 + if hexStr[i] == '0' and (hexStr[i+1] == 'x' or hexStr[i+1] == 'X'): + inc(i, 2) # Ignore 0x and 0X prefix + + assert hexStr.len - i == 2*N + + while i < 2*N: + result = result shl 4 or hexStr[i].readHexChar.uint.u256 + inc(i) + +proc toHex*(n: UInt256): string {.noSideEffect.}= + ## Convert uint256 to its hex representation + ## Output is in lowercase + + var rem = n # reminder to encode + + const + N = 32 # nb of bytes in n + hexChars = "0123456789abcdef" + + result = newString(2*N) + for i in countdown(2*N - 1, 0): + result[i] = hexChars[(rem and 0xF.u256).getUInt.int] + rem = rem shr 4 + +proc toHex*[N: static[int]](ba: ByteArrayBE[N]): string {.noSideEffect.}= + ## Convert a big-endian byte-array to its hex representation + ## Output is in lowercase + ## + ## Warning ⚠: Do not use toHex for hex representation of Public Keys + ## Use the ``serialize`` proc: + ## - PublicKey is actually 2 separate numbers corresponding to coordinate on elliptic curve + ## - It is resistant against timing attack + + const hexChars = "0123456789abcdef" + + result = newString(2*N) + for i in 0 ..< N: + result[2*i] = hexChars[ba[i] shr 4 and 0xF] + result[2*i+1] = hexChars[ba[i] and 0xF] diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 38d0805..3bc36c3 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -1,4 +1,6 @@ # Copyright (c) 2018 Status Research & Development GmbH # Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). -import ./test_key_and_signature_datastructures \ No newline at end of file +import ./test_hex_bytes_conversion, + ./test_private_public_key_consistency + #./test_key_and_signature_datastructures diff --git a/tests/config.nim b/tests/config.nim index 22dac39..c30bd75 100644 --- a/tests/config.nim +++ b/tests/config.nim @@ -28,14 +28,14 @@ import ttmath type testKeySig* = object - privkey*: PrivateKey - pubkey*: PublicKey + privkey*: string + pubkey*: string raw_sig*: Signature let alice* = testKeySig( - privkey: initPrivateKey("9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501"), - pubkey: initPublicKey("5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca"), + privkey: "9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501", + pubkey: "5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca", raw_sig: Signature( v: 1, r: "80536744857756143861726945576089915884233437828013729338039544043241440681784".u256, @@ -44,8 +44,8 @@ let ) bob* = testKeySig( - privkey: initPrivateKey("38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2"), - pubkey: initPublicKey("347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570"), + privkey: "38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2", + pubkey: "347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570", raw_sig: Signature( v: 1, r: "41741612198399299636429810387160790514780876799439767175315078161978521003886".u256, @@ -54,8 +54,8 @@ let ) eve* = testKeySig( - privkey: initPrivateKey("876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c"), - pubkey: initPublicKey("c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0"), + privkey: "876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c", + pubkey: "c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0", raw_sig: Signature( v: 0, r: "84467545608142925331782333363288012579669270632210954476013542647119929595395".u256, diff --git a/tests/test_hex_bytes_conversion.nim b/tests/test_hex_bytes_conversion.nim new file mode 100644 index 0000000..07d40ba --- /dev/null +++ b/tests/test_hex_bytes_conversion.nim @@ -0,0 +1,23 @@ +# Copyright (c) 2018 Status Research & Development GmbH +# Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). + +import ../src/private/lowlevel_types +import unittest, ttmath, strutils + + +suite "Testing conversion functions: Hex, Bytes, Endianness": + let + SECPK1_N_HEX = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141".toLowerAscii + SECPK1_N = "115792089237316195423570985008687907852837564279074904382605163141518161494337".u256 + + test "hex -> uint256": + check: SECPK1_N_HEX.hexToUInt256 == SECPK1_N + + test "uint256 -> hex": + check: SECPK1_N.toHex == SECPK1_N_HEX + + test "hex -> big-endian array -> uint256": + check: hexToByteArrayBE[32](SECPK1_N_HEX).readUint256BE == SECPK1_N + + test "uint256 -> big-endian array -> hex": + check: SECPK1_N.toByteArrayBE.toHex == SECPK1_N_HEX \ No newline at end of file diff --git a/tests/test_private_public_key_consistency.nim b/tests/test_private_public_key_consistency.nim new file mode 100644 index 0000000..49d085b --- /dev/null +++ b/tests/test_private_public_key_consistency.nim @@ -0,0 +1,16 @@ +# Copyright (c) 2018 Status Research & Development GmbH +# Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). + +import ../src/eth_keys, + ./config + +import unittest + +suite "Testing private -> public key conversion": + test "Known private to known public keys (test data from Ethereum eth-keys)": + for person in [alice, bob, eve]: + let privkey = initPrivateKey(person.privkey) + + let computed_pubkey = privkey.public_key.serialize + + check: computed_pubkey == "04" & person.pubkey # Serialization prefixes uncompressed public keys with 04 \ No newline at end of file