公钥密码学
定义:用一对密钥来加密和解密信息,公钥加密的信息只能由私钥解密,私钥加密的信息只能用公钥解密,也叫非对称密码学。广泛应用于数字签名、密钥协商、加密领域。
加密过程:A 将自己的公钥给 B ,B 将机密信息用 A 的公钥加密发送给 A,A再用自己的私钥对加密信息进行解密。区块链中使用非对称加密实现账户创建,并对交易和信息签名和签收。
数学原理:质因数分解、离散对数、椭圆曲线等难以计算的数学难题。
以太坊中的ECC
椭圆曲线定义:
y
2
=
(
x
3
+
a
x
+
b
)
m
o
d
p
y^2=(x^3 + ax + b) mod p
y2=(x3+ax+b)modp
选择椭圆曲线的原因:
- 在这个曲线上加法是可定义的。
- 在这个曲线上的加法求解很简单。
- 在这个曲线上可以证明 对数运算是极复杂问题。
ECC原理:
基于椭圆曲线离散对数问题,是一个标准的椭圆曲线参数集,称为SECP-256k1。密钥长度为256位。在使用SECP-256k1曲线时,参与者获得长度为256位的公钥和私钥,私钥隐私,自己保存,公钥是公开的,用于加密数据和验证签名。
公钥 | 私钥 | |
---|---|---|
生成 | 私钥计算得到 | 随机生成 |
属性 | 公开 | 隐私 |
用途 | 加密数据、验证签名 | 签名 |
基于ECC的算法细节
- 公钥产生算法(KeyGen):
- 选择一条椭圆曲线函数 E p ( a , b ) E_p(a,b) Ep(a,b) 和基点G
- 选择一个随机数作为私钥 d A ( d A < n , n 为该 G 的阶 ) d_A(d_A < n,n为该G的阶 ) dA(dA<n,n为该G的阶),利用基点 G 计算公钥 $Q_A = G \cdot d_A $
- 签名算法(sign):
- 通过选择椭圆曲线和一个生成点,选择一个随机数k作为私钥,并计算公钥 K = k G K=kG K=kG
- 对要签名的数据进行哈希,并使用私钥k对哈希结果进行数字签名,生成一个签名$ (S, R)$。
- 使用公钥 K 和签名 ( S , R ) (S, R) (S,R)验证签名的有效性。
地址生成
过程:
- 根据ECC算法生成一对公钥和私钥,私钥随机生成,公钥则是私钥通过椭圆曲线点乘算法得到的一个点。
- 将公钥通过哈希算法(一般是kccak-256)进行处理,得到唯一的256位(32字节)哈希值。
- 取最后20字节(160位)作为以太坊钱包地址的值。
- 将这160位值进行Base58编码,得到以太坊钱包真正的地址。
算法 | 用途 |
---|---|
ECC算法 | 私钥安全性 |
哈希算法 | 钱包地址的唯一性 |
Base58编码 | 钱包地址的易读性 |
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/hex"
"fmt"
"golang.org/x/crypto/sha3"
)
func main() {
// 生成一个ECDSA密钥对
privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// 获取公钥
pubKey := privKey.PublicKey
// 将公钥序列化为字符串形式,去掉前2个字节(04),并使用Keccak-256哈希算法生成一个32字节哈希值
pubKeyBytes := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y)[1:]
hash := sha3.Sum256(pubKeyBytes)
// 取哈希值的后20字节,得到以太坊钱包地址
address := hex.EncodeToString(hash[12:])
fmt.Printf("私钥: %x\n", privKey.D)
fmt.Printf("公钥: %x\n", pubKeyBytes)
fmt.Printf("以太坊钱包地址: 0x%s\n", address)
}
读代码:
- 生成密钥对
ecdsa.GenerateKey(elliptic.Curve,io.Reader)
函数生成一个ECDSA的密钥对,接受两个参数:椭圆曲线函数类型和随机数生成器,这里 Curve 选择 P-256(以太坊使用的),随机数生成器选择rand.Reader
,标准库中提供的一个安全的随机数生成器。
函数返回两个值,一个是 ECDSA 私钥,类型为 *ecdsa.Privkey,另一个是error
类型,这里用_
忽略。- 如果错误需要处理
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
// 处理错误,例如打印错误信息或记录日志
fmt.Printf("生成ECDSA密钥对出错:%s\n", err)
return
}
- 私钥 =
privKey.D
, 公钥 =privKey.PublicKey
- 公钥取哈希
- 在以太坊中为了提高公钥的可读性和便利性,方便在网络中传播,采用将公钥转换成字符串的方法,字符串形式的公钥称作公钥地址或简称地址。
在生成钱包地址时,使用的字符串公钥需要减掉没用的前缀字节(0x04),可以减少求解哈希值的计算量。- 前缀字节的作用:区分不同的地址类型
地址类型 | 前缀字节 |
---|---|
主网地址 | 0x00 |
测试网地址 | 0x6F |
Contract 地址 | 0x00 |
EOA 地址 | 0x00 |
数字签名
ECDSA数字签名算法包括三个步骤:
- 密钥生成:通过选择椭圆曲线和一个生成点,选择一个随机数k作为私钥,并计算公钥 K = k G K=kG K=kG。
- 签名:对要签名的数据进行哈希,并使用私钥k对哈希结果进行数字签名,生成一个签名 ( S , R ) (S, R) (S,R)。
- 验证:使用公钥K和签名 ( S , R ) (S, R) (S,R)验证签名的有效性。
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
// 选择椭圆曲线secp256k1和一个生成点
curve := crypto.S256()
pubkey := &ecdsa.PublicKey{Curve: curve}
// 生成随机私钥
privkey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Fatal(err)
}
// 计算公钥
pubkey.X, pubkey.Y = curve.ScalarBaseMult(privkey.D.Bytes())
// 要签名的数据
msg := []byte("hello world")
// 哈希数据
hash := crypto.Keccak256Hash(msg)
// 对哈希结果进行数字签名
r, s, err := ecdsa.Sign(rand.Reader, privkey, hash.Bytes())
if err != nil {
log.Fatal(err)
}
// 验证签名的有效性
isValid := ecdsa.Verify(pubkey, hash.Bytes(), r, s)
fmt.Printf("Signature is valid: %v\n", isValid)
// 将签名(S, R)组成字节数组
signature := make([]byte, 64)
rbytes := r.Bytes()
sbytes := s.Bytes()
copy(signature[32-len(rbytes):32], rbytes)
copy(signature[64-len(sbytes):64], sbytes)
// 验证签名的有效性
pubkeyHash := crypto.Keccak256Hash(pubkey.X.Bytes(), pubkey.Y.Bytes())
address := crypto.PubkeyToAddress(*pubkey)
fmt.Printf("Public key: %x\n", pubkeyHash)
fmt.Printf("Address: %s\n", address.Hex())
isValid = crypto.VerifySignature(pubkey, hash.Bytes(), signature)
fmt.Printf("Signature is valid: %v\n", isValid)
}
读代码:
- 选择 secp256k1 椭圆曲线和一个生成点。
- 使用
ecdsa.GenerateKey()
方法生成一个随机私钥和公钥。 - 对要签名的数据进行哈希,并使用
ecdsa.Sign()
方法对哈希结果进行数字签名,生成一个签名。 - 使用公钥和签名验证签名的有效性,即使用
ecdsa.Verify()
方法。在验证签名时,我们还可以使用crypto.VerifySignature()
方法来验证签名的有效性。 - 最后,此代码还输出了公钥的哈希值和地址。
密钥协商
在被攻击者窥探的情况下,客户端与服务器依靠密钥协商机制生成加密应用层数据的密钥(也称“会话密钥”)。解决身份认证前提下的“偷窥”问题。
-
对称密钥协商
-
定义:通讯双方在没有对方任何预先信息的情况下在不安全的信道建立共享密钥,后续通信过程中通过该密钥进行加密和解密。数学原理基于求解离散对数问题的复杂性,确保即使通信过程被监听也不会泄露机密信息。
-
过程:Diffie–Hellman 算法实现流程:
- 客户端先连上服务端
- 服务端生成一个随机数 s 作为自己的私钥,然后根据算法参数计算出公钥 S(算法参数通常是固定的)
- 服务端使用某种签名算法把“算法参数(模数p,基数g)和服务端公钥S”作为一个整体进行签名
- 服务端把“算法参数(模数p,基数g)、服务端公钥S、签名”发送给客户端
- 客户端收到后验证签名是否有效
- 客户端生成一个随机数 c 作为自己的私钥,然后根据算法参数计算出公钥 C
- 客户端把 C 发送给服务端
- 客户端和服务端(根据上述 DH 算法)各自计算出 k 作为会话密钥
-
实例:
- Alice和Bob约定使用一个模 p = 23 和 g = 5
- Alice选择一个保密的整数 a = 4作为私钥,计算出公钥 A = g a m o d p A =g^a mod p A=gamodp,将公钥A发送给Bob; A = 5 4 m o d 23 = 4 A = 5^4 mod 23 = 4 A=54mod23=4
- Bob选在一个保密的整数 b = 3,计算出公钥 B = g b m o d p B = g^b mod p B=gbmodp,将公钥B发送给Alice;
- B = 5 3 m o d 23 = 10 B = 5^3 mod 23 = 10 B=53mod23=10
- Alice 计算出共享密钥 s = B a m o d p s = 1 0 4 m o d 23 = 18 s = B^a mod p s = 10^4 mod 23 = 18 s=Bamodps=104mod23=18
- Bob 计算共享密钥 s = A b m o d p s = 4 3 m o d 23 = 18 s = A^b mod p s = 4^3 mod 23 = 18 s=Abmodps=43mod23=18
- Alice和Bob现在就共享一个密钥(s = 18)
- 注:P 得足够大(至少300位);a , b 也应足够大(大于100位);g 得足够小,不然会影响性能。
- 缺点:无法防止中间人攻击,需要配合签名算法使用。
-
-
非对称密钥协商
- TLS/SSL 握手流程:
- 客户端连上服务端
- 服务端发送 CA 证书给客户端
- 客户端验证该证书的可靠性
- 客户端从 CA 证书中取出公钥
- 客户端生成一个随机密钥 k,并用这个公钥加密得到 k’
- 客户端把 k’ 发送给服务端
- 服务端收到 k’ 后用自己的私钥解密得到 k
- 此时双方都得到了密钥 k,协商完成。
- 实例:
- TLS/SSL 握手流程:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
)
func main() {
// 服务端配置
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
fmt.Println("LoadX509KeyPair error:", err)
return
}
config := &tls.Config{Certificates: []tls.Certificate{cert}}
// 监听地址
ln, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("net.Listen error:", err)
return
}
defer ln.Close()
for {
// 等待客户端连接
conn, err := ln.Accept()
if err != nil {
fmt.Println("Accept error:", err)
continue
}
// 创建 TLS 连接
tlsConn := tls.Server(conn, config)
// 开始握手
err = tlsConn.Handshake()
if err != nil {
fmt.Println("Handshake error:", err)
continue
}
// 获取客户端证书
state := tlsConn.ConnectionState()
peerCert := state.PeerCertificates[0]
// 打印客户端证书信息
fmt.Printf("Received client cert:\nSubject: %s\nIssuer: %s\n",
peerCert.Subject, peerCert.Issuer)
// 获取客户端公钥
clientPubKey := peerCert.PublicKey.(*rsa.PublicKey)
// 随机生成对称密钥
key := make([]byte, 16)
_, err = rand.Read(key)
if err != nil {
fmt.Println("rand.Read error:", err)
continue
}
// 使用客户端公钥加密对称密钥
encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, clientPubKey, key)
if err != nil {
fmt.Println("rsa.EncryptPKCS1v15 error:", err)
continue
}
// 发送加密后的对称密钥到客户端
_, err = tlsConn.Write(encryptedKey)
if err != nil {
fmt.Println("tlsConn.Write error:", err)
continue
}
// 关闭连接
tlsConn.Close()
}
}
哈希函数
定义:将任意长度的输入经过变化后得到固定长度的输出,是一种单向密码体制,只有加密过程没有解密。比特币使用的是 SHA-256,以太坊使用的是 Keccak-256(SHA-3-256)。
特性 | 解释 |
---|---|
不可逆性 | 从哈希值推算原始输入信息是不可能的 |
唯一性 | 不同的输入产生的哈希值是不同的,且输出长度一致 |
固定性 | 同样的输入映射的哈希值应该相同 |
散列性 | 输入进行微小的改变应该导致哈希值的巨大变化 |
用途:区块、交易的编号(地址)和内容验证、共识机制中挖矿节点对随机数的搜索与区块散列验证。 |
package main
import (
"fmt"
"crypto/sha3"
)
func main() {
data := []byte("Hello, world!")
hash := sha3.Sum256(data)
fmt.Printf("Hash: %x\n", hash)
}
SHA-3 算法通过使用crypto/sha3
包实现,调用sha3.Sum256()
函数生成哈希值。
crypto/sha3
与golang.org/x/crypto/sha3
的区别:
前者是后者的子集,golang.org/x/xcrypto/sha3
包提供了更多的哈希数选项和其他密码学算法,crypto/sha3
包只提供SHA3-256算法。