简短回答:secp256k1 是素域 p = 2²⁵⁶ − 2³² − 977 上的椭圆曲线 y² = x³ + 7,由 SEC 2 于2000年定义。它的群包含素数个点(余因子为 1),安全强度约为 128 位。它的系数是为了速度而选定的,并非由种子生成:a = 0 简化了公式,而且这条曲线具有可高效计算的自同态,能加快标量乘法。比特币从2009年的首次提交起就使用它,以太坊随后跟进,如今 Nostr 和大多数 EVM 链也在使用它。WebCrypto、TLS 1.3 和大多数安全硬件都不支持它;在区块链协议之外,应优先选择 P-256 或 Ed25519。在普通 ECDSA 之上,它的生态系统还加入了各自的规则:比特币的中继策略和以太坊交易只接受 low-S 签名;用公钥恢复代替传输公钥;以 Keccak-256 或双重 SHA-256 作为哈希。
参数
SEC 2 v2.0 §2.4.1 用六个数定义这条曲线(SEC 2):
| 参数 | 值 | 含义 |
|---|---|---|
| p | 2²⁵⁶ − 2³² − 977(0xffff…fffefffffc2f) | 域的素数;它的形式让模 p 约简的代价很低 |
| a, b | 0, 7 | 曲线 y² = x³ + 7 |
| G | x = 0x79be667e…16f81798,y = 0x483ada77…fb10d4b8 | 基点 |
| n | 0xffff…fffebaaedce6af48a03bbfd25e8cd0364141 | G 的阶,是素数 |
| h | 1 | 余因子:除无穷远点外,每个点都能生成整个群 |
这里的一切都无须盲目相信,也没有任何数值来自来历不明的种子。下面的脚本用普通的 Python 整数检查这些常量:
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7: ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime: ", probable_prime(n))
print("n * G is infinity: ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n: ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7: True
n is a probable prime: True
n * G is infinity: True
trace t = p + 1 - n: 432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1
add 和 mul 是教科书式的仿射坐标点加和倍点-点加(double-and-add)算法(用来检查常量没有问题,但不能用来处理秘密值:它们不是常量时间的)。迹与 SafeCurves 公布的值一致。由于 n 是素数且等于点的个数,这里不存在小子群:让 Curve25519 协议变得复杂的余因子问题,在这条曲线上并不存在。
为什么这种形式让它更快
这些数有三个特性是刻意设计的:
- 素数是 2²⁵⁶ 减去一个较小的数,因此把 512 位的乘积模 p 约简时,只需乘几次 2³² + 977,而不必做一般的除法。
- a = 0 让每次倍点运算都省去一次乘法。
- 自同态:由于 p ≡ 1 (mod 3),存在模 p 的三次单位根 β,映射 (x, y) → (βx, y) 会把每个点映射到曲线上的另一个点。这个映射等同于乘以模 n 的三次单位根 λ。于是,标量 k 可以拆成各约 128 位的两半,即 k = k₁ + k₂λ,kP 则按 k₁P + k₂(λP) 计算,其中 λP 只需一次域乘法。这就是 GLV 方法(Gallant、Lambert、Vanstone,2001年)。
同一个脚本会求出 β 和 λ,并检查这个恒等式:
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy): ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta =", hex(beta))
print("lambda =", hex(lam))
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy): True
beta = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72
libsecp256k1 用的正是这些常量。Hal Finney 在 2011年2月的一个 bitcointalk 帖子中针对比特币介绍了这个技巧,并预计验证速度能提高 20% 到 25%;Pieter Wuille 正是以他的代码为起点开发了 libsecp256k1。Bitcoin Core 在 PR #20147(Bitcoin Core 0.21,2021年)中随 libsecp256k1 的更新默认启用了 GLV 优化,该 PR 中测得,在启用签名检查的情况下,初始同步快了约 16%。这些优化有多大意义取决于所用的库:OpenSSL 没有为 secp256k1 编写专门的代码,在 P-256 上要快得多(具体数字见 P-256 vs secp256k1)。
比特币和以太坊为什么使用它
比特币的首次提交(2009年8月)用 OpenSSL 的 EC_KEY_new_by_curve_name(NID_secp256k1) 创建密钥。中本聪从未解释过这个选择。通常给出的理由是曲线本身的特性,而不是其使用者的说法:比特币 wiki 指出,它“以一种特殊的非随机方式构造,因而可以进行特别高效的计算”,并且“与流行的 NIST 曲线不同,secp256k1 的常量是以可预测的方式选取的”(Bitcoin wiki)。以太坊沿用了比特币的曲线,但换掉了哈希:交易和消息用 Keccak-256 计算哈希,地址也用它从公钥派生。
标准化组织后来才跟上。RFC 8812(2020年)为 JOSE 和 COSE 注册了 ES256K;NIST SP 800-186(2023年)§H.2 则规定,这条曲线“允许用于区块链相关应用”(它不属于推荐用于一般用途的曲线)。
安全性如何
针对这个群中的离散对数问题,已知最好的攻击是 Pollard rho 算法,据 SafeCurves 计算,约需 0.886·√n ≈ 2¹²⁷·⁸ 次加法,与 P-256 的数字相同,也就是通常所说的“128 位安全强度”。(在这条曲线上,还可以利用 3 阶自同态把 rho 算法再加快一点,约为 √3 倍,但这不会改变整体结论。)SafeCurves 没有把 secp256k1 列为“安全”曲线,但理由涉及的是实现,而不是离散对数:
| SafeCurves 标准 | secp256k1 | 对你意味着什么 |
|---|---|---|
| rho、迁移(transfer)、刚性(rigidity) | 通过 | 离散对数难以求解,常量的来历也有说明 |
| CM 判别式 | 未通过(D = −3) | 判别式小正是自同态的来源;它并不会导致任何攻击 |
| 扭曲安全性(twist security) | 通过,2¹⁰⁹·⁵ | 低于曲线本身;只对跳过点校验的代码有影响 |
| 梯形算法(ladder)、完备公式 | 未通过 | 与 Curve25519 相比,编写常量时间、无例外情况的代码需要更加小心 |
| 不可区分性 | 未通过 | 点看起来不像随机字符串;这影响的是抗审查协议,而不是签名 |
实践中的教训与 P-256 相同:使用专为这条曲线编写的库(libsecp256k1、@noble/curves、Rust 的 k256、go-ethereum 的绑定),在输入时校验公钥,切勿自己实现处理秘密值的标量乘法。以点的形式传入的密钥,使用前必须检查它是否位于 y² = x³ + 7 上;跳过这一步,正是经典的无效曲线攻击得以成立的原因,扭曲安全性的数字也正是在这里起作用。此外,与所有 ECDSA 曲线一样,重复或有偏差的 nonce 会泄露密钥(ECDSA nonce 重用)。
密钥与地址
私钥是 [1, n − 1] 中的一个数 d;公钥是点 Q = dG。同一个 Q 有多种写法,把它们混为一谈是“地址不对”这类 bug 的常见原因。下面以 d = 1 为例,它的公钥就是 G 本身(这是人人都知道的密钥,切勿使用):
compressed (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
- 压缩格式(SEC 1 §2.3.3):y 为偶数时用
02,为奇数时用03,后接 x。十多年来,比特币默认使用压缩公钥。解压缩需要计算模 p 的平方根,对于这个 p(p ≡ 3 mod 4),只需一次模幂运算:y = (x³ + 7)^((p+1)/4)。 - 未压缩格式:
04 ‖ x ‖ y。同一个密钥在这种形式下会得到不同的比特币地址,因此使用未压缩公钥的旧钱包会为同一个私钥显示其他地址。 - 仅 x 坐标(32 字节):Taproot 和 Nostr 使用 BIP-340 Schnorr 签名,其公钥省略 y 坐标,并默认 y 为偶数。
- 以太坊地址:对 64 字节的
x ‖ y(不含04前缀)计算 Keccak-256,取最后 20 字节。Keccak-256 不是 SHA3-256:两者的填充不同,使用hashlib.sha3_256会得到错误的地址。
low-S:一个签名,两种有效形式
ECDSA 签名 (r, s) 有一个同样能通过验证的孪生签名 (r, n − s),因为对 nonce 点取负不会改变 r。在 TLS 中使用 P-256 时,没有人在意这一点。但对比特币来说,这很重要:孪生签名会让同一笔交易产生不同的交易 ID,按 ID 跟踪付款的服务就可能被欺骗。Bitcoin Core 0.11.1(2015年)让节点“在中继或挖矿时要求 ECDSA 签名使用规范的‘low-s’编码”(版本说明),这是一条中继策略,而不是共识规则。以太坊通过 EIP-2(Homestead,2016年)走得更远:“所有 s 值大于 secp256k1n/2 的交易签名现在都被视为无效”,而“ECDSA 恢复预编译合约保持不变,将继续接受高 s 值”。
通用库不遵守这些规则,区块链库则遵守。我们的实验用 Node.js 内置的 OpenSSL 对 1,000 条随机消息签名,然后用三个库验证其中一个 high-S 签名及其 low-S 孪生签名:
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify high-S: ${opensslOk(sig)} low-S twin: ${opensslOk(low)}`);
console.log(`noble default high-S: ${secp256k1.verify(sig, msg, pub)} low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify high-S: true low-S twin: true
noble default high-S: false low-S twin: true
noble lowS: false high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s
OpenSSL 生成的签名约有一半是 high-S,OpenSSL 两种形式都接受;除非另行设置,@noble/curves 在 secp256k1 上会拒绝 high-S 签名;ethers 甚至拒绝用 high-S 签名构造签名对象。用通用库或 HSM 签名、再提交到区块链的系统必须做规范化:如果 s > n/2,就把它替换为 n − s,并翻转恢复位。Google Cloud KMS 在文档中说明,它的 secp256k1 密钥只返回 low-S 签名;AWS KMS 的 ECC_SECG_P256K1 文档没有这样的承诺,因此需要你自己对它的输出做规范化。这些签名传输时使用的 DER 编码和原始编码,在 ECDSA 签名格式中有详细介绍。
公钥恢复
有了签名、消息哈希和额外的一两位信息,就能算出签名者的公钥,因此以太坊交易根本不携带公钥,比特币签名消息则直接对照地址验证。这些额外的位就是恢复 ID(v、yParity,或比特币消息签名的头部字节)。恢复的原理、可能出现的问题以及 ecrecover 的行为,详见 ECDSA 公钥恢复。
哪些平台支持它,哪些不支持
| 平台 | secp256k1 |
|---|---|
WebCrypto(浏览器、Deno、Node 的 crypto.subtle) | 不支持;使用 @noble/curves 或 libsecp256k1 的 WASM 构建 |
OpenSSL 和 Node.js crypto | 支持,使用通用(较慢的)代码 |
| BoringSSL | 不支持:它只内置了 P-224、P-256、P-384 和 P-521 |
| Java(SunEC) | 已在 JDK 16 中移除:Curve not supported: secp256k1;改用 Bouncy Castle |
| Apple CryptoKit、Secure Enclave、Android Keystore、TPM 2.0、PIV 卡 | 不支持 |
| 云 KMS | AWS KMS ECC_SECG_P256K1;Google Cloud KMS EC_SIGN_SECP256K1_SHA256(仅限 HSM 保护级别);Azure Key Vault P-256K 配合 ES256K |
| TLS 1.3、Web PKI 中的 X.509、WebAuthn | 不支持 |
手机和笔记本电脑的安全硬件不支持这条曲线,因此移动钱包要么在软件中保存 secp256k1 密钥(由一个确实存放在硬件中的密钥保护),要么依赖单独的硬件钱包。
常见错误
- 用 SHA3-256 代替 Keccak-256 计算哈希(以太坊),或者用单次 SHA-256 代替比特币在消息和交易中使用的双重 SHA-256。这样签名验证的是另一条消息,公钥恢复得到的则是另一个看似有效的地址。
- 提交 high-S 签名:把 OpenSSL、HSM 或云 KMS 生成的签名提交到以太坊或比特币网络。应先做规范化。
- 混用压缩公钥和未压缩公钥来推导比特币地址。
- 原始字节长度:64 字节的
r ‖ s和 65 字节的r ‖ s ‖ v是不同的格式;库会明确报错,例如 ethers 的 invalid raw signature length。 - 想当然地认为平台支持它:在 Java 11 上能运行的代码,在 Java 17 上会报错;WebCrypto 从来就没有这条曲线。
- 在没有要求的场合使用 secp256k1:用于令牌、TLS、设备身份或通行密钥(passkey)时,它没有任何好处,还会失去硬件支持。
来源
- SEC 2 v2.0:推荐的椭圆曲线域参数,§2.4.1;SEC 1 v2.0,§2.3.3(点的编码)
- NIST SP 800-186,附录 H.2
- SafeCurves:rho、判别式和扭曲安全性表格
- Gallant、Lambert、Vanstone,“Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms”,CRYPTO 2001;Hal Finney,加速签名验证(2011年);Bitcoin Core PR #20147
- Bitcoin Core 0.11.1 版本说明(low-S 中继策略);EIP-2
- RFC 8812(
ES256K) - 密钥服务:AWS KMS 密钥规格、Google Cloud KMS 算法、Azure Key Vault 密钥类型