2、密码学基础回顾:哈希函数、非对称加密、数字签名原理
好,咱们正式开始技术内容之前,先花点时间把密码学这块地基夯实。你想想看,区块链里所有的「信任」机制,说白了就靠三个东西:哈希函数、非对称加密、数字签名。这三兄弟要是搞不明白,后面启动链的签名验证你肯定听得云里雾里。
我个人习惯,讲技术之前先画个图,把知识框架搭起来。这样你脑子里有个地图,学起来不迷路。
2.1 哈希函数:区块链的「指纹」
哈希函数,你可以把它想象成一个「榨汁机」。你扔进去一个苹果(任意长度的数据),出来的一定是一杯固定容量的苹果汁(固定长度的哈希值)。而且,你没法从这杯果汁里把苹果还原出来。
这就是哈希的核心特性——单向性。我在项目中遇到过好几次新人问:「能不能根据哈希值反推出原始数据?」答案很明确:不能。这是数学上设计好的,不是算力够不够的问题。
另一个关键特性是抗碰撞性。就是说,你几乎不可能找到两个不同的输入,得到同一个哈希值。嗯,这里要注意,是「几乎不可能」,不是「绝对不可能」。但从工程角度,你可以把它当成不可能。
区块链里哈希函数用在哪?
- 交易哈希:每笔交易的唯一标识
- 区块哈希:链接前后区块,形成链
- Merkle 树:快速验证交易是否在区块内
- 地址生成:从公钥哈希得到账户地址
常用的哈希算法,我列个表给你对比一下:
| 算法 | 输出长度 | 安全性 | 区块链应用 |
|---|---|---|---|
| SHA-256 | 256 bits (32 bytes) | 高(目前未破解) | 比特币、多数公链 |
| Keccak-256 | 256 bits (32 bytes) | 高(SHA-3 标准) | 以太坊 |
| RIPEMD-160 | 160 bits (20 bytes) | 中 | 比特币地址生成 |
| BLAKE2b | 可变(常用 256/512) | 高(速度快) | 部分新兴链 |
避坑指南:我曾经在写智能合约时,直接用 keccak256(abi.encodePacked(a, b)) 做哈希拼接。结果发现如果 a 和 b 的类型不同,abi.encodePacked 会直接拼接字节,导致哈希碰撞风险。后来改用 abi.encode 补零填充,才彻底解决。记住:哈希函数的输入一定要规范化。
2.2 非对称加密:一把钥匙开一把锁
非对称加密,说白了就是「公钥加密,私钥解密」。你生成一对密钥:公钥可以公开给任何人,私钥只有你自己知道。
别人用你的公钥加密一段消息,只有你能用私钥解密。反过来,你用私钥加密一段消息(这就是签名),别人可以用你的公钥解密验证。
我刚开始学的时候,总觉得「非对称」这个词很抽象。后来我换了个角度理解:公钥是锁,私钥是钥匙。你把锁(公钥)发给所有人,他们可以把消息锁起来寄给你。只有你手里的钥匙(私钥)能打开。
常见的非对称加密算法:
| 算法 | 密钥长度 | 签名长度 | 特点 |
|---|---|---|---|
| ECDSA (secp256k1) | 32 bytes (私钥) | ~70 bytes | 比特币、以太坊标准 |
| Ed25519 | 32 bytes (私钥) | 64 bytes | 速度快,安全性高 |
| RSA | 256-4096 bytes | 与密钥长度相同 | 传统方案,区块链少用 |
| BLS | 32 bytes (私钥) | 48 bytes | 支持签名聚合 |
⚠️ 私钥安全红线:私钥一旦泄露,你的资产和身份就全完了。我在审计一个项目时发现,有人把私钥硬编码在智能合约的构造函数里,部署后私钥直接暴露在链上。这是绝对不能犯的错误。私钥必须离线存储,永远不要通过网络传输。
2.3 数字签名:区块链世界的「手写签名」
数字签名,其实就是哈希函数和非对称加密的组合拳。流程是这样的:
- 签名方:对消息做哈希,得到摘要。然后用私钥加密这个摘要,得到签名。把消息和签名一起发出去。
- 验证方:收到消息和签名后,用同样的哈希算法对消息做哈希,得到摘要A。然后用签名方的公钥解密签名,得到摘要B。如果A == B,签名验证通过。
你想想看,这个流程保证了什么?
- 完整性:消息一旦被篡改,哈希值对不上,验证失败。
- 身份认证:只有持有私钥的人才能生成有效的签名。
- 不可否认:签名者无法抵赖,因为只有他的私钥能生成这个签名。
我记得有一次做跨链桥审计,发现签名验证逻辑里有个漏洞:验证方没有检查签名是否来自合法的验证者集合。攻击者可以伪造一个签名,只要公钥是自己生成的就行。这就是典型的「签名验证不完整」问题。
启动链中的签名验证场景:
- 验证者节点对区块提议签名
- 验证者节点对投票(Pre-vote / Pre-commit)签名
- 轻客户端验证区块头
- 跨链交易证明验证
2.4 实战:用 Go 实现一个简单的签名验证
光说不练假把式。咱们用 Go 写一段代码,把 ECDSA 签名验证跑一遍。这段代码你后面在启动链的实战中会直接用到。
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"math/big"
)
// 生成密钥对
func generateKeyPair() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
// 签名:对消息哈希后签名
func signMessage(privateKey *ecdsa.PrivateKey, message []byte) (r, s *big.Int, err error) {
hash := sha256.Sum256(message)
return ecdsa.Sign(rand.Reader, privateKey, hash[:])
}
// 验证签名
func verifySignature(publicKey *ecdsa.PublicKey, message []byte, r, s *big.Int) bool {
hash := sha256.Sum256(message)
return ecdsa.Verify(publicKey, hash[:], r, s)
}
func main() {
// 1. 生成密钥对
privateKey, _ := generateKeyPair()
publicKey := &privateKey.PublicKey
// 2. 要签名的消息
message := []byte("Hello, 启动链签名验证!")
// 3. 签名
r, s, _ := signMessage(privateKey, message)
fmt.Printf("签名结果: r=%x, s=%x\n", r, s)
// 4. 验证签名
valid := verifySignature(publicKey, message, r, s)
fmt.Printf("签名验证结果: %v\n", valid)
// 5. 篡改消息后验证
tamperedMessage := []byte("Hello, 启动链签名验证?")
validTampered := verifySignature(publicKey, tamperedMessage, r, s)
fmt.Printf("篡改后验证结果: %v\n", validTampered)
}
运行结果:
签名结果: r=2a3b4c5d..., s=6e7f8a9b...
签名验证结果: true
篡改后验证结果: false
看到没?消息哪怕改了一个字符,验证就失败了。这就是哈希函数的「雪崩效应」——输入微小变化,输出天差地别。
2.5 常见误区与避坑
最后,我总结几个我在项目中踩过的坑,你提前知道,能省不少时间。
| 误区 | 正确做法 | 后果 |
|---|---|---|
| 直接用原始消息签名,不做哈希 | 先哈希,再签名 | 签名长度随消息变长,效率低 |
| 签名时使用相同的随机数 k | 每次签名使用不同的随机数 | 私钥可能被推导出来 |
| 验证签名时不检查公钥合法性 | 验证公钥是否在允许的集合内 | 攻击者可以伪造签名 |
| 哈希算法选择不当 | 使用 SHA-256 或 Keccak-256 | 碰撞风险,安全性降低 |
⚠️ 随机数重用攻击:我曾经审计过一个项目,发现他们的签名代码里,随机数 k 是从一个固定的种子生成的。这意味着每次签名用的 k 都一样。攻击者只要拿到两个签名,就能直接算出私钥。这是 ECDSA 签名中最致命的漏洞之一。一定要用密码学安全的随机数生成器。
好了,密码学基础就回顾到这里。哈希函数、非对称加密、数字签名,这三者环环相扣。你理解了它们,后面启动链的签名验证流程,你就能一眼看穿本质。