// 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) }