nim-eth-p2p/ethp2p/ecc.nim

447 lines
16 KiB
Nim

#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# See the file "LICENSE", included in this
# distribution, for details about the copyright.
#
## This module implements `libsecp256k1` ECC/ECDH functions
import secp256k1, hexdump, nimcrypto/sysrand, nimcrypto/utils
const
KeyLength* = 32
PublicKeyLength* = 64
SignatureLength* = 65
type
EccContext* = ref object of RootRef
context*: ptr secp256k1_context
error*: string
EccStatus* = enum
Success, ## Operation was successful
Error ## Operation failed
PublicKey* = secp256k1_pubkey
## Representation of public key
PrivateKey* = array[KeyLength, byte]
## Representation of secret key
SharedSecret* = array[KeyLength, byte]
## Representation of ECDH shared secret
Nonce* = array[KeyLength, byte]
## Representation of nonce
RawPublickey* = object
## Representation of serialized public key
header*: byte
data*: array[KeyLength * 2, byte]
KeyPair* = object
## Representation of private/public keys pair
seckey*: PrivateKey
pubkey*: PublicKey
Signature* = secp256k1_ecdsa_recoverable_signature
## Representation of signature
RawSignature* = object
## Representation of serialized signature
data*: array[KeyLength * 2 + 1, byte]
Secp256k1Exception* = object of Exception
## Exceptions generated by `libsecp256k1`
EccException* = object of Exception
## Exception generated by this module
var eccContext* {.threadvar.}: EccContext
## Thread local variable which holds current context
proc illegalCallback(message: cstring; data: pointer) {.cdecl.} =
let ctx = cast[EccContext](data)
ctx.error = $message
proc errorCallback(message: cstring, data: pointer) {.cdecl.} =
let ctx = cast[EccContext](data)
ctx.error = $message
proc newEccContext*(): EccContext =
## Create new `EccContext`.
result = new EccContext
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(eccContext):
eccContext = newEccContext()
result = eccContext.context
proc getEccContext*(): EccContext =
## Get current `EccContext`
if isNil(eccContext):
eccContext = newEccContext()
result = eccContext
template raiseSecp256k1Error*() =
## Raises `libsecp256k1` error as exception
let mctx = getEccContext()
if len(mctx.error) > 0:
var msg = mctx.error
mctx.error.setLen(0)
raise newException(Secp256k1Exception, msg)
proc eccErrorMsg*(): string =
let mctx = getEccContext()
result = mctx.error
proc setErrorMsg*(m: string) =
let mctx = getEccContext()
mctx.error = m
proc getRaw*(pubkey: PublicKey): RawPublickey =
## Converts public key `pubkey` to serialized form of `secp256k1_pubkey`.
var length = csize(sizeof(RawPublickey))
let ctx = getSecpContext()
if secp256k1_ec_pubkey_serialize(ctx, cast[ptr cuchar](addr result),
addr length, unsafeAddr pubkey,
SECP256K1_EC_UNCOMPRESSED) != 1:
raiseSecp256k1Error()
if length != 65:
raise newException(EccException, "Invalid public key length!")
if result.header != 0x04'u8:
raise newException(EccException, "Invalid public key header!")
proc getRaw*(s: Signature): RawSignature =
## 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.data[64] = uint8(recid)
proc signMessage*(seckey: PrivateKey, data: ptr byte, length: int,
sig: var Signature): EccStatus =
## Sign message pointed by `data` with size `length` and save signature to
## `sig`.
let ctx = getSecpContext()
if secp256k1_ecdsa_sign_recoverable(ctx, addr sig,
cast[ptr cuchar](data),
cast[ptr cuchar](unsafeAddr seckey[0]),
nil, nil) != 1:
return(Error)
return(Success)
proc signMessage*[T](seckey: PrivateKey, data: openarray[T],
sig: var Signature, ostart: int = 0,
ofinish: int = -1): EccStatus =
## Sign message ``data``[`soffset`..`eoffset`] and store result into `sig`.
let so = ostart
let eo = if ofinish == -1: (len(data) - 1) else: ofinish
let length = (eo - so + 1) * sizeof(T)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo >= len(data):
setErrorMsg("Index is out of bounds!")
return(Error)
if len(data) < KeyLength or length < KeyLength:
setErrorMsg("There no reason to sign this message!")
return(Error)
result = signMessage(seckey, cast[ptr byte](unsafeAddr data[so]),
length, sig)
proc recoverSignatureKey*(data: ptr byte, length: int, message: ptr byte,
pubkey: var PublicKey): EccStatus =
## Check signature and return public key from `data` with size `length` and
## `message`.
let ctx = getSecpContext()
var s: secp256k1_ecdsa_recoverable_signature
if length >= 65:
var recid = cint(cast[ptr UncheckedArray[byte]](data)[KeyLength * 2])
if secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, addr s,
cast[ptr cuchar](data),
recid) != 1:
return(Error)
if secp256k1_ecdsa_recover(ctx, addr pubkey, addr s,
cast[ptr cuchar](message)) != 1:
setErrorMsg("Message signature verification failed!")
return(Error)
return(Success)
else:
setErrorMsg("Incorrect signature size")
return(Error)
proc recoverSignatureKey*[A, B](data: openarray[A],
message: openarray[B],
pubkey: var PublicKey,
ostart: int = 0,
ofinish: int = -1): EccStatus =
## Check signature in ``data``[`soffset`..`eoffset`] and recover public key
## from signature to ``pubkey`` using message `message`.
if len(message) == 0:
setErrorMsg("Message could not be empty!")
return(Error)
let so = ostart
let eo = if ofinish == -1: (len(data) - 1) else: ofinish
let length = (eo - so + 1) * sizeof(A)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo > len(data):
setErrorMsg("Index is out of bounds!")
return(Error)
if length < sizeof(RawSignature) or len(data) < sizeof(RawSignature):
setErrorMsg("Invalid signature size!")
return(Error)
result = recoverSignatureKey(cast[ptr byte](unsafeAddr data[so]), length,
cast[ptr byte](unsafeAddr message[0]), pubkey)
proc ecdhAgree*(seckey: PrivateKey, pubkey: PublicKey,
secret: var SharedSecret): EccStatus =
## 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(Error)
copyMem(addr secret[0], addr res[1], KeyLength)
return(Success)
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[0])) != 1:
raiseSecp256k1Error()
proc recoverPublicKey*(data: ptr byte, length: int,
pubkey: var PublicKey): EccStatus =
## Unserialize public key from `data` pointer and size `length` and'
## set `pubkey`.
let ctx = getSecpContext()
if length < sizeof(PublicKey):
setErrorMsg("Invalid public key!")
return(Error)
var rawkey: RawPublickey
rawkey.header = 0x04 # mark key with COMPRESSED flag
copyMem(addr rawkey.data[0], data, len(rawkey.data))
if secp256k1_ec_pubkey_parse(ctx, addr pubkey,
cast[ptr cuchar](addr rawkey),
sizeof(RawPublickey)) != 1:
return(Error)
return(Success)
proc recoverPublicKey*[T](data: openarray[T], pubkey: var PublicKey,
ostart: int = 0, ofinish: int = -1, ): EccStatus =
## Unserialize public key from openarray[T] `data`, from position `ostart` to
## position `ofinish` and save it to `pubkey`.
let so = ostart
let eo = if ofinish == -1: (len(data) - 1) else: ofinish
let length = (eo - so + 1) * sizeof(T)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo > len(data):
setErrorMsg("Index is out of bounds!")
return(Error)
if length < sizeof(PublicKey) or len(data) < sizeof(PublicKey):
setErrorMsg("Invalid public key size!")
return(Error)
result = recoverPublicKey(cast[ptr byte](unsafeAddr data[so]), length,
pubkey)
proc newPrivateKey*(): PrivateKey =
## Generates new secret key.
let ctx = getSecpContext()
while true:
if randomBytes(addr result[0], KeyLength) == KeyLength:
if secp256k1_ec_seckey_verify(ctx, cast[ptr cuchar](addr result[0])) == 1:
break
proc newKeyPair*(): KeyPair =
## Generates new private and public key.
result.seckey = newPrivateKey()
result.pubkey = result.seckey.getPublicKey()
proc getPrivateKey*(hexstr: string): PrivateKey =
## Set secret key from hexadecimal string representation.
let ctx = getSecpContext()
var o = fromHex(stripSpaces(hexstr))
if len(o) < KeyLength:
raise newException(EccException, "Invalid private key!")
copyMem(addr result[0], unsafeAddr o[0], KeyLength)
if secp256k1_ec_seckey_verify(ctx, cast[ptr cuchar](addr result[0])) != 1:
raise newException(EccException, "Invalid private key!")
proc getPublicKey*(hexstr: string): PublicKey =
## Set public key from hexadecimal string representation.
var o = fromHex(stripSpaces(hexstr))
if recoverPublicKey(o, result) != Success:
raise newException(EccException, "Invalid public key!")
proc dump*(s: openarray[byte], c: string = ""): string =
## Return hexadecimal dump of array `s`.
result = if len(c) > 0: c & "=>\n" else: ""
if len(s) > 0:
result &= dumpHex(unsafeAddr s[0], len(s))
else:
result &= "[]"
proc dump*(s: PublicKey, c: string = ""): string =
## Return hexadecimal dump of public key `s`.
result = if len(c) > 0: c & "=>\n" else: ""
result &= dumpHex(unsafeAddr s.data[0], sizeof(secp256k1_pubkey))
proc dump*(s: RawSignature, c: string = ""): string =
## Return hexadecimal dump of serialized signature `s`.
result = if len(c) > 0: c & "=>\n" else: ""
result &= dumpHex(unsafeAddr s.data[0], sizeof(RawSignature))
proc dump*(s: RawPublickey, c: string = ""): string =
## Return hexadecimal dump of serialized public key `s`.
result = if len(c) > 0: c & "=>\n" else: ""
result &= dumpHex(unsafeAddr s, sizeof(RawSignature))
proc dump*(s: secp256k1_ecdsa_recoverable_signature, c: string = ""): string =
## Return hexadecimal dump of signature `s`.
result = if len(c) > 0: c & "=>\n" else: ""
result &= dumpHex(unsafeAddr s.data[0],
sizeof(secp256k1_ecdsa_recoverable_signature))
proc dump*(p: pointer, s: int, c: string = ""): string =
## Return hexadecimal dump of memory blob `p` and size `s`.
result = if len(c) > 0: c & "=>\n" else: ""
result &= dumpHex(p, s)
when isMainModule:
import nimcrypto/hash, nimcrypto/keccak
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
block:
# 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: array[KeyLength, byte]
for i in 0..1:
var s = privateKeys[i].getPrivateKey()
var p = publicKeys[i].getPublicKey()
doAssert(ecdhAgree(s, p, secret) == Success)
var check = fromHex(stripSpaces(sharedSecrets[i]))
doAssert(compare(check, secret))
block:
# ECDHE test vectors
# Copied from https://github.com/ethereum/cpp-ethereum/blob/develop/test/unittests/libdevcrypto/crypto.cpp#L394
var expect = """
8ac7e464348b85d9fdfc0a81f2fdc0bbbb8ee5fb3840de6ed60ad9372e718977"""
var secret: array[KeyLength, byte]
var s = keccak256.digest("ecdhAgree").data
var p = s.getPublicKey()
doAssert(ecdhAgree(s, p, secret) == Success)
var check = fromHex(stripSpaces(expect))
doAssert(compare(check, secret))
block:
# 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: array[KeyLength, byte]
var s = getPrivateKey(s0)
var p = getPublicKey(p0)
var check = fromHex(stripSpaces(e0))
doAssert(ecdhAgree(s, p, secret) == Success)
doAssert(compare(check, secret))
block:
# 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 = keccak256.digest("sec").data
var m = keccak256.digest("msg").data
doAssert(signMessage(s, m, sig) == Success)
var sersig = sig.getRaw().data
doAssert(recoverSignatureKey(sersig, m, key) == Success)
var serkey = key.getRaw().data
doAssert(compare(sersig, check1))
doAssert(compare(serkey, check2))
block:
# signature test
var rkey: PublicKey
var sig: Signature
for i in 1..100:
var m = newPrivateKey()
var s = newPrivateKey()
var key = s.getPublicKey()
doAssert(signMessage(s, m, sig) == Success)
var sersig = sig.getRaw().data
doAssert(recoverSignatureKey(sersig, m, rkey) == Success)
doAssert(key == rkey)
block:
# key create/recovery test
var rkey: PublicKey
for i in 1..100:
var s = newPrivateKey()
var key = s.getPublicKey()
doAssert(recoverPublicKey(key.getRaw().data, rkey) == Success)
doAssert(key == rkey)
block:
# 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()
doAssert(ecdhAgree(aliceSecret, bobPublic, secret1) == Success)
doAssert(ecdhAgree(bobSecret, alicePublic, secret2) == Success)
doAssert(secret1 == secret2)