众所周知,区块链是一种基于密码学的技术,以太坊的账户等规则都是使用EC(椭圆曲线)这一体系来进行的,以太坊使用的EC是著名的secp256k1曲线,从本质来说以太坊账户其实就是一个ECDSA的私钥,因为掌握某一私钥就可以使用私钥对交易进行签名,可以说,就完整的掌握了该账户。而EC的私钥就是一个范围内的随机数,对于secp256k1曲线,其最大值为0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141。账户的地址就是使用该私钥生成的公钥的hash。EC的公钥是椭圆曲线某个私钥对应的点。公钥和私钥的关系简单的说就是Publickey=Privatekey*G,G是某条椭圆曲线的基点,这里的*并不是传统意义上的乘,具体内容请参考椭圆曲线椭圆域的相关知识。所幸的是OpenSSL库提供了完整的椭圆曲线相关计算,所以我们并不需要关注椭圆曲线具体的实现细节。
简单地说,从一个私钥到一个地址需要经过3步:
以太坊使用的是Keccak256算法生成Hash,它生成的Hash会有32字节,但是以太坊的地址只取了后20字节作为地址。
从以太坊的源代码中可以看到,以太坊账户地址的Hash算法使用的是Keccak256,而Keccak算法在2015年成为了SHA3标准算法。但是以太坊使用的Keccak256并不等于SHA3 256,在成为SHA3算法时NIST还是对Keccak进行了改动的。以太坊中其实实现了标准的SHA3算法,但是并没有使用。
// NewKeccak256 creates a new Keccak-256 hash.
func NewKeccak256() hash.Hash { return &state{rate: 136, outputLen: 32, dsbyte: 0x01} }
// New256 creates a new SHA3-256 hash.
// Its generic security strength is 256 bits against preimage attacks,
// and 128 bits against collision attacks.
func New256() hash.Hash { return &state{rate: 136, outputLen: 32, dsbyte: 0x06} }
可以看到其实这两种算法只差了一个dsbyte,而这个dsbyte在这里
// Pad with this instance's domain-separator bits. We know that there's
// at least one byte of space in d.buf because, if it were full,
// permute would have been called to empty it. dsbyte also contains the
// first one bit for the padding. See the comment in the state struct.
d.buf = append(d.buf, dsbyte)
被用到了,看注释发现,这个dsbyte就是pad。
OpenSSL的最新版本1.1.1中并没有非标准的Keccak256,而是只有SHA3 256
typedef struct {
uint64_t A[5][5];
size_t block_size; /* cached ctx->digest->block_size */
size_t md_size; /* output length, variable in XOF */
size_t num; /* used bytes in below buffer */
unsigned char buf[KECCAK1600_WIDTH / 8 - 32];
unsigned char pad;
} KECCAK1600_CTX;
static int init(EVP_MD_CTX *evp_ctx, unsigned char pad)
{
KECCAK1600_CTX *ctx = evp_ctx->md_data;
看OpenSSL的SHA3实现可以发现,其实它的pad被存在了KECCAK1600_CTX中,而KECCAK1600_CTX则是在sha3初始化时存在了EVP_MD_CTX中,因此,使用OpenSSL实现Keccak256就需要利用一些手段把SHA3 256的pad改掉。
首先是初始化EC相关内容,以及生成公钥和私钥
BIGNUM* privatekey = BN_new();
BIGNUM* n = BN_new();
BN_CTX *bn_ctx = BN_CTX_new();
const EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp256k1);
EC_GROUP_get_order(group, n, bn_ctx);
BN_rand_range(privatekey, n);
EC_POINT *publickey = EC_POINT_new(group);
EC_POINT_mul(group, publickey, privatekey, nullptr, nullptr, bn_ctx);
unsigned char **buf = (unsigned char **)malloc(8);
size_t size = EC_POINT_point2buf(group, publickey, POINT_CONVERSION_UNCOMPRESSED, buf, bn_ctx);
然后是初始化EVP相关
const EVP_MD* evp_md = EVP_sha3_256();
EVP_MD_CTX *evp_md_ctx = EVP_MD_CTX_new();
EVP_DigestInit(evp_md_ctx, evp_md);
接下来更改pad,由于OpenSSL的头文件中并没有我们需要修改的结构体的定义,所以我们需要从源代码啊中找出它们复制到这里
struct EVP_MD_CTX_t {
const EVP_MD *digest;
ENGINE *engine; /* functional reference if 'digest' is
* ENGINE-provided */
unsigned long flags;
void *md_data;
/* Public key context for sign/verify */
EVP_PKEY_CTX *pctx;
/* Update function: usually copied from EVP_MD */
int(*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
} /* EVP_MD_CTX */;
struct KECCAK1600_CTX {
uint64_t A[5][5];
size_t block_size; /* cached ctx->digest->block_size */
size_t md_size; /* output length, variable in XOF */
size_t num; /* used bytes in below buffer */
unsigned char buf[1600 / 8 - 32];
unsigned char pad;
};
修改pad并将公钥传给EVP
//change the sha3 pad 0x06 to keccak pad 0x01
KECCAK1600_CTX* keccak256 = reinterpret_cast((reinterpret_cast(evp_md_ctx))->md_data);
keccak256->pad = 0x01;
EVP_DigestUpdate(evp_md_ctx, (*buf) + 1, size - 1);
需要注意的是,使用POINT_CONVERSION_UNCOMPRESSED方式生成的buf的第一字节为曲线类型,后面则是XY的坐标,因此,需要把曲线类型排除掉。
最后就可以获得结果
unsigned int * len = new unsigned[10];
unsigned char* result = (unsigned char*)malloc(32);
EVP_DigestFinal(evp_md_ctx, result, len);
生成的内容与以太坊生成的地址一致。最后取其后20字节就为以太坊的账户地址了。