完善不同加密方法的使用
This commit is contained in:
+662
-99
@@ -1,119 +1,682 @@
|
||||
// Package rsax 提供 RSA 密钥生成、PEM 编解码、加解密与数字签名的安全封装。
|
||||
//
|
||||
// 设计约定:
|
||||
// - 任何失败路径都返回 error,绝不 panic;
|
||||
// - 加解密默认使用 OAEP + SHA-256,签名默认使用 PSS + SHA-256;
|
||||
// - 二进制数据(密钥、明文、密文、签名)一律使用 []byte;
|
||||
// - 不把私钥、明文或随机数写入日志、标准输出或 error 文本。
|
||||
//
|
||||
// 安全注意事项:
|
||||
// - 密钥长度下限为 MinKeyBits(2048 位),更短的密钥会被拒绝;
|
||||
// - PKCS#1 v1.5 加密(EncryptPKCS1v15)仅为兼容老旧系统保留,新代码应使用 OAEP;
|
||||
// - EncryptOAEPLong 的分块格式自身不携带完整性校验与边界信息,
|
||||
// 在需要抵御密文篡改或重排的场景,应先对明文做 AEAD/签名保护,或只使用单块 EncryptOAEP。
|
||||
package rsax
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
)
|
||||
|
||||
//生成RSA私钥和公钥,保存到文件中
|
||||
// 与下面命令等价
|
||||
// 生成私钥
|
||||
// openssl genrsa -out rsa_private_key.pem 1024
|
||||
// 生成公钥
|
||||
// openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
|
||||
// bits 证书大小 1024/2048
|
||||
func GenerateRSAKey(bits int) {
|
||||
//GenerateKey函数使用随机数据生成器random生成一对具有指定字位数的RSA密钥
|
||||
//Reader是一个全局、共享的密码用强随机数生成器
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//保存私钥
|
||||
//通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
|
||||
X509PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
//使用pem格式对x509输出的内容进行编码
|
||||
//创建文件保存私钥
|
||||
privateFile, err := os.Create("private.pem")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer privateFile.Close()
|
||||
//构建一个pem.Block结构体对象
|
||||
privateBlock := pem.Block{Type: "RSA Private Key", Bytes: X509PrivateKey}
|
||||
//将数据保存到文件
|
||||
pem.Encode(privateFile, &privateBlock)
|
||||
// cryptoHash 是 OAEP 与签名使用的标准库哈希标识,必须与 rsaHash 返回的构造器一致。
|
||||
// 安全注意事项:修改该值会同时改变 OAEP 填充与签名格式,导致与既有数据不兼容。
|
||||
const cryptoHash = crypto.SHA256
|
||||
|
||||
//保存公钥
|
||||
//获取公钥的数据
|
||||
publicKey := privateKey.PublicKey
|
||||
//X509对公钥编码
|
||||
X509PublicKey, err := x509.MarshalPKIXPublicKey(&publicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// 密钥长度等关键参数集中定义;调用方应直接引用这些常量,不要硬编码数字。
|
||||
const (
|
||||
// MinKeyBits 是允许**生成**的最小 RSA 密钥长度(位)。
|
||||
// 安全注意事项:小于 2048 位的 RSA 密钥已不满足现代安全强度,
|
||||
// 所有生成入口都会拒绝更小的值。
|
||||
MinKeyBits = 2048
|
||||
|
||||
// MinUsableKeyBits 是允许**解析和使用既有密钥**的最小长度(位)。
|
||||
//
|
||||
// 它低于 MinKeyBits 是刻意的:旧系统里真实存在 1024 位密钥,若连解析都
|
||||
// 拒绝,持有这类密钥的调用方将无法解密自己既有的数据——那是把安全策略
|
||||
// 变成了功能性故障。因此本库只在**生成**环节强制 2048 位,对既有密钥允许
|
||||
// 使用到该下限,并通过 IsWeakPrivateKey / IsWeakPublicKey 让调用方自行判断
|
||||
// 是否要告警或拒绝。
|
||||
//
|
||||
// 低于 1024 位的 RSA 已可被低成本分解,无论何种理由都不接受。
|
||||
// 该值与 Go 标准库自身的下限一致(标准库已拒绝生成低于 1024 位的密钥)。
|
||||
MinUsableKeyBits = 1024
|
||||
|
||||
// WeakKeyBits 是"弱密钥"的判定阈值(位):低于该值即视为不满足当前建议强度。
|
||||
// IsWeakPrivateKey / IsWeakPublicKey 以此为判断依据。
|
||||
WeakKeyBits = 2048
|
||||
|
||||
// DefaultKeyBits 是未显式指定长度时使用的默认 RSA 密钥长度(位)。
|
||||
// 安全注意事项:2048 位是当前通用场景的最低推荐值,
|
||||
// 需要长期保密或高价值数据时请显式指定更大长度。
|
||||
DefaultKeyBits = 2048
|
||||
|
||||
// MaxKeyBits 是允许生成的最大 RSA 密钥长度(位)。
|
||||
// 安全注意事项:设置上限用于防止误传超大位宽导致生成过程长时间占用 CPU 与内存。
|
||||
MaxKeyBits = 8192
|
||||
|
||||
// OAEPHash 是 OAEP 加解密与签名默认使用的哈希算法名。
|
||||
// 安全注意事项:SHA-256 与 2048 位密钥组合是当前推荐配置;
|
||||
// 若生成 8192 位等超大密钥并需要更高强度,可另行封装 SHA-512 版本。
|
||||
OAEPHash = "SHA-256"
|
||||
)
|
||||
|
||||
// IsWeakPrivateKey 报告私钥是否弱于当前建议强度(WeakKeyBits)。
|
||||
//
|
||||
// 用于在解析既有密钥后决定是否告警或强制轮换;nil 或参数缺失一律视为弱密钥。
|
||||
func IsWeakPrivateKey(priv *rsa.PrivateKey) bool {
|
||||
if priv == nil || priv.N == nil {
|
||||
return true
|
||||
}
|
||||
//pem格式编码
|
||||
//创建用于保存公钥的文件
|
||||
publicFile, err := os.Create("public.pem")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer publicFile.Close()
|
||||
//创建一个pem.Block结构体对象
|
||||
publicBlock := pem.Block{Type: "RSA Public Key", Bytes: X509PublicKey}
|
||||
//保存到文件
|
||||
pem.Encode(publicFile, &publicBlock)
|
||||
return priv.N.BitLen() < WeakKeyBits
|
||||
}
|
||||
|
||||
// RSA加密
|
||||
// plainText 要加密的数据
|
||||
// path 公钥匙文件地址
|
||||
func RSA_Encrypt(plainText []byte, path string) []byte {
|
||||
//打开文件
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// IsWeakPublicKey 报告公钥是否弱于当前建议强度(WeakKeyBits)。
|
||||
func IsWeakPublicKey(pub *rsa.PublicKey) bool {
|
||||
if pub == nil || pub.N == nil {
|
||||
return true
|
||||
}
|
||||
defer file.Close()
|
||||
//读取文件的内容
|
||||
info, _ := file.Stat()
|
||||
buf := make([]byte, info.Size())
|
||||
file.Read(buf)
|
||||
//pem解码
|
||||
block, _ := pem.Decode(buf)
|
||||
//x509解码
|
||||
|
||||
publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//类型断言
|
||||
publicKey := publicKeyInterface.(*rsa.PublicKey)
|
||||
//对明文进行加密
|
||||
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plainText)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//返回密文
|
||||
return cipherText
|
||||
return pub.N.BitLen() < WeakKeyBits
|
||||
}
|
||||
|
||||
//RSA解密
|
||||
// cipherText 需要解密的byte数据
|
||||
// path 私钥文件路径
|
||||
func RSA_Decrypt(cipherText []byte, path string) []byte {
|
||||
//打开文件
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
const (
|
||||
// pemTypePublicKey 是 PKIX(SubjectPublicKeyInfo)公钥的 PEM 标签。
|
||||
pemTypePublicKey = "PUBLIC KEY"
|
||||
// pemTypePKCS1PublicKey 是 PKCS#1 公钥的 PEM 标签。
|
||||
pemTypePKCS1PublicKey = "RSA PUBLIC KEY"
|
||||
// pemTypePrivateKey 是 PKCS#8 私钥的 PEM 标签。
|
||||
pemTypePrivateKey = "PRIVATE KEY"
|
||||
// pemTypePKCS1PrivateKey 是 PKCS#1 私钥的 PEM 标签。
|
||||
pemTypePKCS1PrivateKey = "RSA PRIVATE KEY"
|
||||
)
|
||||
|
||||
// 包级哨兵错误,调用方可用 errors.Is 判断失败类别,无需解析错误文本。
|
||||
var (
|
||||
// ErrInvalidKeySize 表示 RSA 密钥长度不在允许范围内。
|
||||
ErrInvalidKeySize = errors.New("rsax: invalid RSA key size")
|
||||
// ErrNilKey 表示传入了 nil 密钥或缺少必要参数。
|
||||
ErrNilKey = errors.New("rsax: nil RSA key")
|
||||
// ErrInvalidPEM 表示 PEM 输入为空、无法解码或标签不受支持。
|
||||
ErrInvalidPEM = errors.New("rsax: invalid PEM data")
|
||||
// ErrInvalidCiphertext 表示密文为空、长度不合法或无法解密。
|
||||
ErrInvalidCiphertext = errors.New("rsax: invalid ciphertext")
|
||||
// ErrInvalidSignature 表示签名验证失败。
|
||||
ErrInvalidSignature = errors.New("rsax: signature verification failed")
|
||||
// ErrEmptyPath 表示文件路径为空。
|
||||
ErrEmptyPath = errors.New("rsax: empty file path")
|
||||
)
|
||||
|
||||
// 私钥文件、公钥文件的权限位(POSIX 语义)。
|
||||
const (
|
||||
privateKeyFileMode os.FileMode = 0o600
|
||||
publicKeyFileMode os.FileMode = 0o644
|
||||
)
|
||||
|
||||
// rsaHash 返回加解密与签名使用的哈希构造器。
|
||||
// 安全注意事项:返回值必须与 OAEP 的单块长度计算保持一致,
|
||||
// 修改此处会改变 padding 结构,导致新旧版本密文/签名互不兼容。
|
||||
func rsaHash() hash.Hash { return sha256.New() }
|
||||
|
||||
// hashSize 是默认哈希(SHA-256)的输出长度(字节)。
|
||||
func hashSize() int { return sha256.Size }
|
||||
|
||||
// checkKeySize 校验请求的密钥长度并补充空值情况说明。
|
||||
//
|
||||
// bits 为期望的密钥长度(位)。安全注意事项:只有 [MinKeyBits, MaxKeyBits]
|
||||
// 区间内的长度才被接受,拒绝弱密钥与可能耗尽资源的超大位宽。
|
||||
func checkKeySize(bits int) error {
|
||||
if bits < MinKeyBits {
|
||||
return fmt.Errorf("%w: need at least %d bits, got %d: 弱密钥会被拒绝", ErrInvalidKeySize, MinKeyBits, bits)
|
||||
}
|
||||
defer file.Close()
|
||||
//获取文件内容
|
||||
info, _ := file.Stat()
|
||||
buf := make([]byte, info.Size())
|
||||
file.Read(buf)
|
||||
//pem解码
|
||||
block, _ := pem.Decode(buf)
|
||||
//X509解码
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if bits > MaxKeyBits {
|
||||
return fmt.Errorf("%w: need at most %d bits, got %d", ErrInvalidKeySize, MaxKeyBits, bits)
|
||||
}
|
||||
//对密文进行解密
|
||||
plainText, _ := rsa.DecryptPKCS1v15(rand.Reader, privateKey, cipherText)
|
||||
//返回明文
|
||||
return plainText
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPrivateKey 校验私钥非 nil 且长度不低于可使用的下限。
|
||||
//
|
||||
// priv 为待校验的私钥。这里用的是 MinUsableKeyBits 而非 MinKeyBits:本函数服务
|
||||
// 于"使用既有密钥"的路径(解析、加解密、签名),允许 2048 位以下的旧密钥继续
|
||||
// 工作,只拒绝低于绝对下限的密钥。是否对弱密钥告警请用 IsWeakPrivateKey。
|
||||
func checkPrivateKey(priv *rsa.PrivateKey) error {
|
||||
if priv == nil {
|
||||
return fmt.Errorf("%w: private key", ErrNilKey)
|
||||
}
|
||||
if priv.N == nil || priv.D == nil {
|
||||
return fmt.Errorf("%w: private key 缺少 N/D 参数", ErrNilKey)
|
||||
}
|
||||
if bits := priv.N.BitLen(); bits < MinUsableKeyBits {
|
||||
return fmt.Errorf("%w: private key 只有 %d 位, 少于可使用下限 %d 位",
|
||||
ErrInvalidKeySize, bits, MinUsableKeyBits)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPublicKey 校验公钥非 nil 且长度不低于可使用的下限。
|
||||
//
|
||||
// pub 为待校验的公钥。与 checkPrivateKey 同理,这里允许既有弱公钥继续使用,
|
||||
// 只拒绝低于 MinUsableKeyBits 的密钥。
|
||||
func checkPublicKey(pub *rsa.PublicKey) error {
|
||||
if pub == nil {
|
||||
return fmt.Errorf("%w: public key", ErrNilKey)
|
||||
}
|
||||
if pub.N == nil {
|
||||
return fmt.Errorf("%w: public key 缺少 N 参数", ErrNilKey)
|
||||
}
|
||||
if bits := pub.N.BitLen(); bits < MinUsableKeyBits {
|
||||
return fmt.Errorf("%w: public key 只有 %d 位, 少于可使用下限 %d 位",
|
||||
ErrInvalidKeySize, bits, MinUsableKeyBits)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateKey 生成一对 RSA 密钥,返回私钥(公钥可通过 priv.PublicKey 获取)。
|
||||
//
|
||||
// bits 为密钥长度(位),必须位于 [MinKeyBits, MaxKeyBits] 区间内;
|
||||
// 传入 0 之外的越界值都会返回包装了错误原因的 error。
|
||||
// 安全注意事项:随机源固定为 crypto/rand,密钥生成失败时不会返回半成品密钥。
|
||||
func GenerateKey(bits int) (*rsa.PrivateKey, error) {
|
||||
if err := checkKeySize(bits); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 生成 %d 位 RSA 密钥失败: %w", bits, err)
|
||||
}
|
||||
if err := priv.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("rsax: 生成的 RSA 私钥未通过自检: %w", err)
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPairPEM 生成一对 RSA 密钥并直接返回 PEM 编码结果。
|
||||
//
|
||||
// bits 为密钥长度(位),取值范围同 GenerateKey。私钥使用 PKCS#8 编码,
|
||||
// 公钥使用 PKIX 编码。返回的切片仅在内存中流转,调用方负责在不再需要时清零。
|
||||
// 安全注意事项:私钥 PEM 属于高度敏感数据,不要写入日志或通过网络明文传输。
|
||||
func GenerateKeyPairPEM(bits int) (privatePEM, publicPEM []byte, err error) {
|
||||
priv, err := GenerateKey(bits)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
privatePEM, err = MarshalPrivateKeyPEM(priv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
publicPEM, err = MarshalPublicKeyPEM(&priv.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return privatePEM, publicPEM, nil
|
||||
}
|
||||
|
||||
// MarshalPrivateKeyPEM 把私钥编码为 PKCS#8 的 PEM 数据。
|
||||
//
|
||||
// priv 为待编码私钥,必须非 nil 且长度不小于 MinKeyBits。
|
||||
// 安全注意事项:返回内容包含私钥明文材料,等价于私钥本身,须妥善保管。
|
||||
func MarshalPrivateKeyPEM(priv *rsa.PrivateKey) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 编码 PKCS#8 私钥失败: %w", err)
|
||||
}
|
||||
block := &pem.Block{Type: pemTypePrivateKey, Bytes: der}
|
||||
out := pem.EncodeToMemory(block)
|
||||
if out == nil {
|
||||
return nil, fmt.Errorf("rsax: 生成私钥 PEM 失败: %w", ErrInvalidPEM)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MarshalPublicKeyPEM 把公钥编码为 PKIX(SubjectPublicKeyInfo)的 PEM 数据。
|
||||
//
|
||||
// pub 为待编码公钥,必须非 nil 且长度不小于 MinKeyBits。
|
||||
// 安全注意事项:公钥可公开分发,但仍应确认其来源真实,避免被替换为攻击者公钥。
|
||||
func MarshalPublicKeyPEM(pub *rsa.PublicKey) ([]byte, error) {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 编码 PKIX 公钥失败: %w", err)
|
||||
}
|
||||
block := &pem.Block{Type: pemTypePublicKey, Bytes: der}
|
||||
out := pem.EncodeToMemory(block)
|
||||
if out == nil {
|
||||
return nil, fmt.Errorf("rsax: 生成公钥 PEM 失败: %w", ErrInvalidPEM)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ParsePrivateKeyPEM 从 PEM 数据解析 RSA 私钥,同时支持 PKCS#1 与 PKCS#8。
|
||||
//
|
||||
// data 为 PEM 文本(通常来自文件或配置),不得为空。
|
||||
// 解析失败(空输入、非 PEM、DER 损坏、不是 RSA 私钥、密钥长度不足)一律返回 error。
|
||||
// 安全注意事项:不支持的算法(如 ECDSA/Ed25519 私钥)会被明确拒绝,绝不降级或猜测;
|
||||
// 调用方应把解析失败的 PEM 视为不可信输入,不要回显其内容。
|
||||
func ParsePrivateKeyPEM(data []byte) (*rsa.PrivateKey, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("%w: 输入为空", ErrInvalidPEM)
|
||||
}
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("%w: 无法解码 PEM(缺少 BEGIN/END 边界或格式非法)", ErrInvalidPEM)
|
||||
}
|
||||
if len(block.Bytes) == 0 {
|
||||
return nil, fmt.Errorf("%w: PEM 块内容为空", ErrInvalidPEM)
|
||||
}
|
||||
|
||||
var (
|
||||
priv *rsa.PrivateKey
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
switch block.Type {
|
||||
case pemTypePKCS1PrivateKey:
|
||||
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
case pemTypePrivateKey:
|
||||
var key any
|
||||
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err == nil {
|
||||
priv, ok = key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: PKCS#8 私钥不是 RSA 类型(%T)", ErrInvalidPEM, key)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: 不支持的私钥 PEM 标签 %q", ErrInvalidPEM, block.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 解析私钥失败: %w", err)
|
||||
}
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := priv.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("rsax: 私钥参数不合法: %w", err)
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// ParsePublicKeyPEM 从 PEM 数据解析 RSA 公钥,同时支持 PKIX 与 PKCS#1。
|
||||
//
|
||||
// data 为 PEM 文本(通常来自文件或配置),不得为空。
|
||||
// 解析失败(空输入、非 PEM、DER 损坏、不是 RSA 公钥、密钥长度不足)一律返回 error。
|
||||
// 安全注意事项:非 RSA 公钥会被拒绝;调用方应通过可信渠道获取公钥文本,
|
||||
// 否则可能被中间人替换导致加密内容泄露。
|
||||
func ParsePublicKeyPEM(data []byte) (*rsa.PublicKey, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("%w: 输入为空", ErrInvalidPEM)
|
||||
}
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("%w: 无法解码 PEM(缺少 BEGIN/END 边界或格式非法)", ErrInvalidPEM)
|
||||
}
|
||||
if len(block.Bytes) == 0 {
|
||||
return nil, fmt.Errorf("%w: PEM 块内容为空", ErrInvalidPEM)
|
||||
}
|
||||
|
||||
var (
|
||||
pub *rsa.PublicKey
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
switch block.Type {
|
||||
case pemTypePublicKey:
|
||||
var key any
|
||||
key, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err == nil {
|
||||
pub, ok = key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: PKIX 公钥不是 RSA 类型(%T)", ErrInvalidPEM, key)
|
||||
}
|
||||
}
|
||||
case pemTypePKCS1PublicKey:
|
||||
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: 不支持的公钥 PEM 标签 %q", ErrInvalidPEM, block.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 解析公钥失败: %w", err)
|
||||
}
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
// LoadPrivateKey 从文件读取并解析 RSA 私钥(PKCS#1 或 PKCS#8)。
|
||||
//
|
||||
// path 为私钥 PEM 文件路径,不得为空;读取失败或内容不合法都返回 error。
|
||||
// 安全注意事项:调用方应保证该文件权限为 0600 且不被其他用户或进程读写。
|
||||
func LoadPrivateKey(path string) (*rsa.PrivateKey, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("%w: 私钥路径为空", ErrEmptyPath)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 读取私钥文件失败: %w", err)
|
||||
}
|
||||
defer clear(data)
|
||||
priv, err := ParsePrivateKeyPEM(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 解析私钥文件 %s 失败: %w", path, err)
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// LoadPublicKey 从文件读取并解析 RSA 公钥(PKIX 或 PKCS#1)。
|
||||
//
|
||||
// path 为公钥 PEM 文件路径,不得为空;读取失败或内容不合法都返回 error。
|
||||
// 安全注意事项:公钥文件可能被替换,加载后应结合证书链或指纹校验其真实性。
|
||||
func LoadPublicKey(path string) (*rsa.PublicKey, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("%w: 公钥路径为空", ErrEmptyPath)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 读取公钥文件失败: %w", err)
|
||||
}
|
||||
pub, err := ParsePublicKeyPEM(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 解析公钥文件 %s 失败: %w", path, err)
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
// SaveKeyPair 生成一对 RSA 密钥并写入指定的 PEM 文件。
|
||||
//
|
||||
// privatePath、publicPath 分别为私钥、公钥输出路径,均不得为空,且不能相同;
|
||||
// bits 取值范围同 GenerateKey。私钥文件权限 0600,公钥文件权限 0644。
|
||||
// 安全注意事项:若私钥路径已存在会被覆盖,请先在应用层确认不会误删既有密钥;
|
||||
// 在 Windows 上 0600/0644 仅作尽力而为的权限设置,实际访问控制依赖 ACL。
|
||||
func SaveKeyPair(privatePath, publicPath string, bits int) error {
|
||||
if privatePath == "" || publicPath == "" {
|
||||
return fmt.Errorf("%w: 私钥或公钥路径为空", ErrEmptyPath)
|
||||
}
|
||||
if privatePath == publicPath {
|
||||
return fmt.Errorf("%w: 私钥与公钥路径相同 %s", ErrEmptyPath, privatePath)
|
||||
}
|
||||
privatePEM, publicPEM, err := GenerateKeyPairPEM(bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clear(privatePEM)
|
||||
if err := os.WriteFile(privatePath, privatePEM, privateKeyFileMode); err != nil {
|
||||
return fmt.Errorf("rsax: 写入私钥文件 %s 失败: %w", privatePath, err)
|
||||
}
|
||||
if err := os.WriteFile(publicPath, publicPEM, publicKeyFileMode); err != nil {
|
||||
return fmt.Errorf("rsax: 写入公钥文件 %s 失败: %w", publicPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxOAEPMessageLen 返回单块 OAEP 加密可容纳的最大明文长度。
|
||||
//
|
||||
// k 为 RSA 模长(字节);哈希长度由 hashSize 决定。
|
||||
// 计算依据 PKCS#1 v2.2:k - 2*hLen - 2。
|
||||
func maxOAEPMessageLen(k int) int {
|
||||
return k - 2*hashSize() - 2
|
||||
}
|
||||
|
||||
// EncryptOAEP 使用 RSA-OAEP(SHA-256) 加密单块明文。
|
||||
//
|
||||
// pub 为接收方公钥,plaintext 为明文,长度不得超过 len(pub)-2*32-2 字节。
|
||||
// 明文过长时返回明确错误,此时可改用 EncryptOAEPLong。
|
||||
// 安全注意事项:OAEP 只保证机密性,不提供完整性;密文被篡改后解密会失败,
|
||||
// 但攻击者仍可观察失败与否,需要抵抗选择密文攻击时请配合 AEAD 或签名使用。
|
||||
func EncryptOAEP(pub *rsa.PublicKey, plaintext []byte) ([]byte, error) {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxLen := maxOAEPMessageLen(pub.Size())
|
||||
if len(plaintext) > maxLen {
|
||||
return nil, fmt.Errorf("rsax: 明文长度 %d 超过单块 OAEP 上限 %d 字节, 请改用 EncryptOAEPLong", len(plaintext), maxLen)
|
||||
}
|
||||
ciphertext, err := rsa.EncryptOAEP(rsaHash(), rand.Reader, pub, plaintext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: OAEP 加密失败: %w", err)
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptOAEP 解密单块 RSA-OAEP(SHA-256) 密文。
|
||||
//
|
||||
// priv 为接收方私钥,必须在长度上不小于 MinKeyBits;
|
||||
// ciphertext 必须是恰好 len(priv)-字节的单块密文,否则返回错误。
|
||||
// 安全注意事项:解密失败的具体原因(长度错误、padding 非法)不会泄露明文信息,
|
||||
// 但调用方不应把底层错误细节返回给远端攻击者。
|
||||
func DecryptOAEP(priv *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, fmt.Errorf("%w: 密文为空", ErrInvalidCiphertext)
|
||||
}
|
||||
if k := priv.Size(); len(ciphertext) != k {
|
||||
return nil, fmt.Errorf("%w: 密文长度 %d 不是密钥长度 %d", ErrInvalidCiphertext, len(ciphertext), k)
|
||||
}
|
||||
plaintext, err := rsa.DecryptOAEP(rsaHash(), rand.Reader, priv, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: OAEP 解密失败: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptOAEPLong 使用 RSA-OAEP(SHA-256) 分块加密任意长度的明文。
|
||||
//
|
||||
// pub 为接收方公钥,plaintext 为明文,可为空(返回空密文与 nil);
|
||||
// 每块明文长度上限为 k-2*hLen-2(k = pub.Size(),hLen = SHA-256 输出长度),
|
||||
// 逐块调用 EncryptOAEP 后顺序拼接,密文总长度为 k 的整数倍。
|
||||
// 安全注意事项:分块格式不含块边界以外的元数据,也不提供跨块完整性,
|
||||
// 攻击者可能重排或删除整块密文而不被检测;如需抗篡改请先做 AEAD/签名,
|
||||
// 或仅对不超过单块上限的明文使用 EncryptOAEP。
|
||||
func EncryptOAEPLong(pub *rsa.PublicKey, plaintext []byte) ([]byte, error) {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(plaintext) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
k := pub.Size()
|
||||
blockSize := maxOAEPMessageLen(k)
|
||||
if blockSize <= 0 {
|
||||
return nil, fmt.Errorf("rsax: 密钥长度 %d 字节过小, 无法进行 OAEP 分块", k)
|
||||
}
|
||||
blockCount := (len(plaintext) + blockSize - 1) / blockSize
|
||||
ciphertext := make([]byte, 0, blockCount*k)
|
||||
for offset := 0; offset < len(plaintext); offset += blockSize {
|
||||
end := offset + blockSize
|
||||
if end > len(plaintext) {
|
||||
end = len(plaintext)
|
||||
}
|
||||
chunk, err := EncryptOAEP(pub, plaintext[offset:end])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 分块加密第 %d 块失败: %w", offset/blockSize, err)
|
||||
}
|
||||
ciphertext = append(ciphertext, chunk...)
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptOAEPLong 解密由 EncryptOAEPLong 产生的分块密文。
|
||||
//
|
||||
// priv 为接收方私钥,ciphertext 必须是 k 的整数倍(k = priv.Size())且非空,
|
||||
// 否则返回错误;每块独立解密后按顺序拼接还原完整明文。
|
||||
// 安全注意事项:本函数不做跨块完整性校验,无法发现整块的删除、重排或替换;
|
||||
// 若对端使用不同公钥加密相同明文,可能出现块替换攻击,因此请配合上层完整性保护。
|
||||
func DecryptOAEPLong(priv *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, fmt.Errorf("%w: 密文为空", ErrInvalidCiphertext)
|
||||
}
|
||||
k := priv.Size()
|
||||
if len(ciphertext)%k != 0 {
|
||||
return nil, fmt.Errorf("%w: 密文长度 %d 不是密钥长度 %d 的整数倍", ErrInvalidCiphertext, len(ciphertext), k)
|
||||
}
|
||||
blockSize := maxOAEPMessageLen(k)
|
||||
blockCount := len(ciphertext) / k
|
||||
plaintext := make([]byte, 0, blockCount*blockSize)
|
||||
for offset := 0; offset < len(ciphertext); offset += k {
|
||||
chunk, err := DecryptOAEP(priv, ciphertext[offset:offset+k])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: 分块解密第 %d 块失败: %w", offset/k, err)
|
||||
}
|
||||
plaintext = append(plaintext, chunk...)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptPKCS1v15 使用 RSAES-PKCS1-v1_5 加密单块明文。
|
||||
//
|
||||
// pub 为接收方公钥,plaintext 长度不得超过 len(pub)-11 字节。
|
||||
// 安全注意事项:该方案是为兼容老旧系统保留的,存在 Bleichenbacher 类填充预言机风险,
|
||||
// 新代码必须优先使用 EncryptOAEP/EncryptOAEPLong;
|
||||
// 若对端只能识别 PKCS#1 v1.5,请确保解密失败信息不会暴露给攻击者。
|
||||
func EncryptPKCS1v15(pub *rsa.PublicKey, plaintext []byte) ([]byte, error) {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//lint:ignore SA1019 PKCS#1 v1.5 加密自 Go 1.26 起被标准库标记为危险;本函数仅为解密既有密文/与旧系统互操作而提供,函数注释已明确建议改用 OAEP。
|
||||
ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, pub, plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: PKCS#1 v1.5 加密失败: %w", err)
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptPKCS1v15 解密单块 RSAES-PKCS1-v1_5 密文。
|
||||
//
|
||||
// priv 为接收方私钥,ciphertext 必须恰好为 len(priv) 字节。
|
||||
// 安全注意事项:解密错误(包含填充校验失败)会被完整包装返回,
|
||||
// 调用方绝不能把该错误直接回显给远端,否则会形成填充预言机导致明文泄露;
|
||||
// 新代码请使用 DecryptOAEP。
|
||||
func DecryptPKCS1v15(priv *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, fmt.Errorf("%w: 密文为空", ErrInvalidCiphertext)
|
||||
}
|
||||
if k := priv.Size(); len(ciphertext) != k {
|
||||
return nil, fmt.Errorf("%w: 密文长度 %d 不是密钥长度 %d", ErrInvalidCiphertext, len(ciphertext), k)
|
||||
}
|
||||
//lint:ignore SA1019 PKCS#1 v1.5 解密自 Go 1.26 起被标准库标记为危险(错误信息会泄漏信息、可能构成 padding oracle);本函数仅为解密既有密文而提供,函数注释已明确建议改用 OAEP。
|
||||
plaintext, err := rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: PKCS#1 v1.5 解密失败: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// SignPSS 使用 RSASSA-PSS(SHA-256) 对消息签名。
|
||||
//
|
||||
// priv 为签名私钥,message 为待签名消息原文(函数内部完成哈希)。
|
||||
// 盐长度固定为 rsa.PSSSaltLengthEqualsHash(等于哈希输出长度 32 字节),
|
||||
// 这样签名结果长度确定,且符合 RFC 8017 推荐做法。
|
||||
// 安全注意事项:签名私钥泄露等同于身份泄露,请确保私钥以 0600 权限保存;
|
||||
// 本函数对 message 做完整哈希,不要传入已经哈希过的摘要。
|
||||
func SignPSS(priv *rsa.PrivateKey, message []byte) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digest := sha256.Sum256(message)
|
||||
signature, err := rsa.SignPSS(rand.Reader, priv, cryptoHash, digest[:], &rsa.PSSOptions{
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
Hash: cryptoHash,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: PSS 签名失败: %w", err)
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// VerifyPSS 验证 RSASSA-PSS(SHA-256) 签名,成功返回 nil,失败返回非 nil 错误。
|
||||
//
|
||||
// pub 为签名者公钥,message 为原始消息,signature 为 SignPSS 的输出。
|
||||
// 安全注意事项:验证失败(含长度不合法、填充错误、内容不匹配)统一包装
|
||||
// rsa.ErrVerification 返回,调用方可用 errors.Is(err, rsa.ErrVerification) 判断;
|
||||
// 盐长度参数在验证时按自动探测处理,以兼容其他实现产生的合法签名。
|
||||
func VerifyPSS(pub *rsa.PublicKey, message, signature []byte) error {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
return fmt.Errorf("%w: 签名为空", ErrInvalidSignature)
|
||||
}
|
||||
if k := pub.Size(); len(signature) != k {
|
||||
return fmt.Errorf("%w: 签名长度 %d 不是密钥长度 %d", ErrInvalidSignature, len(signature), k)
|
||||
}
|
||||
digest := sha256.Sum256(message)
|
||||
if err := rsa.VerifyPSS(pub, cryptoHash, digest[:], signature, &rsa.PSSOptions{
|
||||
SaltLength: rsa.PSSSaltLengthAuto,
|
||||
Hash: cryptoHash,
|
||||
}); err != nil {
|
||||
return wrapVerifyError("PSS", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignPKCS1v15 使用 RSASSA-PKCS1-v1_5(SHA-256) 对消息签名。
|
||||
//
|
||||
// priv 为签名私钥,message 为待签名消息原文(函数内部完成哈希)。
|
||||
// 安全注意事项:PKCS#1 v1.5 签名本身仍属可用方案,但缺乏 PSS 的可证明安全性;
|
||||
// 新系统建议使用 SignPSS,本函数主要用于与既有系统互通。
|
||||
func SignPKCS1v15(priv *rsa.PrivateKey, message []byte) ([]byte, error) {
|
||||
if err := checkPrivateKey(priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digest := sha256.Sum256(message)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, priv, cryptoHash, digest[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsax: PKCS#1 v1.5 签名失败: %w", err)
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// VerifyPKCS1v15 验证 RSASSA-PKCS1-v1_5(SHA-256) 签名,成功返回 nil,失败返回非 nil 错误。
|
||||
//
|
||||
// pub 为签名者公钥,message 为原始消息,signature 为 SignPKCS1v15 的输出。
|
||||
// 安全注意事项:验证失败统一包装 rsa.ErrVerification 返回,
|
||||
// 调用方不应向未认证的对端区分“签名格式错误”与“签名内容不匹配”。
|
||||
func VerifyPKCS1v15(pub *rsa.PublicKey, message, signature []byte) error {
|
||||
if err := checkPublicKey(pub); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
return fmt.Errorf("%w: 签名为空", ErrInvalidSignature)
|
||||
}
|
||||
if k := pub.Size(); len(signature) != k {
|
||||
return fmt.Errorf("%w: 签名长度 %d 不是密钥长度 %d", ErrInvalidSignature, len(signature), k)
|
||||
}
|
||||
digest := sha256.Sum256(message)
|
||||
if err := rsa.VerifyPKCS1v15(pub, cryptoHash, digest[:], signature); err != nil {
|
||||
return wrapVerifyError("PKCS#1 v1.5", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapVerifyError 统一包装验签失败原因。
|
||||
//
|
||||
// scheme 为方案名(用于错误文本),err 为底层 verify 错误。
|
||||
// 安全注意事项:不把底层库的细节暴露给未认证对端,同时保留 rsa.ErrVerification
|
||||
// 的错误链,便于调用方用 errors.Is(err, rsa.ErrVerification) 精确判断“签名不匹配”。
|
||||
// 这里刻意不回显签名或消息内容,避免敏感数据进入日志。
|
||||
func wrapVerifyError(scheme string, err error) error {
|
||||
if errors.Is(err, rsa.ErrVerification) {
|
||||
return fmt.Errorf("%w: %s 验签失败: %w", ErrInvalidSignature, scheme, rsa.ErrVerification)
|
||||
}
|
||||
return fmt.Errorf("%w: %s 验签失败: %w", ErrInvalidSignature, scheme, err)
|
||||
}
|
||||
|
||||
+766
-9
@@ -1,19 +1,776 @@
|
||||
package rsax_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/rsax"
|
||||
)
|
||||
|
||||
func TestRsa(t *testing.T) {
|
||||
//加密
|
||||
data := []byte("hello world")
|
||||
encrypt := rsax.RSA_Encrypt(data, "public.pem")
|
||||
fmt.Println(string(encrypt))
|
||||
// testKeyBits 是测试用密钥长度,取安全下限以缩短密钥生成时间。
|
||||
const testKeyBits = rsax.MinKeyBits
|
||||
|
||||
// 解密
|
||||
decrypt := rsax.RSA_Decrypt(encrypt, "private.pem")
|
||||
fmt.Println(string(decrypt))
|
||||
var (
|
||||
sharedKeyOnce sync.Once
|
||||
sharedKey *rsa.PrivateKey
|
||||
sharedKeyErr error
|
||||
|
||||
otherKeyOnce sync.Once
|
||||
otherKey *rsa.PrivateKey
|
||||
otherKeyErr error
|
||||
)
|
||||
|
||||
// testKey 返回测试共享的 RSA 私钥(进程内只生成一次,避免重复的密钥生成开销)。
|
||||
// 测试辅助函数不会 panic:失败通过 t.Fatalf 报告。
|
||||
func testKey(t *testing.T) *rsa.PrivateKey {
|
||||
t.Helper()
|
||||
sharedKeyOnce.Do(func() {
|
||||
sharedKey, sharedKeyErr = rsax.GenerateKey(testKeyBits)
|
||||
})
|
||||
if sharedKeyErr != nil {
|
||||
t.Fatalf("生成测试密钥失败: %v", sharedKeyErr)
|
||||
}
|
||||
return sharedKey
|
||||
}
|
||||
|
||||
// otherTestKey 返回另一把与 testKey 无关的 RSA 私钥,用于“错误密钥”负向用例。
|
||||
func otherTestKey(t *testing.T) *rsa.PrivateKey {
|
||||
t.Helper()
|
||||
otherKeyOnce.Do(func() {
|
||||
otherKey, otherKeyErr = rsax.GenerateKey(testKeyBits)
|
||||
})
|
||||
if otherKeyErr != nil {
|
||||
t.Fatalf("生成第二把测试密钥失败: %v", otherKeyErr)
|
||||
}
|
||||
return otherKey
|
||||
}
|
||||
|
||||
// requireNoPanic 断言 fn 在失败路径上只返回 error,绝不 panic。
|
||||
func requireNoPanic(t *testing.T, what string, fn func()) {
|
||||
t.Helper()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("%s 发生 panic: %v", what, r)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
|
||||
// randomBytes 生成指定长度的随机数据作为测试明文。
|
||||
func randomBytes(t *testing.T, n int) []byte {
|
||||
t.Helper()
|
||||
buf := make([]byte, n)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
t.Fatalf("生成随机测试数据失败: %v", err)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// ecdsaTestKey 生成一把 ECDSA 密钥,用于构造“非 RSA 密钥”的负向用例。
|
||||
func ecdsaTestKey(t *testing.T) *ecdsa.PrivateKey {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("生成 ECDSA 测试密钥失败: %v", err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// xorByteAt 翻转 data[pos] 的一个比特位,用于篡改密文/签名。
|
||||
func xorByteAt(t *testing.T, data []byte, pos int) []byte {
|
||||
t.Helper()
|
||||
if pos < 0 || pos >= len(data) {
|
||||
t.Fatalf("篡改位置 %d 越界(长度 %d)", pos, len(data))
|
||||
}
|
||||
out := make([]byte, len(data))
|
||||
copy(out, data)
|
||||
out[pos] ^= 0x01
|
||||
return out
|
||||
}
|
||||
|
||||
// TestGenerateKeyAndPEMRoundTrip 覆盖密钥生成、PEM 往返与公私钥一致性。
|
||||
func TestGenerateKeyAndPEMRoundTrip(t *testing.T) {
|
||||
priv, err := rsax.GenerateKey(rsax.DefaultKeyBits)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey(%d) 失败: %v", rsax.DefaultKeyBits, err)
|
||||
}
|
||||
if got := priv.N.BitLen(); got != rsax.DefaultKeyBits {
|
||||
t.Fatalf("密钥位宽 = %d, 期望 %d", got, rsax.DefaultKeyBits)
|
||||
}
|
||||
|
||||
privatePEM, err := rsax.MarshalPrivateKeyPEM(priv)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalPrivateKeyPEM 失败: %v", err)
|
||||
}
|
||||
publicPEM, err := rsax.MarshalPublicKeyPEM(&priv.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalPublicKeyPEM 失败: %v", err)
|
||||
}
|
||||
if block, _ := pem.Decode(privatePEM); block == nil || block.Type != "PRIVATE KEY" {
|
||||
t.Fatalf("私钥 PEM 标签不是 PRIVATE KEY: %#v", block)
|
||||
}
|
||||
if block, _ := pem.Decode(publicPEM); block == nil || block.Type != "PUBLIC KEY" {
|
||||
t.Fatalf("公钥 PEM 标签不是 PUBLIC KEY: %#v", block)
|
||||
}
|
||||
|
||||
parsedPriv, err := rsax.ParsePrivateKeyPEM(privatePEM)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePrivateKeyPEM 失败: %v", err)
|
||||
}
|
||||
if parsedPriv.D.Cmp(priv.D) != 0 || parsedPriv.N.Cmp(priv.N) != 0 {
|
||||
t.Fatal("私钥 PEM 往返后参数不一致")
|
||||
}
|
||||
parsedPub, err := rsax.ParsePublicKeyPEM(publicPEM)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePublicKeyPEM 失败: %v", err)
|
||||
}
|
||||
if !parsedPub.Equal(&priv.PublicKey) {
|
||||
t.Fatal("公钥 PEM 往返后与私钥内公钥不一致")
|
||||
}
|
||||
if !parsedPub.Equal(&parsedPriv.PublicKey) {
|
||||
t.Fatal("公钥 PEM 与私钥 PEM 导出的公钥不一致")
|
||||
}
|
||||
|
||||
// GenerateKeyPairPEM 直接产出的 PEM 也必须可解析且互相匹配。
|
||||
genPrivatePEM, genPublicPEM, err := rsax.GenerateKeyPairPEM(testKeyBits)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPairPEM(%d) 失败: %v", testKeyBits, err)
|
||||
}
|
||||
genPriv, err := rsax.ParsePrivateKeyPEM(genPrivatePEM)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 GenerateKeyPairPEM 私钥失败: %v", err)
|
||||
}
|
||||
genPub, err := rsax.ParsePublicKeyPEM(genPublicPEM)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 GenerateKeyPairPEM 公钥失败: %v", err)
|
||||
}
|
||||
if !genPub.Equal(&genPriv.PublicKey) {
|
||||
t.Fatal("GenerateKeyPairPEM 的公私钥不匹配")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePrivateKeyPEMAcceptsPKCS1 覆盖 PKCS#1 私钥的兼容解析。
|
||||
func TestParsePrivateKeyPEMAcceptsPKCS1(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
pkcs1PEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
})
|
||||
parsed, err := rsax.ParsePrivateKeyPEM(pkcs1PEM)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 PKCS#1 私钥失败: %v", err)
|
||||
}
|
||||
if parsed.D.Cmp(priv.D) != 0 {
|
||||
t.Fatal("PKCS#1 私钥往返后 D 不一致")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePublicKeyPEMAcceptsPKCS1 覆盖 PKCS#1 公钥的兼容解析。
|
||||
func TestParsePublicKeyPEMAcceptsPKCS1(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
pkcs1PEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: x509.MarshalPKCS1PublicKey(&priv.PublicKey),
|
||||
})
|
||||
parsed, err := rsax.ParsePublicKeyPEM(pkcs1PEM)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 PKCS#1 公钥失败: %v", err)
|
||||
}
|
||||
if !parsed.Equal(&priv.PublicKey) {
|
||||
t.Fatal("PKCS#1 公钥往返后不一致")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateKeyRejectsInvalidBits 覆盖密钥长度下限/上限及非 RSA 尺寸的拒绝逻辑。
|
||||
func TestGenerateKeyRejectsInvalidBits(t *testing.T) {
|
||||
for _, bits := range []int{-1024, -1, 0, 1, 512, 1024, 2047, rsax.MinKeyBits - 1, rsax.MaxKeyBits + 1, 16384} {
|
||||
if _, err := rsax.GenerateKey(bits); err == nil {
|
||||
t.Fatalf("GenerateKey(%d) 应当返回错误,实际返回 nil", bits)
|
||||
}
|
||||
}
|
||||
// 弱密钥同样不能进入 PEM 生成路径。
|
||||
for _, bits := range []int{0, 1024} {
|
||||
if _, _, err := rsax.GenerateKeyPairPEM(bits); err == nil {
|
||||
t.Fatalf("GenerateKeyPairPEM(%d) 应当返回错误,实际返回 nil", bits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOAEPRoundTripAndTamper 覆盖 OAEP 往返、错误私钥解密与密文篡改检测。
|
||||
func TestOAEPRoundTripAndTamper(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
message := []byte("RSA-OAEP 往返测试 with ASCII + 中文 + 符号 !@#$%^&*()")
|
||||
|
||||
ciphertext, err := rsax.EncryptOAEP(&priv.PublicKey, message)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptOAEP 失败: %v", err)
|
||||
}
|
||||
if len(ciphertext) != priv.Size() {
|
||||
t.Fatalf("单块密文长度 = %d, 期望 %d", len(ciphertext), priv.Size())
|
||||
}
|
||||
plaintext, err := rsax.DecryptOAEP(priv, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptOAEP 失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, message) {
|
||||
t.Fatalf("解密结果 = %q, 期望 %q", plaintext, message)
|
||||
}
|
||||
|
||||
// 用另一把密钥解密必须失败。
|
||||
if _, err := rsax.DecryptOAEP(otherTestKey(t), ciphertext); err == nil {
|
||||
t.Fatal("使用错误私钥解密应当失败,实际成功")
|
||||
}
|
||||
|
||||
// OAEP 有严格的填充校验:逐字节篡改后解密必须全部失败。
|
||||
k := priv.Size()
|
||||
for _, pos := range []int{0, k / 3, k / 2, k - 3, k - 2, k - 1} {
|
||||
tampered := xorByteAt(t, ciphertext, pos)
|
||||
if _, err := rsax.DecryptOAEP(priv, tampered); err == nil {
|
||||
t.Fatalf("篡改密文第 %d 字节后解密应当失败,实际成功", pos)
|
||||
}
|
||||
}
|
||||
|
||||
// 长度不合法的密文必须被拒绝。
|
||||
for _, bad := range [][]byte{{}, ciphertext[:k-1], append(append([]byte{}, ciphertext...), 0x00)} {
|
||||
if _, err := rsax.DecryptOAEP(priv, bad); err == nil {
|
||||
t.Fatalf("长度 %d 的密文应当被拒绝", len(bad))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncryptOAEPLong 覆盖远超单块上限的分块加解密往返与分块错误处理。
|
||||
func TestEncryptOAEPLong(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
pub := &priv.PublicKey
|
||||
k := priv.Size()
|
||||
maxSingle := k - 2*sha256.Size - 2
|
||||
|
||||
// 分块路径:5000 字节远超单块上限,覆盖“满块 + 末块更短”的组合。
|
||||
message := randomBytes(t, 5000)
|
||||
ciphertext, err := rsax.EncryptOAEPLong(pub, message)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptOAEPLong(5000) 失败: %v", err)
|
||||
}
|
||||
expectedBlocks := (len(message) + maxSingle - 1) / maxSingle
|
||||
if len(ciphertext) != expectedBlocks*k {
|
||||
t.Fatalf("分块密文长度 = %d, 期望 %d(%d 块 × %d)", len(ciphertext), expectedBlocks*k, expectedBlocks, k)
|
||||
}
|
||||
if len(ciphertext) <= k {
|
||||
t.Fatal("5000 字节明文应当产生多块密文")
|
||||
}
|
||||
plaintext, err := rsax.DecryptOAEPLong(priv, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptOAEPLong 失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, message) {
|
||||
t.Fatalf("分块往返结果不一致: 长度 %d vs %d", len(plaintext), len(message))
|
||||
}
|
||||
|
||||
// 单块路径:短明文同样走 EncryptOAEPLong 时只有一块。
|
||||
short := []byte("short")
|
||||
shortCipher, err := rsax.EncryptOAEPLong(pub, short)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptOAEPLong(short) 失败: %v", err)
|
||||
}
|
||||
if len(shortCipher) != k {
|
||||
t.Fatalf("短明文分块密文长度 = %d, 期望单块 %d", len(shortCipher), k)
|
||||
}
|
||||
shortPlain, err := rsax.DecryptOAEPLong(priv, shortCipher)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptOAEPLong(short) 失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(shortPlain, short) {
|
||||
t.Fatalf("短明文分块往返结果 = %q, 期望 %q", shortPlain, short)
|
||||
}
|
||||
|
||||
// 单块上限边界:恰好等于上限可以加密,恰好超过上限必须报错并提示改用 Long 版本。
|
||||
exact := randomBytes(t, maxSingle)
|
||||
if _, err := rsax.EncryptOAEP(pub, exact); err != nil {
|
||||
t.Fatalf("恰好 %d 字节的明文应当可以单块加密: %v", maxSingle, err)
|
||||
}
|
||||
if _, err := rsax.EncryptOAEP(pub, randomBytes(t, maxSingle+1)); err == nil {
|
||||
t.Fatal("超过单块上限的明文应当返回错误")
|
||||
}
|
||||
if _, err := rsax.EncryptOAEPLong(pub, exact); err != nil {
|
||||
t.Fatalf("Long 版本加密边界长度失败: %v", err)
|
||||
}
|
||||
|
||||
// 密文长度不是 k 的整数倍时 DecryptOAEPLong 必须报错。
|
||||
if _, err := rsax.DecryptOAEPLong(priv, ciphertext[:len(ciphertext)-1]); err == nil {
|
||||
t.Fatal("非 k 整数倍长度的密文应当返回错误")
|
||||
}
|
||||
if _, err := rsax.DecryptOAEPLong(priv, nil); err == nil {
|
||||
t.Fatal("空密文应当返回错误")
|
||||
}
|
||||
// 篡改分块密文(最后一字节)后整串解密必须失败。
|
||||
if _, err := rsax.DecryptOAEPLong(priv, xorByteAt(t, ciphertext, len(ciphertext)-1)); err == nil {
|
||||
t.Fatal("篡改分块密文后解密应当失败")
|
||||
}
|
||||
|
||||
// 空明文往返为空密文。
|
||||
emptyCipher, err := rsax.EncryptOAEPLong(pub, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptOAEPLong(nil) 失败: %v", err)
|
||||
}
|
||||
if len(emptyCipher) != 0 {
|
||||
t.Fatalf("空明文密文长度 = %d, 期望 0", len(emptyCipher))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncryptOAEPRejectsOversizedPlaintext 单独固化“单块超长必须报错”的行为。
|
||||
func TestEncryptOAEPRejectsOversizedPlaintext(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
maxSingle := priv.Size() - 2*sha256.Size - 2
|
||||
_, err := rsax.EncryptOAEP(&priv.PublicKey, randomBytes(t, maxSingle+1))
|
||||
if err == nil {
|
||||
t.Fatal("超过单块上限的明文应当返回错误")
|
||||
}
|
||||
if !bytes.Contains([]byte(err.Error()), []byte("EncryptOAEPLong")) {
|
||||
t.Fatalf("错误信息应提示改用 EncryptOAEPLong, 实际: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPKCS1v15EncryptRoundTrip 覆盖兼容用 PKCS#1 v1.5 加解密路径。
|
||||
func TestPKCS1v15EncryptRoundTrip(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
message := []byte("PKCS#1 v1.5 legacy path")
|
||||
|
||||
ciphertext, err := rsax.EncryptPKCS1v15(&priv.PublicKey, message)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptPKCS1v15 失败: %v", err)
|
||||
}
|
||||
plaintext, err := rsax.DecryptPKCS1v15(priv, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptPKCS1v15 失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, message) {
|
||||
t.Fatalf("解密结果 = %q, 期望 %q", plaintext, message)
|
||||
}
|
||||
if _, err := rsax.DecryptPKCS1v15(otherTestKey(t), ciphertext); err == nil {
|
||||
t.Fatal("使用错误私钥解密 PKCS#1 v1.5 密文应当失败")
|
||||
}
|
||||
if _, err := rsax.DecryptPKCS1v15(priv, xorByteAt(t, ciphertext, priv.Size()-1)); err == nil {
|
||||
t.Fatal("篡改 PKCS#1 v1.5 密文后解密应当失败")
|
||||
}
|
||||
// 明文超过 len(pub)-11 时底层实现应返回错误而非 panic。
|
||||
if _, err := rsax.EncryptPKCS1v15(&priv.PublicKey, randomBytes(t, priv.Size()-10)); err == nil {
|
||||
t.Fatal("超过 PKCS#1 v1.5 长度上限的明文应当返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignVerifyPSS 覆盖 PSS 签名/验签往返与篡改检测。
|
||||
func TestSignVerifyPSS(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
message := []byte("需要签名的消息 with 中文")
|
||||
|
||||
signature, err := rsax.SignPSS(priv, message)
|
||||
if err != nil {
|
||||
t.Fatalf("SignPSS 失败: %v", err)
|
||||
}
|
||||
if len(signature) != priv.Size() {
|
||||
t.Fatalf("PSS 签名长度 = %d, 期望 %d", len(signature), priv.Size())
|
||||
}
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, message, signature); err != nil {
|
||||
t.Fatalf("VerifyPSS 对合法签名返回错误: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, []byte("被篡改的消息"), signature); err == nil {
|
||||
t.Fatal("消息被篡改后验签应当失败")
|
||||
}
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, message, xorByteAt(t, signature, len(signature)/2)); err == nil {
|
||||
t.Fatal("签名被篡改后验签应当失败")
|
||||
}
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, message, signature[:len(signature)-1]); err == nil {
|
||||
t.Fatal("签名长度不合法时应当返回错误")
|
||||
}
|
||||
if err := rsax.VerifyPSS(&otherTestKey(t).PublicKey, message, signature); err == nil {
|
||||
t.Fatal("使用错误公钥验签应当失败")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignVerifyPKCS1v15 覆盖 PKCS#1 v1.5 签名/验签往返与篡改检测。
|
||||
func TestSignVerifyPKCS1v15(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
message := []byte("PKCS#1 v1.5 signed message")
|
||||
|
||||
signature, err := rsax.SignPKCS1v15(priv, message)
|
||||
if err != nil {
|
||||
t.Fatalf("SignPKCS1v15 失败: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&priv.PublicKey, message, signature); err != nil {
|
||||
t.Fatalf("VerifyPKCS1v15 对合法签名返回错误: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&priv.PublicKey, []byte("tampered message"), signature); err == nil {
|
||||
t.Fatal("消息被篡改后验签应当失败")
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&priv.PublicKey, message, xorByteAt(t, signature, 0)); err == nil {
|
||||
t.Fatal("签名被篡改后验签应当失败")
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&otherTestKey(t).PublicKey, message, signature); err == nil {
|
||||
t.Fatal("使用错误公钥验签应当失败")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyWrapsErrVerification 确认验签失败包装了 rsa.ErrVerification,便于 errors.Is 判断。
|
||||
func TestVerifyWrapsErrVerification(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
message := []byte("errors.Is 检查")
|
||||
|
||||
pssSig, err := rsax.SignPSS(priv, message)
|
||||
if err != nil {
|
||||
t.Fatalf("SignPSS 失败: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, []byte("other"), pssSig); !errors.Is(err, rsa.ErrVerification) {
|
||||
t.Fatalf("PSS 失败应包装 rsa.ErrVerification, 实际: %v", err)
|
||||
}
|
||||
// 长度非法的签名属于输入错误,不应伪装成 rsa.ErrVerification。
|
||||
if err := rsax.VerifyPSS(&priv.PublicKey, message, []byte("short")); errors.Is(err, rsa.ErrVerification) {
|
||||
t.Fatalf("长度非法的签名不应返回 rsa.ErrVerification, 实际: %v", err)
|
||||
}
|
||||
|
||||
v15Sig, err := rsax.SignPKCS1v15(priv, message)
|
||||
if err != nil {
|
||||
t.Fatalf("SignPKCS1v15 失败: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&priv.PublicKey, []byte("other"), v15Sig); !errors.Is(err, rsa.ErrVerification) {
|
||||
t.Fatalf("PKCS#1 v1.5 失败应包装 rsa.ErrVerification, 实际: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPKCS1v15(&priv.PublicKey, message, []byte("short")); errors.Is(err, rsa.ErrVerification) {
|
||||
t.Fatalf("长度非法的签名不应返回 rsa.ErrVerification, 实际: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePEMRejectsBadInput 覆盖垃圾输入、空输入、合法 PEM 但内容错误等场景。
|
||||
func TestParsePEMRejectsBadInput(t *testing.T) {
|
||||
priv := testKey(t)
|
||||
validPublicPEM, err := rsax.MarshalPublicKeyPEM(&priv.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("准备合法公钥 PEM 失败: %v", err)
|
||||
}
|
||||
|
||||
// 其他算法生成的 PKIX 公钥(ECDSA)必须被明确拒绝,而不是类型断言 panic。
|
||||
ecdsaDER, err := x509.MarshalPKIXPublicKey(&ecdsaTestKey(t).PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("编码 ECDSA 公钥失败: %v", err)
|
||||
}
|
||||
|
||||
badInputs := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"nil 输入", nil},
|
||||
{"空切片", []byte{}},
|
||||
{"纯文本垃圾", []byte("this is not a pem file at all")},
|
||||
{"只有 BEGIN 行", []byte("-----BEGIN RSA PRIVATE KEY-----\n")},
|
||||
{"PEM 边界不完整", []byte("-----BEGIN PUBLIC KEY-----\nQUJD\n")},
|
||||
{"私钥标签但内容是随机字节", pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: []byte("garbage bytes here")})},
|
||||
{"私钥标签但内容是公钥 DER", func() []byte {
|
||||
block, _ := pem.Decode(validPublicPEM)
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: block.Bytes})
|
||||
}()},
|
||||
{"PKCS#8 标签但内容是 PKCS#1 私钥", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})},
|
||||
{"EC PRIVATE KEY 标签", pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: []byte("junk")})},
|
||||
}
|
||||
for _, tc := range badInputs {
|
||||
tc := tc
|
||||
t.Run("private/"+tc.name, func(t *testing.T) {
|
||||
var err error
|
||||
requireNoPanic(t, "ParsePrivateKeyPEM", func() {
|
||||
_, err = rsax.ParsePrivateKeyPEM(tc.data)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("ParsePrivateKeyPEM(%s) 应当返回错误", tc.name)
|
||||
}
|
||||
})
|
||||
t.Run("public/"+tc.name, func(t *testing.T) {
|
||||
var err error
|
||||
requireNoPanic(t, "ParsePublicKeyPEM", func() {
|
||||
_, err = rsax.ParsePublicKeyPEM(tc.data)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("ParsePublicKeyPEM(%s) 应当返回错误", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 公钥专属:合法 PUBLIC KEY PEM 但内容是私钥 DER、或非 RSA 的 PKIX 公钥。
|
||||
publicBadInputs := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"公钥标签但内容是 PKCS#1 私钥", pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})},
|
||||
{"公钥标签但内容是 PKCS#8 私钥", func() []byte {
|
||||
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
t.Fatalf("编码 PKCS#8 私钥失败: %v", err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})
|
||||
}()},
|
||||
{"PKIX 标签但内容是 ECDSA 公钥", pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: ecdsaDER})},
|
||||
{"CERTIFICATE 标签", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("junk")})},
|
||||
}
|
||||
for _, tc := range publicBadInputs {
|
||||
tc := tc
|
||||
t.Run("public/"+tc.name, func(t *testing.T) {
|
||||
var err error
|
||||
requireNoPanic(t, "ParsePublicKeyPEM", func() {
|
||||
_, err = rsax.ParsePublicKeyPEM(tc.data)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("ParsePublicKeyPEM(%s) 应当返回错误", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWeakKeyPolicy 锁定弱密钥策略。
|
||||
//
|
||||
// 该策略有意区分「生成」与「使用」两条路径:
|
||||
//
|
||||
// - 生成:一律要求 ≥ MinKeyBits(2048),绝不产生弱密钥;
|
||||
// - 使用既有的 1024 位密钥:允许解析、导出、加解密与签名。旧系统里真实存在
|
||||
// 这类密钥,若连解析都拒绝,持有者将无法解密自己的历史数据——那是把安全
|
||||
// 策略变成了功能性故障;
|
||||
// - 但调用方可以通过 IsWeakPrivateKey / IsWeakPublicKey 得知它已不达标;
|
||||
// - 低于 MinUsableKeyBits(1024) 的密钥无论如何都拒绝。
|
||||
func TestWeakKeyPolicy(t *testing.T) {
|
||||
// 1. 生成环节必须拒绝弱密钥
|
||||
for _, bits := range []int{512, 1024, 2047} {
|
||||
if _, err := rsax.GenerateKey(bits); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("GenerateKey(%d) 应当被拒绝, 得到 %v", bits, err)
|
||||
}
|
||||
if _, _, err := rsax.GenerateKeyPairPEM(bits); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("GenerateKeyPairPEM(%d) 应当被拒绝, 得到 %v", bits, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 既有的 1024 位密钥:允许导出、解析与使用
|
||||
legacy, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("生成 1024 位测试密钥失败: %v", err)
|
||||
}
|
||||
if !rsax.IsWeakPrivateKey(legacy) {
|
||||
t.Error("IsWeakPrivateKey(1024 位) = false, 期望 true")
|
||||
}
|
||||
if !rsax.IsWeakPublicKey(&legacy.PublicKey) {
|
||||
t.Error("IsWeakPublicKey(1024 位) = false, 期望 true")
|
||||
}
|
||||
|
||||
privatePEM, err := rsax.MarshalPrivateKeyPEM(legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("既有 1024 位私钥应当允许导出: %v", err)
|
||||
}
|
||||
publicPEM, err := rsax.MarshalPublicKeyPEM(&legacy.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("既有 1024 位公钥应当允许导出: %v", err)
|
||||
}
|
||||
parsed, err := rsax.ParsePrivateKeyPEM(privatePEM)
|
||||
if err != nil {
|
||||
t.Fatalf("既有 1024 位私钥应当允许解析: %v", err)
|
||||
}
|
||||
if _, err := rsax.ParsePublicKeyPEM(publicPEM); err != nil {
|
||||
t.Fatalf("既有 1024 位公钥应当允许解析: %v", err)
|
||||
}
|
||||
|
||||
// 加解密与签名都要能跑通,否则"允许使用"只是空话
|
||||
plaintext := []byte("legacy data that must stay readable")
|
||||
ciphertext, err := rsax.EncryptOAEPLong(&legacy.PublicKey, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("1024 位密钥加密失败: %v", err)
|
||||
}
|
||||
decrypted, err := rsax.DecryptOAEPLong(parsed, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("1024 位密钥解密失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Error("1024 位密钥加解密往返不一致")
|
||||
}
|
||||
|
||||
signature, err := rsax.SignPSS(parsed, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("1024 位密钥签名失败: %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPSS(&legacy.PublicKey, plaintext, signature); err != nil {
|
||||
t.Fatalf("1024 位密钥验签失败: %v", err)
|
||||
}
|
||||
|
||||
// 2048 位密钥不应被标记为弱
|
||||
strong, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("生成 2048 位密钥失败: %v", err)
|
||||
}
|
||||
if rsax.IsWeakPrivateKey(strong) {
|
||||
t.Error("IsWeakPrivateKey(2048 位) = true, 期望 false")
|
||||
}
|
||||
if rsax.IsWeakPublicKey(&strong.PublicKey) {
|
||||
t.Error("IsWeakPublicKey(2048 位) = true, 期望 false")
|
||||
}
|
||||
|
||||
// 3. 低于可使用下限:一律拒绝
|
||||
//
|
||||
// 这里用合成密钥而不是 rsa.GenerateKey:Go 1.26 的标准库已拒绝生成低于
|
||||
// 1024 位的密钥,恰好与本库的 MinUsableKeyBits 一致,因此无法用标准库造出
|
||||
// 更弱的密钥来做测试。
|
||||
tooWeakPriv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{N: new(big.Int).Lsh(big.NewInt(1), 511), E: 65537},
|
||||
D: big.NewInt(3),
|
||||
}
|
||||
tooWeakPub := &rsa.PublicKey{N: new(big.Int).Lsh(big.NewInt(1), 511), E: 65537}
|
||||
if tooWeakPriv.N.BitLen() >= rsax.MinUsableKeyBits {
|
||||
t.Fatalf("测试前提不成立: 合成密钥有 %d 位", tooWeakPriv.N.BitLen())
|
||||
}
|
||||
|
||||
if _, err := rsax.MarshalPrivateKeyPEM(tooWeakPriv); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位私钥应当拒绝导出, 得到 %v", err)
|
||||
}
|
||||
if _, err := rsax.MarshalPublicKeyPEM(tooWeakPub); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位公钥应当拒绝导出, 得到 %v", err)
|
||||
}
|
||||
if _, err := rsax.EncryptOAEP(tooWeakPub, []byte("x")); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位公钥应当拒绝加密, 得到 %v", err)
|
||||
}
|
||||
if _, err := rsax.SignPSS(tooWeakPriv, []byte("x")); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位私钥应当拒绝签名, 得到 %v", err)
|
||||
}
|
||||
if _, err := rsax.DecryptOAEP(tooWeakPriv, []byte("x")); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位私钥应当拒绝解密, 得到 %v", err)
|
||||
}
|
||||
if err := rsax.VerifyPSS(tooWeakPub, []byte("x"), []byte("y")); !errors.Is(err, rsax.ErrInvalidKeySize) {
|
||||
t.Errorf("512 位公钥应当拒绝验签, 得到 %v", err)
|
||||
}
|
||||
|
||||
// 4. nil 一律视为弱密钥
|
||||
if !rsax.IsWeakPrivateKey(nil) || !rsax.IsWeakPublicKey(nil) {
|
||||
t.Error("nil 密钥应当被视为弱密钥")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNilKeysReturnErrorRatherThanPanic 确认 nil 密钥在各入口返回错误而非 panic。
|
||||
func TestNilKeysReturnErrorRatherThanPanic(t *testing.T) {
|
||||
// nil 密钥必须返回错误而不是 panic。
|
||||
requireNoPanic(t, "EncryptOAEP(nil)", func() {
|
||||
if _, err := rsax.EncryptOAEP(nil, []byte("x")); err == nil {
|
||||
t.Error("EncryptOAEP(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
requireNoPanic(t, "DecryptOAEP(nil)", func() {
|
||||
if _, err := rsax.DecryptOAEP(nil, []byte("x")); err == nil {
|
||||
t.Error("DecryptOAEP(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
requireNoPanic(t, "SignPSS(nil)", func() {
|
||||
if _, err := rsax.SignPSS(nil, []byte("x")); err == nil {
|
||||
t.Error("SignPSS(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
requireNoPanic(t, "VerifyPSS(nil)", func() {
|
||||
if err := rsax.VerifyPSS(nil, nil, nil); err == nil {
|
||||
t.Error("VerifyPSS(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
requireNoPanic(t, "MarshalPrivateKeyPEM(nil)", func() {
|
||||
if _, err := rsax.MarshalPrivateKeyPEM(nil); err == nil {
|
||||
t.Error("MarshalPrivateKeyPEM(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
requireNoPanic(t, "MarshalPublicKeyPEM(nil)", func() {
|
||||
if _, err := rsax.MarshalPublicKeyPEM(nil); err == nil {
|
||||
t.Error("MarshalPublicKeyPEM(nil) 应当返回错误")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSaveAndLoadKeyPair 覆盖文件落盘、加载往返与私钥文件权限。
|
||||
func TestSaveAndLoadKeyPair(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
privatePath := filepath.Join(dir, "private.pem")
|
||||
publicPath := filepath.Join(dir, "public.pem")
|
||||
|
||||
if err := rsax.SaveKeyPair(privatePath, publicPath, testKeyBits); err != nil {
|
||||
t.Fatalf("SaveKeyPair 失败: %v", err)
|
||||
}
|
||||
|
||||
priv, err := rsax.LoadPrivateKey(privatePath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPrivateKey 失败: %v", err)
|
||||
}
|
||||
pub, err := rsax.LoadPublicKey(publicPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPublicKey 失败: %v", err)
|
||||
}
|
||||
if !pub.Equal(&priv.PublicKey) {
|
||||
t.Fatal("从文件加载的公私钥不匹配")
|
||||
}
|
||||
if priv.N.BitLen() != testKeyBits {
|
||||
t.Fatalf("加载的私钥位宽 = %d, 期望 %d", priv.N.BitLen(), testKeyBits)
|
||||
}
|
||||
|
||||
// 加载出的密钥必须真的可用。
|
||||
message := []byte("落盘密钥可用性检查")
|
||||
ciphertext, err := rsax.EncryptOAEP(pub, message)
|
||||
if err != nil {
|
||||
t.Fatalf("用加载的公钥加密失败: %v", err)
|
||||
}
|
||||
plaintext, err := rsax.DecryptOAEP(priv, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("用加载的私钥解密失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, message) {
|
||||
t.Fatalf("落盘密钥往返结果 = %q, 期望 %q", plaintext, message)
|
||||
}
|
||||
|
||||
// Windows 的权限位语义与 POSIX 不同(os.Stat().Perm() 通常恒为 0666),因此跳过断言。
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Log("Windows 平台跳过私钥文件权限位断言(POSIX 权限位语义不适用)")
|
||||
} else {
|
||||
info, err := os.Stat(privatePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat 私钥文件失败: %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0o600 {
|
||||
t.Fatalf("私钥文件权限 = %#o, 期望 0600", perm)
|
||||
}
|
||||
publicInfo, err := os.Stat(publicPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat 公钥文件失败: %v", err)
|
||||
}
|
||||
if perm := publicInfo.Mode().Perm(); perm != 0o644 {
|
||||
t.Fatalf("公钥文件权限 = %#o, 期望 0644", perm)
|
||||
}
|
||||
}
|
||||
|
||||
// 路径非法时必须返回错误。
|
||||
if err := rsax.SaveKeyPair(filepath.Join(dir, "same.pem"), filepath.Join(dir, "same.pem"), testKeyBits); err == nil {
|
||||
t.Fatal("私钥与公钥路径相同时应当返回错误")
|
||||
}
|
||||
if err := rsax.SaveKeyPair("", publicPath, testKeyBits); err == nil {
|
||||
t.Fatal("私钥路径为空时应当返回错误")
|
||||
}
|
||||
if _, err := rsax.LoadPrivateKey(filepath.Join(dir, "not-exist.pem")); err == nil {
|
||||
t.Fatal("加载不存在的私钥文件应当返回错误")
|
||||
}
|
||||
if _, err := rsax.LoadPublicKey(filepath.Join(dir, "not-exist.pem")); err == nil {
|
||||
t.Fatal("加载不存在的公钥文件应当返回错误")
|
||||
}
|
||||
if _, err := rsax.LoadPrivateKey(""); err == nil {
|
||||
t.Fatal("私钥路径为空时应当返回错误")
|
||||
}
|
||||
if _, err := rsax.LoadPublicKey(""); err == nil {
|
||||
t.Fatal("公钥路径为空时应当返回错误")
|
||||
}
|
||||
if _, err := rsax.LoadPrivateKey(publicPath); err == nil {
|
||||
t.Fatal("把公钥文件当私钥加载应当返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user