完善不同加密方法的使用

This commit is contained in:
yun
2026-09-30 15:49:12 +08:00
parent 034315ddbd
commit 7db722f8b9
69 changed files with 21362 additions and 2563 deletions
+662 -99
View File
@@ -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
View File
@@ -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("把公钥文件当私钥加载应当返回错误")
}
}