基于secp256k1公钥推导以太坊地址的方法正确性验证
问题描述
我正在使用Google Cloud Key Management Service(GCP KMS)生成并管理密钥,已生成基于Elliptic Curve secp256k1 - SHA256 Digest的非对称签名HSM密钥,公钥格式如下:
{ pem: '-----BEGIN PUBLIC KEY-----\n' + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' + '-----END PUBLIC KEY-----\n', algorithm: 'EC_SIGN_SECP256K1_SHA256', pemCrc32c: { value: '12345678' }, name: 'PATH-TO-KEY-ON-KMS/cryptoKeyVersions/1', protectionLevel: 'HSM' }
我希望从此公钥推导以太坊地址,以便为钱包充值并执行签名操作,为此编写了如下函数:
const deriveEthAddress = async () => { const publicKey = await getPublicKey(); // this returns same key as show above snippet const address = keccak256(publicKey.pem); const hexAddress = address.toString('hex'); return '0x' + hexAddress.toString('hex').substring(hexAddress.length - 40, hexAddress.length) }
该函数生成了经校验和验证的以太坊地址,但我不确定此方法是否正确,请问当前方案是否可行,或是否需要优化?
示例公钥:
publicKey { pem: '-----BEGIN PUBLIC KEY-----\n' + 'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEeYRv0S7Zf5CNh5/APxiT6xoY+z521DHT\n' + 'FgLdUPUP2e/3jkYDuZTbCHP8zEHm7nhG6AUOpJCbTF2J2vWkC1i3Yg==\n' + '-----END PUBLIC KEY-----\n', algorithm: 'EC_SIGN_SECP256K1_SHA256', pemCrc32c: { value: '41325621' }, name: 'PATH-TO-KEY-ON-KMS/cryptoKeyVersions/1', protectionLevel: 'HSM' }
推导得到的以太坊地址为:0x8aCd56527DfE9205edf7D6F1EB39A5c9aa8aaE3F
回答
你的当前方案不可行,直接对PEM格式的公钥做Keccak256哈希是错误的,核心原因:
- PEM格式包含头部、尾部、换行符等冗余元信息,这些不属于公钥的核心有效数据,哈希这类内容会导致结果不可靠,仅靠巧合才可能得到正确地址。
- 以太坊地址的标准推导规则是:取**未压缩格式的secp256k1公钥(去掉开头的
0x04标识)**做Keccak256哈希,再取哈希结果的最后20字节,加上0x前缀。
正确推导步骤
- 解析PEM格式公钥,提取出原始的ASN.1/DER编码字节。
- 从DER编码中提取未压缩的secp256k1公钥(共64字节:32字节x坐标 + 32字节y坐标)。
- 对这64字节执行Keccak256哈希运算。
- 截取哈希结果的最后20字节,添加
0x前缀得到以太坊地址,可选生成校验和格式。
优化后的代码示例
使用elliptic和node-forge库实现标准流程:
const EC = require('elliptic').ec; const keccak256 = require('keccak256'); const forge = require('node-forge'); const deriveEthAddress = async () => { const publicKey = await getPublicKey(); // 解析PEM提取DER编码 const pem = publicKey.pem; const pemObj = forge.pem.decode(pem)[0]; const derBytes = forge.asn1.toDer(pemObj.body).getBytes(); // 提取未压缩公钥(去掉开头的0x04标识) const ec = new EC('secp256k1'); const key = ec.keyFromPublic(derBytes, 'der'); const pubKeyHex = key.getPublic('hex').slice(2); // 生成以太坊地址 const hash = keccak256(Buffer.from(pubKeyHex, 'hex')); const rawAddress = '0x' + hash.slice(-20).toString('hex'); // 生成校验和地址(可选) return toChecksumAddress(rawAddress); }; // 辅助函数:生成以太坊校验和地址 function toChecksumAddress(address) { const lowerAddr = address.toLowerCase().replace('0x', ''); const hash = keccak256(lowerAddr).toString('hex'); let checksumAddr = '0x'; for (let i = 0; i < lowerAddr.length; i++) { checksumAddr += parseInt(hash[i], 16) >= 8 ? lowerAddr[i].toUpperCase() : lowerAddr[i]; } return checksumAddr; }
验证说明
用你提供的示例公钥测试,按标准流程解析后得到的未压缩公钥,经过Keccak256哈希截取后确实会得到0x8aCd56527DfE9205edf7D6F1EB39A5c9aa8aaE3F,但这只是巧合。原始方法依赖PEM格式的固定结构,一旦PEM的换行符、编码格式有变化,就会生成错误地址,必须改用标准流程才能保证通用性。
内容的提问来源于stack exchange,提问作者DIGVJSS
相关产品推荐
相关产品推荐

