Crypto OS
Technical Crypto OS第一阶段 · Blockchain Developer Mental Model

T2 · 密码学基础

为什么一串签名可以证明「是我」?

练习的能力
BuilderProtocol Literacy
动手
本地生成密钥对,对一段消息签名,再用公钥验签;故意改一个字节,看验签怎样失败。
AI Lab
让 AI 写一个 Merkle 证明的验证函数,自己构造一个伪造证明,确认它会被拒绝。

一个现实问题

登录你的 DApp 不需要密码。

用户点「连接钱包」,弹出一个框,点一下「签名」,就登录了。你的后端收到两样东西:一个地址,和一串 132 个字符的十六进制字符串。凭这两样东西,后端就敢认定这个请求来自那个地址的主人。

换成一个传统后端,同一件事需要:注册流程、数据库里的口令摘要、一次比对、一个会话。这里一样都没有。服务器从来没见过这个用户,也不保存关于他的任何秘密。

更让人不安的是那串十六进制是公开的。它走在 HTTP 请求里,可能被记进访问日志,可能被中间人看到,可能被你自己不小心打印到控制台。

它被看到之后,为什么别人还是没法冒充你?

一串任何人都能抄走的字符,凭什么能证明「是我」?

思想实验

把问题缩到最小:你在一个陌生城市,要向一个素不相识的人证明「这张纸条是我写的」。

三个约束:你们没有任何共同的秘密;你不能把自己的秘密告诉他;这张纸条可能被任何人看到和复制。

第一版:约定一个暗号,写在纸条末尾。

问题立刻出现。他一旦看到暗号,就能自己写一张一模一样的纸条。**验证者同时变成了伪造者。**这不是实现细节的问题,是结构上的死结。

第二版:不写暗号本身,写一个由暗号算出来的、算不回去的乱码。

他没法从乱码反推暗号了。但他也不需要反推——他把这串乱码原样抄到另一张纸条后面就行。证明和内容没有绑定,抄一次就能永远复用。

第三版:把暗号和纸条内容拼在一起,再算成乱码。

现在证明和内容绑定了,换一张纸条乱码就对不上。可是验证者要想重算一遍,他必须知道暗号——于是又回到第一版。

要么他能验证,要么他不能伪造,二者不可兼得。

一个普通的单向函数到这里就走到头了。要打破这个死结,需要一样更奇怪的东西:

一对数字。一个用来生成证明,一个用来检查证明,而且从「检查用的那个」推不出「生成用的那个」。

有了它,你把检查用的那个公之于众——谁都能验证;生成用的那个你一辈子不交给任何人——谁都不能伪造。死结解开了。

现在回头看开头那个问题:那串 132 个字符不是你的秘密,它是用秘密算出来的一个一次性结果。看到它没有用,因为它只对那一条消息有效。

你来决定

后端要确认「这个请求来自地址 A 的主人」。你选哪一种?

观察结果

把四种做法放在一起,暴露出来的是同一组维度:

方案验证者需要知道秘密吗能绑定具体消息吗能被重放吗泄露后果
直接发秘密需要不能随便重放资产全部丢失
发秘密的摘要不需要不能随便重放身份可被冒充
发「秘密加消息」的摘要需要不能双方都能伪造
发一个可公开验证的证明不需要取决于消息内容只影响这一条消息

最后一行有一个含糊的地方,值得单独拎出来:能不能被重放,不取决于密码学,取决于你在消息里写了什么。

一条签名永远有效。如果你签的是「登录」两个字,那这条签名可以在任何时刻、任何网站、任何一条链上被重复使用。要让它只用一次、只在一个地方有效,你必须把随机数、域名、链的编号、过期时间写进被签的内容里

这不是密码学帮你做的事。这是你自己要设计的事。

建立模型

三个原语,加一个组合结构。

**原语一:单向摘要。**任意长度的输入,固定长度的输出。同样的输入永远得到同样的输出;改一个字节,输出面目全非;从输出倒推输入在计算上不可行。

它的用处有三个:给任意数据一个稳定的短名字;检测内容有没有被改;把「很多数据」压缩成「一个值」。

