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

777 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_test
import (
"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"
)
// testKeyBits 是测试用密钥长度,取安全下限以缩短密钥生成时间。
const testKeyBits = rsax.MinKeyBits
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("把公钥文件当私钥加载应当返回错误")
}
}