完善不同加密方法的使用
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user