**原语二:一对密钥。**私钥是一个随机数,公钥由私钥单向推导。正推很快,反推不可行。地址又是公钥的摘要——所以地址、公钥、私钥是一条单向链:

  1. 随机数 = 私钥
  2. 椭圆曲线运算 → 公钥
  3. 摘要并截断 → 地址
每一步都只能正着走。丢了私钥,没有任何办法从地址倒推回去,也没有任何人能帮你找回。

**原语三:签名与验签。**签名把「私钥」和「消息摘要」吃进去,吐出一个数字(在以太坊上是 r、s、v 三段,共 65 字节)。验签把「消息」和「签名」吃进去,反推出签名者的公钥,再算成地址。

注意这个方向:验签不是「拿公钥去解密」,而是从签名里恢复出公钥。所以你不需要事先知道对方是谁,你是通过验证过程才知道他是谁的。这也是为什么以太坊的交易里不需要携带发送方地址——它是算出来的。

一次完整的身份证明:

  1. 服务端给出一条含随机数与域名的消息
  2. 钱包对它的摘要签名
  3. 客户端把消息与签名一起送回
  4. 服务端恢复出地址
  5. 比对并作废这个随机数
最后一步最容易被漏掉。不作废随机数,这条签名就能被无限重放。

组合结构:把很多条数据压成一个值,并且还能单独证明其中任意一条。

场景:一份 30 万个地址的名单,要让链上合约能判断「某个地址在不在名单里」。全部写进合约,成本高到不可接受。

做法:把 30 万条数据各自摘要,得到 30 万个叶子;相邻两个拼起来再摘要,得到 15 万个;一层层往上,最后剩下一个值。链上只存这一个值

用户领取时提交两样东西:自己那条数据,以及从自己的叶子到顶端这一路上,每一层「兄弟」的值——30 万条数据大约只需要 19 个。合约照着这条路径重算一遍,算出来的顶端值等于链上存的那个,就放行。

它解决的问题一句话:在不给出全部数据的情况下,证明某一条数据确实在里面。

这个结构在链上无处不在:区块头里用它承诺整个交易列表,空投用它承诺名单,跨链桥用它承诺一批消息。形状都一样。

它叫什么

Hash哈希 / 摘要

把任意长度的输入映射成固定长度输出的单向函数。同输入必然同输出,改一个比特输出就完全不同,反推不可行。

以太坊用的是 Keccak-256,比特币用 SHA-256。它们不是同一个函数,把两者混用是新手最常见的错误之一,算出来的地址会完全对不上。

Private Key私钥

一个随机数。它不是「生成的」,是「抽的」——随机性的质量直接决定安全性。

它同时是身份和资产控制权。这一点在传统系统里没有对应物:那边重置密码就能找回账号,这边丢了就是永久丢失。

Public Key公钥

由私钥单向推导出的值,可以公开。地址是公钥再做一次摘要并截断得到的。

所以「暴露公钥」是安全的,「暴露地址」更是本来就要公开的。危险的只有私钥,以及任何能推出私钥的东西——包括签名时用到的随机数。

ECDSA椭圆曲线数字签名算法

以太坊与比特币使用的签名算法,曲线是 secp256k1。Solana 用的是另一套(Ed25519),密钥长度和签名格式都不同。

ECDSA 每次签名要用一个一次性随机数。这个随机数重复使用一次,私钥就能被直接解出来——下面「真实案例」里有真实事故。

Signature签名

对某条消息的一次性证明。在以太坊上是 65 字节:r 和 s 各 32 字节,v 是 1 字节的恢复标识。

签名只能证明两件事:这条消息没被改过;签名者在签的那一刻持有私钥。它不能证明签名者理解了自己签了什么。

Merkle Tree默克尔树

把一个集合逐层两两摘要,压成一个根值的结构。给出一条数据加上一条从叶子到根的路径,任何人都能独立验证这条数据属于这个集合,而不必拿到整个集合。

路径的长度是数据条数的对数——一百万条数据,二十来个哈希就够。这个「对数级」是它值钱的全部原因。

动手

动手本地生成密钥对,签名、验签,然后改一个字节openssl(macOS 与 Linux 自带)0 元

全程在本机,不涉及任何链、任何测试网、任何真实资产。这一章不发交易,生成的密钥只用于实验,做完请删掉。

生成一个 secp256k1 的私钥,再从它导出公钥。

