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 数字签名:区块链世界的「手写签名」

数字签名,其实就是哈希函数和非对称加密的组合拳。流程是这样的:

  1. 签名方:对消息做哈希,得到摘要。然后用私钥加密这个摘要,得到签名。把消息和签名一起发出去。
  2. 验证方:收到消息和签名后,用同样的哈希算法对消息做哈希,得到摘要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 签名中最致命的漏洞之一。一定要用密码学安全的随机数生成器。

好了,密码学基础就回顾到这里。哈希函数、非对称加密、数字签名,这三者环环相扣。你理解了它们,后面启动链的签名验证流程,你就能一眼看穿本质。