diff --git a/eth.nimble b/eth.nimble index 3d3f8a7..9b312e3 100644 --- a/eth.nimble +++ b/eth.nimble @@ -4,12 +4,21 @@ description = "Ethereum Common library" license = "MIT" skipDirs = @["tests"] -requires "nim > 0.18.0", +requires "nim > 0.19.0", "nimcrypto", "ranges", "stint", - "byteutils" + "byteutils", + "secp256k1" + +proc test(filename: string) = + echo "Running: ", filename + exec "nim c -r " & filename + +import strutils, os task test, "run tests": - cd "tests" - exec "nim c -r test_common" + for i in walkDirRec("tests"): + let fn = splitPath(i).tail + if fn.startsWith("test_") and fn.endsWith(".nim"): + test(i) diff --git a/eth/keys.nim b/eth/keys.nim new file mode 100644 index 0000000..8e41700 --- /dev/null +++ b/eth/keys.nim @@ -0,0 +1,162 @@ +# +# Nim Ethereum Keys (nim-eth-keys) +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# - Apache License, version 2.0, (LICENSE-APACHEv2) +# - MIT license (LICENSE-MIT) +# + +import nimcrypto/hash, nimcrypto/keccak + +type + EthKeysStatus* = enum + Success, ## Operation was successful + Error ## Operation failed + + EthKeysException* = object of Exception + ## Exception generated by this module + +when not defined(native): + include eth/keys/libsecp256k1 + + proc ekErrorMsg*(): string {.inline.} = + ## Return current error message. + result = libsecp256k1ErrorMsg() + +proc isZeroKey*(seckey: PrivateKey): bool = + ## Check if private key `seckey` contains only 0 bytes. + result = true + for i in seckey.data: + if i != byte(0): + result = false + +proc isZeroKey*(pubkey: PublicKey): bool = + ## Check if public key `pubkey` contains only 0 bytes. + result = true + for i in pubkey.data: + if i != byte(0): + result = false + +proc signMessage*(seckey: PrivateKey, + data: openarray[byte]): Signature {.inline.} = + ## Sign message of arbitrary length `data` using private key `seckey`. + let hash = keccak256.digest(data) + if signRawMessage(hash.data, seckey, result) != EthKeysStatus.Success: + raise newException(EthKeysException, ekErrorMsg()) + +proc signMessage*(seckey: PrivateKey, data: string): Signature {.inline.} = + ## Sign message of arbitrary length `data` using private key `seckey`. + signMessage(seckey, cast[seq[byte]](data)) + +proc signMessage*(seckey: PrivateKey, + hash: MDigest[256]): Signature {.inline.} = + ## Sign 256bit `hash` using private key `seckey`. + result = signMessage(seckey, hash.data) + +proc verifyMessage*(data: openarray[byte], message: openarray[byte]): bool = + ## Verify binary data blob `data` has properly signed message `message`. + var pubkey: PublicKey + if recoverSignatureKey(data, message, pubkey) == EthKeysStatus.Success: + result = true + else: + result = false + +proc verifyMessage*(data: openarray[byte], + hash: MDigest[256]): bool {.inline.} = + ## Verify binary data blob `data` has properly signed hash `hash`. + result = verifyMessage(data, hash.data) + +proc recoverKeyFromMessage*(data: openarray[byte], + hash: MDigest[256]): PublicKey {.inline.} = + ## Recover public key from signed binary blob `data` using 256bit hash `hash`. + if recoverSignatureKey(data, hash.data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, ekErrorMsg()) + +proc recoverKeyFromMessage*(data: openarray[byte], + message: string): PublicKey {.inline.} = + ## Recover public key from signed binary blob `data` using `message`. + var hash = keccak256.digest(message) + if recoverSignatureKey(data, hash.data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, ekErrorMsg()) + +proc recoverKeyFromSignature*(signature: Signature, + message: string): PublicKey {.inline.} = + ## Recover public key from signature `signature` using `message`. + var hash = keccak256.digest(message) + if recoverSignatureKey(signature, hash.data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, ekErrorMsg()) + +proc recoverKeyFromSignature*(signature: Signature, + hash: MDigest[256]): PublicKey {.inline.} = + ## Recover public key from signature `signature` using `message`. + if recoverSignatureKey(signature, hash.data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, ekErrorMsg()) + +proc toAddress*(pubkey: PublicKey, with0x = true): string = + ## Convert public key to hexadecimal string address. + var hash = keccak256.digest(pubkey.getRaw()) + result = if with0x: "0x" else: "" + result.add(toHex(toOpenArray(hash.data, 12, len(hash.data) - 1), true)) + +proc toChecksumAddress*(pubkey: PublicKey, with0x = true): string = + ## Convert public key to checksumable mixed-case address (EIP-55). + result = if with0x: "0x" else: "" + var hash1 = keccak256.digest(pubkey.getRaw()) + var hhash1 = toHex(toOpenArray(hash1.data, 12, len(hash1.data) - 1), true) + var hash2 = keccak256.digest(hhash1) + var hhash2 = toHex(hash2.data, true) + for i in 0..= '0' and hhash2[i] <= '7': + result.add(hhash1[i]) + else: + if hhash1[i] >= '0' and hhash1[i] <= '9': + result.add(hhash1[i]) + else: + let ch = chr(ord(hhash1[i]) - ord('a') + ord('A')) + result.add(ch) + +proc validateChecksumAddress*(a: string): bool = + ## Validate checksumable mixed-case address (EIP-55). + var address = "" + var check = "0x" + if len(a) != 42: + return false + if a[0] != '0' and a[1] != 'x': + return false + for i in 2..41: + let ch = a[i] + if ch in {'0'..'9'} or ch in {'a'..'f'}: + address &= ch + elif ch in {'A'..'F'}: + address &= chr(ord(ch) - ord('A') + ord('a')) + else: + return false + var hash = keccak256.digest(address) + var hexhash = toHex(hash.data, true) + for i in 0..= '0' and hexhash[i] <= '7': + check.add(address[i]) + else: + if address[i] >= '0' and address[i] <= '9': + check.add(address[i]) + else: + let ch = chr(ord(address[i]) - ord('a') + ord('A')) + check.add(ch) + result = (check == a) + +proc toCanonicalAddress*(pubkey: PublicKey): array[20, byte] = + ## Convert public key to canonical address. + var hash = keccak256.digest(pubkey.getRaw()) + copyMem(addr result[0], addr hash.data[12], 20) + +proc `$`*(pubkey: PublicKey): string = + ## Convert public key to hexadecimal string representation. + result = toHex(pubkey.getRaw(), true) + +proc `$`*(sig: Signature): string = + ## Convert signature to hexadecimal string representation. + result = toHex(sig.getRaw(), true) + +proc `$`*(seckey: PrivateKey): string = + ## Convert private key to hexadecimal string representation + result = toHex(seckey.data, true) diff --git a/eth/keys/libsecp256k1.nim b/eth/keys/libsecp256k1.nim new file mode 100644 index 0000000..9b57a77 --- /dev/null +++ b/eth/keys/libsecp256k1.nim @@ -0,0 +1,321 @@ +# +# Nim Ethereum Keys (nim-eth-keys) +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# - Apache License, version 2.0, (LICENSE-APACHEv2) +# - MIT license (LICENSE-MIT) +# + +## This is libsecp256k1 backend. + +import secp256k1, nimcrypto/sysrand, nimcrypto/utils + +const + KeyLength* = 256 div 8 + RawSignatureSize* = KeyLength * 2 + 1 + RawPublicKeySize* = KeyLength * 2 + InvalidPrivateKey = "Invalid private key!" + InvalidPublicKey = "Invalid public key!" + InvalidSignature = "Invalid signature!" + VerificationFailed = "Signature verification has been failed!" + MessageSizeError = "Size of message to sign must be KeyLength bytes!" + +type + PublicKey* = secp256k1_pubkey + ## Representation of public key + + PrivateKey* = object + ## Representation of secret key + data*: array[KeyLength, byte] + + SharedSecret* = object + ## Representation of ECDH shared secret + data*: array[KeyLength, byte] + + KeyPair* = object + ## Representation of private/public keys pair + seckey*: PrivateKey + pubkey*: PublicKey + + Signature* = secp256k1_ecdsa_recoverable_signature + ## Representation of signature + + Secp256k1Exception* = object of Exception + ## Exceptions generated by `libsecp256k1` + + EthKeysContext = ref object + context: ptr secp256k1_context + error: string + +var ekContext {.threadvar.}: EthKeysContext + ## Thread local variable which holds current context + +## +## Private procedures interface +## + +proc illegalCallback(message: cstring; data: pointer) {.cdecl.} = + let ctx = cast[EthKeysContext](data) + ctx.error = $message + +proc errorCallback(message: cstring, data: pointer) {.cdecl.} = + let ctx = cast[EthKeysContext](data) + ctx.error = $message + +proc shutdownLibsecp256k1(ekContext: EthKeysContext) = + # TODO: use destructor when finalizer are deprecated for destructors + if not isNil(ekContext.context): + secp256k1_context_destroy(ekContext.context) + +proc newEthKeysContext(): EthKeysContext = + ## Create new `EthKeysContext`. + new(result, shutdownLibsecp256k1) + let flags = cuint(SECP256K1_CONTEXT_VERIFY or SECP256K1_CONTEXT_SIGN) + result.context = secp256k1_context_create(flags) + secp256k1_context_set_illegal_callback(result.context, illegalCallback, + cast[pointer](result)) + secp256k1_context_set_error_callback(result.context, errorCallback, + cast[pointer](result)) + result.error = "" + +proc getSecpContext(): ptr secp256k1_context = + ## Get current `secp256k1_context` + if isNil(ekContext): + ekContext = newEthKeysContext() + result = ekContext.context + +proc getContext(): EthKeysContext = + ## Get current `EccContext` + if isNil(ekContext): + ekContext = newEthKeysContext() + result = ekContext + +template raiseSecp256k1Error() = + ## Raises `libsecp256k1` error as exception + let mctx = getContext() + if len(mctx.error) > 0: + var msg = mctx.error + mctx.error.setLen(0) + raise newException(Secp256k1Exception, msg) + +proc libsecp256k1ErrorMsg(): string = + let mctx = getContext() + result = mctx.error + +proc setErrorMsg(m: string) = + let mctx = getContext() + mctx.error = m + +## +## Public procedures interface +## + +proc newPrivateKey*(): PrivateKey = + ## Generates new private key. + let ctx = getSecpContext() + while true: + if randomBytes(result.data) == KeyLength: + if secp256k1_ec_seckey_verify(ctx, cast[ptr cuchar](addr result)) == 1: + break + +proc getPublicKey*(seckey: PrivateKey): PublicKey = + ## Return public key for private key `seckey`. + let ctx = getSecpContext() + if secp256k1_ec_pubkey_create(ctx, addr result, + cast[ptr cuchar](unsafeAddr seckey)) != 1: + raiseSecp256k1Error() + +proc newKeyPair*(): KeyPair = + ## Generates new private and public key. + result.seckey = newPrivateKey() + result.pubkey = result.seckey.getPublicKey() + +proc initPrivateKey*(hexstr: string): PrivateKey = + ## Create new private key from hexadecimal string representation. + let ctx = getSecpContext() + var o = fromHex(stripSpaces(hexstr)) + if len(o) < KeyLength: + raise newException(EthKeysException, InvalidPrivateKey) + copyMem(addr result, addr o[0], KeyLength) + if secp256k1_ec_seckey_verify(ctx, cast[ptr cuchar](addr result)) != 1: + raise newException(EthKeysException, InvalidPrivateKey) + +proc initPrivateKey*(data: openarray[byte]): PrivateKey = + ## Create new private key from binary data blob. + let ctx = getSecpContext() + if len(data) < KeyLength: + raise newException(EthKeysException, InvalidPrivateKey) + copyMem(addr result, unsafeAddr data[0], KeyLength) + if secp256k1_ec_seckey_verify(ctx, cast[ptr cuchar](addr result)) != 1: + raise newException(EthKeysException, InvalidPrivateKey) + +proc recoverPublicKey*(data: openarray[byte], + pubkey: var PublicKey): EthKeysStatus = + ## Unserialize public key from `data`. + let ctx = getSecpContext() + let length = len(data) + if length < RawPublicKeySize: + setErrorMsg(InvalidPublicKey) + return(EthKeysStatus.Error) + var rawkey: array[RawPublicKeySize + 1, byte] + rawkey[0] = 0x04'u8 # mark key with UNCOMPRESSED flag + copyMem(addr rawkey[1], unsafeAddr data[0], RawPublicKeySize) + if secp256k1_ec_pubkey_parse(ctx, addr pubkey, + cast[ptr cuchar](addr rawkey), + RawPublicKeySize + 1) != 1: + return(EthKeysStatus.Error) + result = EthKeysStatus.Success + +proc recoverSignature*(data: openarray[byte], + signature: var Signature): EthKeysStatus = + ## Unserialize signature from `data`. + let ctx = getSecpContext() + let length = len(data) + if length < RawSignatureSize: + setErrorMsg(InvalidSignature) + return(EthKeysStatus.Error) + var recid = cint(data[KeyLength * 2]) + if secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, addr signature, + cast[ptr cuchar](unsafeAddr data[0]), + recid) != 1: + return(EthKeysStatus.Error) + result = EthKeysStatus.Success + +proc initPublicKey*(hexstr: string): PublicKey = + ## Create new public key from hexadecimal string representation. + var o = fromHex(stripSpaces(hexstr)) + if len(o) < RawPublicKeySize: + raise newException(EthKeysException, InvalidPublicKey) + if recoverPublicKey(o, result) != EthKeysStatus.Success: + raise newException(EthKeysException, InvalidPublicKey) + +proc initPublicKey*(data: openarray[byte]): PublicKey = + ## Create new public key from binary data blob. + if recoverPublicKey(data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, InvalidPublicKey) + +proc initSignature*(hexstr: string): Signature = + ## Create new signature from hexadecimal string representation. + var o = fromHex(stripSpaces(hexstr)) + if recoverSignature(o, result) != EthKeysStatus.Success: + raise newException(EthKeysException, libsecp256k1ErrorMsg()) + +proc initSignature*(data: openarray[byte]): Signature = + ## Create new signature from 'data'. + if recoverSignature(data, result) != EthKeysStatus.Success: + raise newException(EthKeysException, libsecp256k1ErrorMsg()) + +proc ecdhAgree*(seckey: PrivateKey, pubkey: PublicKey, + secret: var SharedSecret): EthKeysStatus = + ## Calculate ECDH shared secret. + var res: array[KeyLength + 1, byte] + let ctx = getSecpContext() + if secp256k1_ecdh_raw(ctx, cast[ptr cuchar](addr res), + unsafeAddr pubkey, + cast[ptr cuchar](unsafeAddr seckey)) != 1: + return(EthKeysStatus.Error) + copyMem(addr secret, addr res[1], KeyLength) + return(EthKeysStatus.Success) + +proc getRaw*(pubkey: PublicKey): array[RawPublicKeySize, byte] {.noinit.} = + ## Converts public key `pubkey` to serialized form. + var key: array[RawPublicKeySize + 1, byte] + var length = csize(sizeof(key)) + let ctx = getSecpContext() + if secp256k1_ec_pubkey_serialize(ctx, cast[ptr cuchar](addr key), + addr length, unsafeAddr pubkey, + SECP256K1_EC_UNCOMPRESSED) != 1: + raiseSecp256k1Error() + assert(length == RawPublicKeySize + 1) + assert(key[0] == 0x04'u8) + copyMem(addr result[0], addr key[1], RawPublicKeySize) + +proc toRaw*(pubkey: PublicKey, data: var openarray[byte]) = + ## Converts public key `pubkey` to serialized form and store it in `data`. + var key: array[RawPublicKeySize + 1, byte] + assert(len(data) >= RawPublicKeySize) + var length = csize(sizeof(key)) + let ctx = getSecpContext() + if secp256k1_ec_pubkey_serialize(ctx, cast[ptr cuchar](addr key), + addr length, unsafeAddr pubkey, + SECP256K1_EC_UNCOMPRESSED) != 1: + raiseSecp256k1Error() + assert(length == RawPublicKeySize + 1) + assert(key[0] == 0x04'u8) + copyMem(addr data[0], addr key[1], RawPublicKeySize) + +proc getRaw*(s: Signature): array[RawSignatureSize, byte] {.noinit.} = + ## Converts signature `s` to serialized form. + let ctx = getSecpContext() + var recid = cint(0) + if secp256k1_ecdsa_recoverable_signature_serialize_compact( + ctx, cast[ptr cuchar](unsafeAddr result), addr recid, unsafeAddr s) != 1: + raiseSecp256k1Error() + result[64] = uint8(recid) + +proc toRaw*(s: Signature, data: var openarray[byte]) = + ## Converts signature `s` to serialized form and store it in `data`. + let ctx = getSecpContext() + var recid = cint(0) + assert(len(data) >= RawSignatureSize) + if secp256k1_ecdsa_recoverable_signature_serialize_compact( + ctx, cast[ptr cuchar](addr data[0]), addr recid, unsafeAddr s) != 1: + raiseSecp256k1Error() + data[64] = uint8(recid) + +proc recoverSignatureKey*(data: openarray[byte], + msg: openarray[byte], + pubkey: var PublicKey): EthKeysStatus = + ## Perform check on digitally signed `data` using original message `msg` and + ## recover public key to `pubkey` on success. + let ctx = getSecpContext() + let length = len(data) + if len(msg) < KeyLength: + setErrorMsg(MessageSizeError) + return(EthKeysStatus.Error) + if length < RawSignatureSize: + setErrorMsg(InvalidSignature) + return(EthKeysStatus.Error) + var recid = cint(data[KeyLength * 2]) + var s: secp256k1_ecdsa_recoverable_signature + if secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, addr s, + cast[ptr cuchar](unsafeAddr data[0]), + recid) != 1: + return(EthKeysStatus.Error) + if secp256k1_ecdsa_recover(ctx, addr pubkey, addr s, + cast[ptr cuchar](msg)) != 1: + setErrorMsg(VerificationFailed) + return(EthKeysStatus.Error) + result = EthKeysStatus.Success + +proc recoverSignatureKey*(signature: Signature, + msg: openarray[byte], + pubkey: var PublicKey): EthKeysStatus = + ## Perform check of `signature` using original message `msg` and + ## recover public key to `pubkey` on success. + let ctx = getSecpContext() + if len(msg) < KeyLength: + setErrorMsg(MessageSizeError) + return(EthKeysStatus.Error) + if secp256k1_ecdsa_recover(ctx, addr pubkey, unsafeAddr signature, + cast[ptr cuchar](msg)) != 1: + setErrorMsg(VerificationFailed) + return(EthKeysStatus.Error) + result = EthKeysStatus.Success + +proc signRawMessage*(data: openarray[byte], seckey: PrivateKey, + signature: var Signature): EthKeysStatus = + ## Sign message `data` of `KeyLength` size using private key `seckey` and + ## store result into `signature`. + let ctx = getSecpContext() + let length = len(data) + if length != KeyLength: + setErrorMsg(MessageSizeError) + return(EthKeysStatus.Error) + if secp256k1_ecdsa_sign_recoverable(ctx, addr signature, + cast[ptr cuchar](unsafeAddr data[0]), + cast[ptr cuchar](unsafeAddr seckey), + nil, nil) != 1: + return(EthKeysStatus.Error) + return(EthKeysStatus.Success) + diff --git a/tests/keys/config.nim b/tests/keys/config.nim new file mode 100644 index 0000000..a60e58f --- /dev/null +++ b/tests/keys/config.nim @@ -0,0 +1,79 @@ +# Nim Eth-keys +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +# This is a sample of signatures generated with a known-good implementation of the ECDSA +# algorithm, which we use to test our ECC backends. If necessary, it can be generated from scratch +# with the following code: +# +# """python +# from devp2p import crypto +# from eth_utils import encode_hex +# msg = b'message' +# msghash = crypto.sha3(b'message') +# for secret in ['alice', 'bob', 'eve']: +# print("'{}': dict(".format(secret)) +# privkey = crypto.mk_privkey(secret) +# pubkey = crypto.privtopub(privkey) +# print(" privkey='{}',".format(encode_hex(privkey))) +# print(" pubkey='{}',".format(encode_hex(crypto.privtopub(privkey)))) +# ecc = crypto.ECCx(raw_privkey=privkey) +# sig = ecc.sign(msghash) +# print(" sig='{}',".format(encode_hex(sig))) +# print(" raw_sig='{}')".format(crypto._decode_sig(sig))) +# assert crypto.ecdsa_recover(msghash, sig) == pubkey +# """ + +import nimcrypto + +type + testKeySig* = object + privkey*: string + pubkey*: string + raw_sig*: tuple[v: int, r, s: string] + serialized_sig*: string + +let + MSG* = "message" + MSGHASH* = keccak256.digest(MSG) + +# Conversion done through https://www.mobilefish.com/services/big_number/big_number.php + +let + alice* = testKeySig( + privkey: "9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501", + pubkey: "5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca", + raw_sig: ( + v: 1, + r: "B20E2EA5D3CBAA83C1E0372F110CF12535648613B479B64C1A8C1A20C5021F38", # Decimal "80536744857756143861726945576089915884233437828013729338039544043241440681784", + s: "0434D07EC5795E3F789794351658E80B7FAF47A46328F41E019D7B853745CDFD" # Decimal "1902566422691403459035240420865094128779958320521066670269403689808757640701" + ), + serialized_sig: "b20e2ea5d3cbaa83c1e0372f110cf12535648613b479b64c1a8c1a20c5021f380434d07ec5795e3f789794351658e80b7faf47a46328f41e019d7b853745cdfd01" + ) + + bob* = testKeySig( + privkey: "38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2", + pubkey: "347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570", + raw_sig: ( + v: 1, + r: "5C48EA4F0F2257FA23BD25E6FCB0B75BBE2FF9BBDA0167118DAB2BB6E31BA76E", # Decimal "41741612198399299636429810387160790514780876799439767175315078161978521003886", + s: "691DBDAF2A231FC9958CD8EDD99507121F8184042E075CF10F98BA88ABFF1F36" # Decimal "47545396818609319588074484786899049290652725314938191835667190243225814114102" + ), + serialized_sig: "5c48ea4f0f2257fa23bd25e6fcb0b75bbe2ff9bbda0167118dab2bb6e31ba76e691dbdaf2a231fc9958cd8edd99507121f8184042e075cf10f98ba88abff1f3601" + ) + + eve* = testKeySig( + privkey: "876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c", + pubkey: "c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0", + raw_sig: ( + v: 0, + r: "BABEEFC5082D3CA2E0BC80532AB38F9CFB196FB9977401B2F6A98061F15ED603", # Decimal "84467545608142925331782333363288012579669270632210954476013542647119929595395", + s: "603D0AF084BF906B2CDF6CDDE8B2E1C3E51A41AF5E9ADEC7F3643B3F1AA2AADF" # Decimal "43529886636775750164425297556346136250671451061152161143648812009114516499167" + ), + serialized_sig: "babeefc5082d3ca2e0bc80532ab38f9cfb196fb9977401b2f6a98061f15ed603603d0af084bf906b2cdf6cdde8b2e1c3e51a41af5e9adec7f3643b3f1aa2aadf00" + ) diff --git a/tests/keys/disabled_test_key_and_signature_datastructures.nim b/tests/keys/disabled_test_key_and_signature_datastructures.nim new file mode 100644 index 0000000..257e1c9 --- /dev/null +++ b/tests/keys/disabled_test_key_and_signature_datastructures.nim @@ -0,0 +1,66 @@ +# Nim Eth-keys +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import eth/keys, #../src/private/conversion_bytes, + ./config + +import unittest + +suite "Test key and signature data structure": + + test "Signing from private key object (ported from official eth-keys)": + for person in [alice, bob, eve]: + let + pk = initPrivateKey(person.privkey) + signature = pk.sign_msg(MSG) + + check: verify_msg(pk.public_key, MSG, signature) + + test "Hash signing from private key object (ported from official eth-keys)": + for person in [alice, bob, eve]: + let + pk = initPrivateKey(person.privkey) + signature = pk.sign_msg(MSGHASH) + + check: verify_msg(pk.public_key, MSGHASH, signature) + + test "Recover public key from message": + for person in [alice, bob, eve]: + let + pk = initPrivateKey(person.privkey) + signature = pk.sign_msg(MSG) + + recovered_pubkey = recover_pubkey_from_msg(MSG, signature) + + check: pk.public_key == recovered_pubkey + + test "Recover public key from message hash": + for person in [alice, bob, eve]: + let + pk = initPrivateKey(person.privkey) + signature = pk.sign_msg(MSGHASH) + + recovered_pubkey = recover_pubkey_from_msg(MSGHASH, signature) + + check: pk.public_key == recovered_pubkey + + test "Signature serialization and deserialization": + for person in [alice, bob, eve]: + let + pk = initPrivateKey(person.privkey) + signature = pk.sign_msg(MSG) + deserializedSignature = parseSignature(hexToSeqByteBE(person.serialized_sig)) + + var serialized_sig: array[65, byte] + signature.serialize(serialized_sig) + + check: + signature == deserializedSignature + serialized_sig.toHex() == person.serialized_sig + $signature == person.serialized_sig diff --git a/tests/keys/disabled_test_private_public_key_consistency.nim b/tests/keys/disabled_test_private_public_key_consistency.nim new file mode 100644 index 0000000..4cfcca1 --- /dev/null +++ b/tests/keys/disabled_test_private_public_key_consistency.nim @@ -0,0 +1,22 @@ +# Nim Eth-keys +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +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 + + check: computed_pubkey == person.pubkey diff --git a/tests/keys/test_keys.nim b/tests/keys/test_keys.nim new file mode 100644 index 0000000..3e35f9a --- /dev/null +++ b/tests/keys/test_keys.nim @@ -0,0 +1,267 @@ +# +# Ethereum P2P +# (c) Copyright 2018 +# Status Research & Development GmbH +# +# See the file "LICENSE", included in this +# distribution, for details about the copyright. +# + +import unittest, strutils +import eth/keys +import nimcrypto/hash, nimcrypto/keccak, nimcrypto/utils + +proc compare(x: openarray[byte], y: openarray[byte]): bool = + result = len(x) == len(y) + if result: + for i in 0..(len(x) - 1): + if x[i] != y[i]: + result = false + break +const + pkbytes = "58d23b55bc9cdce1f18c2500f40ff4ab7245df9a89505e9b1fa4851f623d241d" + message = "message" + address = "dc544d1aa88ff8bbd2f2aec754b1f1e99e1812fd" + + alice = [ + "9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501", + """5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b42 + 6078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca""", + """b20e2ea5d3cbaa83c1e0372f110cf12535648613b479b64c1a8c1a20c5021f38 + 0434d07ec5795e3f789794351658e80b7faf47a46328f41e019d7b853745cdfd01""" + ] + bob = [ + "38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2", + """347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937 + d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570""", + """5c48ea4f0f2257fa23bd25e6fcb0b75bbe2ff9bbda0167118dab2bb6e31ba76e + 691dbdaf2a231fc9958cd8edd99507121f8184042e075cf10f98ba88abff1f3601""" + ] + eve = [ + "876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c", + """c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91 + e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0""", + """babeefc5082d3ca2e0bc80532ab38f9cfb196fb9977401b2f6a98061f15ed603 + 603d0af084bf906b2cdf6cdde8b2e1c3e51a41af5e9adec7f3643b3f1aa2aadf00""" + ] + +suite "ECC/ECDSA/ECDHE tests suite": + test "Known private to known public keys (test data from Ethereum eth-keys)": + for person in [alice, bob, eve]: + let privkey = initPrivateKey(person[0]) + var pubkeyHex = $privkey.getPublicKey() + check: + pubkeyHex == stripSpaces(person[1]) + + test "Recover public key from message": + for person in [alice, bob, eve]: + let privkey = initPrivateKey(person[0]) + let signature = privkey.signMessage(message) + let recoveredKey = signature.recoverKeyFromSignature(message) + check: + $privkey.getPublicKey() == $recoveredKey + + test "Signature serialization and deserialization": + for person in [alice, bob, eve]: + let privkey = initPrivateKey(person[0]) + let signature = privkey.signMessage(message) + let expectSignature = initSignature(person[2]) + check: + $signature == $expectSignature + + test "test_signing_from_private_key_obj": + var s = initPrivateKey(pkbytes) + var signature = s.signMessage(message) + var mhash = keccak256.digest(message) + check verifyMessage(signature.data, mhash) == true + + test "test_recover_from_signature_obj": + var s = initPrivateKey(pkbytes) + var mhash = keccak256.digest(message) + var signature = s.signMessage(message) + var p = recoverKeyFromSignature(signature, mhash) + check: + s.getPublicKey() == p + + test "test_to_address_from_public_key": + var s = initPrivateKey(pkbytes) + var chk = s.getPublicKey().toAddress() + var expect = "0x" & address + check chk == expect + + test "test_to_canonical_address_from_public_key": + var s = initPrivateKey(pkbytes) + var chk = s.getPublicKey().toCanonicalAddress() + var expect = fromHex(stripSpaces(address)) + check compare(chk, expect) == true + + test "test_to_checksum_address_from_public_key": + var s = initPrivateKey(pkbytes) + var chk = s.getPublicKey().toChecksumAddress() + var expect = "0x" & address + check: + chk.toLowerAscii() == expect + + test "EIP-55 checksum addresses test cases": + var checks = [ + "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "0x52908400098527886E0F7030069857D2E4169EE7", + "0x8617E340B3D01FA5F11F306F4090FD50E238070D", + "0xde709f2102306220921060314715629080e2fb77", + "0x27b1fdb04752bbc536007a920d24acb045561c26", + "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", + "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", + "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + ] + var badchecks = [ + "", + "0xXB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d35X", + "0XfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "XXfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "0xfB6916095" + ] + for item in checks: + check validateChecksumAddress(item) == true + for item in badchecks: + check validateChecksumAddress(item) == false + + test "EIP-55 100 addresses": + for i in 1..100: + var kp = newKeyPair() + var chaddress = kp.pubkey.toChecksumAddress() + var noaddress = kp.pubkey.toAddress() + if noaddress != chaddress: + check validateChecksumAddress(noaddress) == false + check validateChecksumAddress(chaddress) == true + + test "ECDHE/py-evm test_ecies.py#L19": + # ECDHE test vectors + # Copied from + # https://github.com/ethereum/py-evm/blob/master/tests/p2p/test_ecies.py#L19 + const privateKeys = [ + "332143e9629eedff7d142d741f896258f5a1bfab54dab2121d3ec5000093d74b", + "7ebbc6a8358bc76dd73ebc557056702c8cfc34e5cfcd90eb83af0347575fd2ad" + ] + const publicKeys = [ + """f0d2b97981bd0d415a843b5dfe8ab77a30300daab3658c578f2340308a2da1a07 + f0821367332598b6aa4e180a41e92f4ebbae3518da847f0b1c0bbfe20bcf4e1""", + """83ede0f19c3c98649265956a4193677b14c338a22de2086a08d84e4446fe37e4e + 233478259ec90dbeef52f4f6c890f8c38660ec7b61b9d439b8a6d1c323dc025""" + ] + const sharedSecrets = [ + "ee1418607c2fcfb57fda40380e885a707f49000a5dda056d828b7d9bd1f29a08", + "167ccc13ac5e8a26b131c3446030c60fbfac6aa8e31149d0869f93626a4cdf62" + ] + var secret: SharedSecret + for i in 0..1: + var s = privateKeys[i].initPrivateKey() + var p = publicKeys[i].initPublicKey() + let expect = fromHex(stripSpaces(sharedSecrets[i])) + check: + ecdhAgree(s, p, secret) == EthKeysStatus.Success + compare(expect, secret.data) == true + + test "ECDHE/cpp-ethereum crypto.cpp#L394": + # ECDHE test vectors + # Copied from https://github.com/ethereum/cpp-ethereum/blob/develop/test/unittests/libdevcrypto/crypto.cpp#L394 + var expectm = """ + 8ac7e464348b85d9fdfc0a81f2fdc0bbbb8ee5fb3840de6ed60ad9372e718977""" + var secret: SharedSecret + var s = initPrivateKey(keccak256.digest("ecdhAgree").data) + var p = s.getPublicKey() + let expect = fromHex(stripSpaces(expectm)) + check: + ecdhAgree(s, p, secret) == EthKeysStatus.Success + compare(expect, secret.data) == true + + test "ECDHE/cpp-ethereum rlpx.cpp#L425": + # ECDHE test vectors + # Copied from https://github.com/ethereum/cpp-ethereum/blob/2409d7ec7d34d5ff5770463b87eb87f758e621fe/test/unittests/libp2p/rlpx.cpp#L425 + var s0 = """ + 332143e9629eedff7d142d741f896258f5a1bfab54dab2121d3ec5000093d74b""" + var p0 = """ + f0d2b97981bd0d415a843b5dfe8ab77a30300daab3658c578f2340308a2da1a0 + 7f0821367332598b6aa4e180a41e92f4ebbae3518da847f0b1c0bbfe20bcf4e1""" + var e0 = """ + ee1418607c2fcfb57fda40380e885a707f49000a5dda056d828b7d9bd1f29a08""" + var secret: SharedSecret + var s = initPrivateKey(s0) + var p = initPublicKey(p0) + let expect = fromHex(stripSpaces(e0)) + check: + ecdhAgree(s, p, secret) == Success + compare(expect, secret.data) == true + + test "ECDSA/cpp-ethereum crypto.cpp#L132": + # ECDSA test vectors + # Copied from https://github.com/ethereum/cpp-ethereum/blob/develop/test/unittests/libdevcrypto/crypto.cpp#L132 + var signature = """ + b826808a8c41e00b7c5d71f211f005a84a7b97949d5e765831e1da4e34c9b8295d + 2a622eee50f25af78241c1cb7cfff11bcf2a13fe65dee1e3b86fd79a4e3ed000""" + var pubkey = """ + e40930c838d6cca526795596e368d16083f0672f4ab61788277abfa23c3740e1cc + 84453b0b24f49086feba0bd978bb4446bae8dff1e79fcc1e9cf482ec2d07c3""" + var check1 = fromHex(stripSpaces(signature)) + var check2 = fromHex(stripSpaces(pubkey)) + var sig: Signature + var key: PublicKey + var s = initPrivateKey(keccak256.digest("sec").data) + var m = keccak256.digest("msg").data + check signRawMessage(m, s, sig) == Success + var sersig = sig.getRaw() + check recoverSignatureKey(sersig, m, key) == Success + var serkey = key.getRaw() + check: + compare(sersig, check1) == true + compare(serkey, check2) == true + + test "ECDSA/100 signatures": + # signature test + var rkey: PublicKey + var sig: Signature + for i in 1..100: + var m = newPrivateKey().data + var s = newPrivateKey() + var key = s.getPublicKey() + check signRawMessage(m, s, sig) == Success + var sersig = sig.getRaw() + check: + recoverSignatureKey(sersig, m, rkey) == Success + key == rkey + + test "KEYS/100 create/recovery keys": + # key create/recovery test + var rkey: PublicKey + for i in 1..100: + var s = newPrivateKey() + var key = s.getPublicKey() + check: + recoverPublicKey(key.getRaw(), rkey) == Success + key == rkey + + test "ECDHE/100 shared secrets": + # ECDHE shared secret test + var secret1, secret2: SharedSecret + for i in 1..100: + var aliceSecret = newPrivateKey() + var alicePublic = aliceSecret.getPublicKey() + var bobSecret = newPrivateKey() + var bobPublic = bobSecret.getPublicKey() + check: + ecdhAgree(aliceSecret, bobPublic, secret1) == Success + ecdhAgree(bobSecret, alicePublic, secret2) == Success + secret1 == secret2 + + test "isZeroKey() checks": + var seckey1: PrivateKey + var pubkey1: PublicKey + var seckey2 = newPrivateKey() + var pubkey2 = seckey2.getPublicKey() + + check: + seckey1.isZeroKey() == true + pubkey1.isZeroKey() == true + seckey2.isZeroKey() == false + pubkey2.isZeroKey() == false