openssl ecparam -name secp256k1 -genkey -noout -out priv.pem
openssl ec -in priv.pem -pubout -out pub.pem
cat priv.pem pub.pem

两个文件的关系是单向的:有 priv.pem 随时能重新生成 pub.pem,反过来不行。试着删掉 pub.pem 再生成一次,确认结果一模一样——这说明公钥里没有任何额外信息。

准备一条消息并签名。

printf 'transfer 100 to alice' > msg.txt
openssl dgst -sha256 -sign priv.pem -out sig.bin msg.txt
xxd sig.bin | head

看一眼 sig.bin 的长度。它和消息长度无关——签的不是消息,是消息的摘要。

用公钥验签。

openssl dgst -sha256 -verify pub.pem -signature sig.bin msg.txt

应该输出 Verified OK。注意这里只用到了 pub.pem,私钥完全没参与。这就是那个死结被解开的地方。

改一个字节,再验一次。

printf 'transfer 100 to blice' > tampered.txt
openssl dgst -sha256 -verify pub.pem -signature sig.bin tampered.txt

失败。改的是名字里的一个字母,摘要完全变了,签名就对不上。

再试一种:消息不动,把签名文件改一个字节,同样失败。签名验证没有「差不多」这种状态,只有过和不过。

看一眼真实链上的签名长什么样。

打开任意一个钱包,找到「签名消息」功能,签一条你自己写的短消息,把结果复制出来。

数一下字符:0x 加 130 个十六进制字符,正好 65 字节。前 64 个字符是 r,接着 64 个是 s,最后 2 个是 v。

和上一步 openssl 的输出对比:曲线是同一条,但以太坊用 Keccak-256 而不是 SHA-256,用固定的 65 字节而不是 DER 编码,而且在摘要之前还会给消息加一段固定前缀。**数学一样,编码和预处理不一样。**这个差异会让你在跨语言实现验签时踩至少一次坑。

把签名内容读一遍。

回到钱包,这次找一个真实 DApp 的登录签名,把弹窗里的全文读完。检查里面有没有:域名、随机数、过期时间、链的编号。

缺哪一项,就意味着这条签名在哪个维度上可以被重放。

AI Lab

AI Lab让 AI 写一个 Merkle 证明验证函数,然后你去伪造一个证明骗它Level 2 · AI Copilot

分两步,第二步才是重点。

第一步:
用我指定的语言写一个函数,验证一条 Merkle 证明:
输入是 leaf、proof 数组、root,输出 true 或 false。
说明你对哈希函数、拼接顺序、叶子与内部节点区分这三点分别做了什么假设。

第二步:
现在你来当攻击者。给定你刚才写的这个实现,
构造一组输入,让它对一条根本不在树里的数据返回 true。
如果你认为构造不出来,说明是哪一行代码挡住了。

第二步的价值在于:Merkle 验证有一个经典缺陷叫第二原像攻击——如果叶子和内部节点用同一种方式哈希,那么把一个内部节点当成叶子提交,加上更短的路径,同样能算到正确的根。防法是给叶子和内部节点加不同的前缀,或者强制它们长度不同。

模型很可能第一步写出有这个缺陷的版本,第二步又说「构造不出来」。这时候你把上面这段缺陷描述贴给它,让它重写。

你要练的不是写出这个函数,是知道该去问什么。

另外:让它给一个可以直接跑的例子时,如果它顺手编了一个空投合约地址或者一个 Merkle 根,去链上查一下。大概率查不到。

AI 说完之后,你必须自己验证

  • 它写的哈希函数确实是这条链上用的那个,没有把 SHA-256 和 Keccak-256 混用
  • 它对两个子节点的拼接顺序有明确规定,而不是隐含假设
  • 它处理了叶子与内部节点的区分,否则一个内部节点可以被当作叶子来伪造
  • 它给的库名和函数签名真实存在,你能在官方文档里查到同名的方法
  • 十六进制字符串与字节数组的转换没有搞错,特别是有没有多算那两个 0x 字符
  • 它没有把「证明验证通过」说成「这条数据一定是合法的」,根本身的可信度是另一件事

真实案例

随机数重用,私钥被直接解出来2010 年与 2013 年

ECDSA 每次签名需要一个一次性随机数。如果同一个私钥用同一个随机数签了两条不同的消息,任何人拿到这两条签名就能用初中代数解出私钥。

