Files
encryptx/rsax/rsax.go
T
2026-09-30 15:49:12 +08:00

683 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
)
// cryptoHash 是 OAEP 与签名使用的标准库哈希标识,必须与 rsaHash 返回的构造器一致。
// 安全注意事项:修改该值会同时改变 OAEP 填充与签名格式,导致与既有数据不兼容。
const cryptoHash = crypto.SHA256
// 密钥长度等关键参数集中定义;调用方应直接引用这些常量,不要硬编码数字。
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
}
return priv.N.BitLen() < WeakKeyBits
}
// IsWeakPublicKey 报告公钥是否弱于当前建议强度(WeakKeyBits)。
func IsWeakPublicKey(pub *rsa.PublicKey) bool {
if pub == nil || pub.N == nil {
return true
}
return pub.N.BitLen() < WeakKeyBits
}
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)
}
if bits > MaxKeyBits {
return fmt.Errorf("%w: need at most %d bits, got %d", ErrInvalidKeySize, MaxKeyBits, bits)
}
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)
}