mirror of https://github.com/status-im/nim-eth.git
250 lines
9.1 KiB
Nim
250 lines
9.1 KiB
Nim
|
# eth
|
||
|
# Copyright (c) 2024 Status Research & Development GmbH
|
||
|
# Licensed and distributed under either of
|
||
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
||
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
||
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||
|
|
||
|
{.push raises: [].}
|
||
|
|
||
|
# This module contains adaptations of the general secp interface to help make
|
||
|
# working with keys and signatures as they appear in Ethereum in particular:
|
||
|
#
|
||
|
# * Public keys as serialized in uncompressed format without the initial byte
|
||
|
# * Shared secrets are serialized in raw format without the initial byte
|
||
|
# * distinct types are used to avoid confusion with the "standard" secp types
|
||
|
|
||
|
import
|
||
|
std/strformat,
|
||
|
secp256k1, bearssl/rand,
|
||
|
stew/[byteutils, objects, ptrops],
|
||
|
results,
|
||
|
"."/[hashes, addresses]
|
||
|
|
||
|
from nimcrypto/utils import burnMem
|
||
|
|
||
|
export secp256k1, results, rand
|
||
|
|
||
|
const
|
||
|
KeyLength* = SkEcdhSecretSize
|
||
|
## Ecdh shared secret key length without leading byte
|
||
|
## (publicKey * privateKey).x, where length of x is 32 bytes
|
||
|
|
||
|
FullKeyLength* = KeyLength + 1
|
||
|
## Ecdh shared secret with leading byte 0x02 or 0x03
|
||
|
|
||
|
RawPublicKeySize* = SkRawPublicKeySize - 1
|
||
|
## Size of uncompressed public key without format marker (0x04)
|
||
|
|
||
|
RawSignatureSize* = SkRawRecoverableSignatureSize
|
||
|
|
||
|
RawSignatureNRSize* = SkRawSignatureSize
|
||
|
|
||
|
type
|
||
|
PrivateKey* = distinct SkSecretKey
|
||
|
|
||
|
PublicKey* = distinct SkPublicKey
|
||
|
## Public key that's serialized to raw format without 0x04 marker
|
||
|
Signature* = distinct SkRecoverableSignature
|
||
|
## Ethereum uses recoverable signatures allowing some space savings
|
||
|
SignatureNR* = distinct SkSignature
|
||
|
## ...but ENR uses non-recoverable signatures!
|
||
|
|
||
|
SharedSecretFull* = object
|
||
|
## Representation of ECDH shared secret, with leading `y` byte
|
||
|
## (`y` is 0x02 when (publicKey * privateKey).y is even or 0x03 when odd)
|
||
|
data*: array[FullKeyLength, byte]
|
||
|
|
||
|
SharedSecret* = object
|
||
|
## Representation of ECDH shared secret, without leading `y` byte
|
||
|
data*: array[KeyLength, byte]
|
||
|
|
||
|
KeyPair* = distinct SkKeyPair
|
||
|
|
||
|
template pubkey*(v: KeyPair): PublicKey = PublicKey(SkKeyPair(v).pubkey)
|
||
|
template seckey*(v: KeyPair): PrivateKey = PrivateKey(SkKeyPair(v).seckey)
|
||
|
|
||
|
proc newRng*(): ref HmacDrbgContext =
|
||
|
# You should only create one instance of the RNG per application / library
|
||
|
# Ref is used so that it can be shared between components
|
||
|
HmacDrbgContext.new()
|
||
|
|
||
|
proc random*(T: type PrivateKey, rng: var HmacDrbgContext): T =
|
||
|
let rngPtr = unsafeAddr rng # doesn't escape
|
||
|
proc callRng(data: var openArray[byte]) =
|
||
|
generate(rngPtr[], data)
|
||
|
|
||
|
T(SkSecretKey.random(callRng))
|
||
|
|
||
|
func fromRaw*(T: type PrivateKey, data: openArray[byte]): SkResult[T] =
|
||
|
SkSecretKey.fromRaw(data).mapConvert(T)
|
||
|
|
||
|
func fromHex*(T: type PrivateKey, data: string): SkResult[T] =
|
||
|
SkSecretKey.fromHex(data).mapConvert(T)
|
||
|
|
||
|
func toRaw*(seckey: PrivateKey): array[SkRawSecretKeySize, byte] =
|
||
|
SkSecretKey(seckey).toRaw()
|
||
|
|
||
|
func toPublicKey*(seckey: PrivateKey): PublicKey {.borrow.}
|
||
|
|
||
|
func fromRaw*(T: type PublicKey, data: openArray[byte]): SkResult[T] =
|
||
|
if data.len() == SkRawCompressedPublicKeySize:
|
||
|
return SkPublicKey.fromRaw(data).mapConvert(T)
|
||
|
|
||
|
if len(data) < SkRawPublicKeySize - 1:
|
||
|
return err(static(
|
||
|
&"keys: raw eth public key should be {SkRawPublicKeySize - 1} bytes"))
|
||
|
|
||
|
var d: array[SkRawPublicKeySize, byte]
|
||
|
d[0] = 0x04'u8
|
||
|
copyMem(addr d[1], unsafeAddr data[0], 64)
|
||
|
|
||
|
SkPublicKey.fromRaw(d).mapConvert(T)
|
||
|
|
||
|
func fromHex*(T: type PublicKey, data: string): SkResult[T] =
|
||
|
T.fromRaw(? seq[byte].fromHex(data))
|
||
|
|
||
|
func toRaw*(pubkey: PublicKey): array[RawPublicKeySize, byte] =
|
||
|
let tmp = SkPublicKey(pubkey).toRaw()
|
||
|
copyMem(addr result[0], unsafeAddr tmp[1], 64)
|
||
|
|
||
|
func toRawCompressed*(pubkey: PublicKey): array[33, byte] {.borrow.}
|
||
|
|
||
|
proc random*(T: type KeyPair, rng: var HmacDrbgContext): T =
|
||
|
let seckey = SkSecretKey(PrivateKey.random(rng))
|
||
|
KeyPair(SkKeyPair(
|
||
|
seckey: seckey,
|
||
|
pubkey: seckey.toPublicKey()
|
||
|
))
|
||
|
|
||
|
func toKeyPair*(seckey: PrivateKey): KeyPair =
|
||
|
KeyPair(SkKeyPair(
|
||
|
seckey: SkSecretKey(seckey), pubkey: SkSecretKey(seckey).toPublicKey()))
|
||
|
|
||
|
func fromRaw*(T: type Signature, data: openArray[byte]): SkResult[T] =
|
||
|
SkRecoverableSignature.fromRaw(data).mapConvert(T)
|
||
|
|
||
|
func fromHex*(T: type Signature, data: string): SkResult[T] =
|
||
|
T.fromRaw(? seq[byte].fromHex(data))
|
||
|
|
||
|
func toRaw*(sig: Signature): array[RawSignatureSize, byte] {.borrow.}
|
||
|
|
||
|
func fromRaw*(T: type SignatureNR, data: openArray[byte]): SkResult[T] =
|
||
|
SkSignature.fromRaw(data).mapConvert(T)
|
||
|
|
||
|
func toRaw*(sig: SignatureNR): array[RawSignatureNRSize, byte] {.borrow.}
|
||
|
|
||
|
func to*(pubkey: PublicKey, _: type Address): Address =
|
||
|
## Convert public key to canonical address.
|
||
|
let hash = keccak256(pubkey.toRaw())
|
||
|
hash.to(Address)
|
||
|
|
||
|
func toAddress*(pubkey: PublicKey): string {.deprecated.} =
|
||
|
## Convert public key to hexadecimal string address.
|
||
|
pubkey.to(Address).to0xHex()
|
||
|
|
||
|
func toChecksumAddress*(pubkey: PublicKey): string =
|
||
|
## Convert public key to checksumable mixed-case address (EIP-55).
|
||
|
pubkey.to(Address).toChecksum0xHex()
|
||
|
|
||
|
func validateChecksumAddress*(a: string): bool =
|
||
|
## Validate checksumable mixed-case address (EIP-55).
|
||
|
Address.hasValidChecksum(a)
|
||
|
|
||
|
template toCanonicalAddress*(pubkey: PublicKey): Address =
|
||
|
## Convert public key to canonical address.
|
||
|
pubkey.to(Address)
|
||
|
|
||
|
func `$`*(pubkey: PublicKey): string =
|
||
|
## Convert public key to hexadecimal string representation.
|
||
|
toHex(pubkey.toRaw())
|
||
|
|
||
|
func `$`*(sig: Signature): string =
|
||
|
## Convert signature to hexadecimal string representation.
|
||
|
toHex(sig.toRaw())
|
||
|
|
||
|
func `$`*(seckey: PrivateKey): string =
|
||
|
## Convert private key to hexadecimal string representation
|
||
|
toHex(seckey.toRaw())
|
||
|
|
||
|
func `==`*(lhs, rhs: PublicKey): bool {.borrow.}
|
||
|
func `==`*(lhs, rhs: Signature): bool {.borrow.}
|
||
|
func `==`*(lhs, rhs: SignatureNR): bool {.borrow.}
|
||
|
|
||
|
func clear*(v: var PrivateKey) {.borrow.}
|
||
|
func clear*(v: var KeyPair) =
|
||
|
v.seckey.clear()
|
||
|
|
||
|
func clear*(v: var SharedSecret) = burnMem(v.data)
|
||
|
func clear*(v: var SharedSecretFull) = burnMem(v.data)
|
||
|
|
||
|
func sign*(seckey: PrivateKey, msg: SkMessage): Signature =
|
||
|
Signature(signRecoverable(SkSecretKey(seckey), msg))
|
||
|
|
||
|
func sign*(seckey: PrivateKey, msg: openArray[byte]): Signature =
|
||
|
let hash = keccak256(msg)
|
||
|
sign(seckey, SkMessage(hash.data))
|
||
|
|
||
|
func signNR*(seckey: PrivateKey, msg: SkMessage): SignatureNR =
|
||
|
SignatureNR(sign(SkSecretKey(seckey), msg))
|
||
|
|
||
|
func signNR*(seckey: PrivateKey, msg: openArray[byte]): SignatureNR =
|
||
|
let hash = keccak256(msg)
|
||
|
signNR(seckey, SkMessage(hash.data))
|
||
|
|
||
|
func recover*(sig: Signature, msg: SkMessage): SkResult[PublicKey] =
|
||
|
recover(SkRecoverableSignature(sig), msg).mapConvert(PublicKey)
|
||
|
|
||
|
func recover*(sig: Signature, msg: openArray[byte]): SkResult[PublicKey] =
|
||
|
let hash = keccak256(msg)
|
||
|
recover(sig, SkMessage(hash.data))
|
||
|
|
||
|
func verify*(sig: SignatureNR, msg: SkMessage, key: PublicKey): bool =
|
||
|
verify(SkSignature(sig), msg, SkPublicKey(key))
|
||
|
|
||
|
func verify*(sig: SignatureNR, msg: openArray[byte], key: PublicKey): bool =
|
||
|
let hash = keccak256(msg)
|
||
|
verify(sig, SkMessage(hash.data), key)
|
||
|
|
||
|
proc ecdhSharedSecretHash(output: ptr byte, x32, y32: ptr byte, data: pointer): cint
|
||
|
{.cdecl, raises: [].} =
|
||
|
## Hash function used by `ecdhSharedSecret` below
|
||
|
# `x32` and `y32` are result of scalar multiplication of publicKey * privateKey.
|
||
|
# Both `x32` and `y32` are 32 bytes length.
|
||
|
# Take the `x32` part as ecdh shared secret.
|
||
|
|
||
|
# output length is derived from x32 length and taken from ecdh
|
||
|
# generic parameter `KeyLength`
|
||
|
copyMem(output, x32, KeyLength)
|
||
|
return 1
|
||
|
|
||
|
func ecdhSharedSecret*(seckey: PrivateKey, pubkey: PublicKey): SharedSecret =
|
||
|
## Compute ecdh agreed shared secret.
|
||
|
let res = ecdh[KeyLength](SkSecretKey(seckey), SkPublicKey(pubkey), ecdhSharedSecretHash, nil)
|
||
|
# This function only fail if the hash function return zero.
|
||
|
# Because our hash function always success, we can turn the error into defect
|
||
|
doAssert res.isOk, $res.error
|
||
|
SharedSecret(data: res.get)
|
||
|
|
||
|
proc ecdhSharedSecretFullHash(output: ptr byte, x32, y32: ptr byte, data: pointer): cint
|
||
|
{.cdecl, raises: [].} =
|
||
|
## Hash function used by `ecdhSharedSecretFull` below
|
||
|
# `x32` and `y32` are result of scalar multiplication of publicKey * privateKey.
|
||
|
# Leading byte is 0x02 if `y32` is even and 0x03 if odd. Then concat with `x32`.
|
||
|
|
||
|
# output length is derived from `x32` length + 1 and taken from ecdh
|
||
|
# generic parameter `FullKeyLength`
|
||
|
|
||
|
# output[0] = 0x02 | (y32[31] & 1)
|
||
|
output[] = 0x02 or (y32.offset(31)[] and 0x01)
|
||
|
copyMem(output.offset(1), x32, KeyLength)
|
||
|
return 1
|
||
|
|
||
|
func ecdhSharedSecretFull*(seckey: PrivateKey, pubkey: PublicKey): SharedSecretFull =
|
||
|
## Compute ecdh agreed shared secret with leading byte.
|
||
|
let res = ecdh[FullKeyLength](SkSecretKey(seckey), SkPublicKey(pubkey), ecdhSharedSecretFullHash, nil)
|
||
|
# This function only fail if the hash function return zero.
|
||
|
# Because our hash function always success, we can turn the error into defect
|
||
|
doAssert res.isOk, $res.error
|
||
|
SharedSecretFull(data: res.get)
|