2010 年某游戏主机的代码签名系统把这个随机数写成了常量,私钥被公开计算出来,整套签名体系当场失效。2013 年某移动系统的随机数生成器存在缺陷,导致一批钱包生成了重复的随机数,地址里的币被陆续搬空。

这说明:**密码学算法没有被攻破,实现被攻破了。**你自己实现签名逻辑时,随机数这一块是最不该自己写的部分。

盲签名钓鱼

用户在一个仿冒站点点了「签名」,弹窗里是一串看不懂的十六进制。他签了。

那条消息实际上是一份挂单授权:把他钱包里的某个 NFT 以零价格卖给攻击者。签名本身完全合法,链上没有任何异常,钱包也没有任何理由拦截——用户确实同意了,只是他不知道自己同意了什么。

这类攻击在 2022 到 2023 年造成了大量损失。防御手段是结构化签名:把被签的内容按字段展开,让钱包能渲染成人话,同时把域名和链编号绑进去。T13 会完整讲这一层。

同一条签名在另一条链上又用了一次

早期的交易格式里不包含链的编号。于是一条在测试网上发出的交易,原样广播到主网同样有效。

后来的规范把链编号写进被签的内容里解决了交易重放,但离线签名的消息仍然要靠应用自己处理。很多 DApp 的登录签名至今不带链编号和过期时间——意味着一条一年前抓到的签名今天还能登录。

30 万地址的空投,链上只存 32 字节

把整份名单写进合约,成本高到不可接受;每次领取时由项目方签名放行,又要求项目方的服务一直在线。

用 Merkle 根的做法:链上只存一个 32 字节的根,名单挂在任意公开位置,用户自带路径来证明自己在名单里。项目方的服务器挂了也不影响领取。

这是 Merkle 结构在应用层最常见的一次落地,也是你最可能自己实现一遍的一次。

改一个变量

如果有人找到了这个哈希函数的碰撞

两份不同的数据算出同一个摘要,意味着一份 Merkle 证明可以指向两条不同的数据,一份合约的字节码校验可以被替换。

整套「用摘要代表数据」的做法会同时失效,而不是某一个应用出问题。这就是为什么链在选哈希函数上极度保守,宁可慢也不换新的。

如果私钥不是随机抽的,而是你自己想的一句话

所谓脑钱包。攻击者不需要破解算法,只需要把常见的诗句、歌词、密码字典挨个试一遍。

这类地址被扫空的平均时间是秒级。安全性不在算法里,在那个随机数的熵里。

如果登录签名里不带随机数和过期时间

签名变成一个永久有效的通行证。你的服务器日志、你的错误上报平台、任何一次抓包,都成了凭证泄露源。

更隐蔽的是:即使用户后来把这个地址的所有授权都撤销了,这条登录签名依然有效——因为它从来没上过链,链上没有任何东西可以撤销它。

如果换成 Solana

曲线和算法都不同,地址就是公钥本身而不是公钥的摘要,所以从地址能直接拿到公钥。

一个直接后果:Solana 的交易里必须显式列出所有签名者,而以太坊是从签名反推的。T6 会讲这个差异背后的账户模型。

带走的问题

2
为什么需要 Blockchain?

签名验证这件事本身不需要区块链,SSH 和 HTTPS 用了几十年。这一章里真正需要链的,是「验证的规则对所有人一样,而且没有人能改」——公钥恢复的逻辑写在协议里,不是写在某家公司的服务器上。

9
谁承担风险?

私钥丢了,风险百分之百由持有人承担,没有任何追索。这和传统金融的责任分配完全相反,也是为什么这门课后面所有涉及密钥的地方都要单独设计:多签、社交恢复、权限分层,本质上都是在把这个风险重新分配掉。

16
Agent 有什么权限?

一个 AI Agent 拿到的如果是私钥,它的权限就是「这个地址的全部」,没有中间档位。要给它更小的权限,只能靠一份限定了额度、对象和时效的签名授权。先想清楚要授什么权,再想怎么签,顺序反了就会做出一个能被一句提示词掏空的 Agent。

本章自测

一句话带走

你永远不发送私钥,你只发送一个任何人都能验证的证明。

做完这一章的动手环节了?勾上它查看全部进度

本页目录