3. 密钥对的生成与安全存储:使用OpenSSL生成RSA/SM2密钥、密钥存储到Android Keystore/iOS Keychain
3.1 原理概述
支付SDK在进行数据签名、验签和密钥协商时,需要一套可靠的密钥对。密钥对的生成与安全存储是整个安全体系的根基。本章聚焦两种主流非对称算法:RSA(国际通用,2048/4096位)和SM2(中国国家密码标准,基于椭圆曲线)。安全存储利用移动端硬件隔离能力:Android Keystore(基于TEE/StrongBox)和iOS Keychain(基于Secure Enclave),防止私钥被应用进程或恶意软件读取。
3.2 关键概念对比
| 维度 | RSA | SM2 |
|---|---|---|
| 算法基础 | 大整数分解 | 椭圆曲线离散对数 |
| 推荐密钥长度 | 2048 / 4096 bit | 256 bit(曲线参数固定) |
| 签名性能 | 较慢(指数运算) | 快(标量乘) |
| 国密合规 | 否 | 是(GM/T 0003-2012) |
| Android Keystore支持 | 是(API 18+) | 是(API 28+,需系统支持SM2) |
| iOS Keychain支持 | 是(iOS 10+) | 是(需CryptoKit或第三方实现) |
3.3 实现流程
- 选择算法与密钥长度 — 支付场景建议RSA-4096或SM2-256。
- 使用OpenSSL命令行生成密钥对 — 开发阶段生成测试密钥;生产环境需在安全设备中生成。
- 解析密钥文件 — 提取公钥(.pem/.der)和私钥(PKCS#8/EC私有密钥格式)。
- 集成到移动端 — 将私钥导入Android Keystore / iOS Keychain;公钥用于传输。
- 验证存储完整性 — 签名一个随机数并验签,确保硬件安全模块正确工作。
安全建议:私钥永远不应离开安全存储区。生成环节应在专用离线机器或HSM中进行,导出公钥后立即销毁临时私钥副本。
3.4 密钥对生成命令(OpenSSL)
RSA密钥对生成
# 生成私钥(PKCS#8格式,无加密便于导入Keystore;生产环境建议加aes256)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out private_rsa.pem
# 提取公钥
openssl pkey -in private_rsa.pem -pubout -out public_rsa.pem
# 查看私钥详细信息
openssl pkey -in private_rsa.pem -text -noout
SM2密钥对生成
# 生成SM2私钥(使用EC参数sm2p256v1)
openssl ecparam -genkey -name SM2 -out private_sm2.pem
# 提取公钥
openssl ec -in private_sm2.pem -pubout -out public_sm2.pem
# 验证曲线
openssl ec -in private_sm2.pem -text -noout | head -5
注意:OpenSSL 1.1.1+原生支持SM2;如果使用旧版本(1.0.2),需要编译时开启SM2支持或使用国密分支(如GmSSL)。SM2私钥文件格式为EC私钥,Android Keystore导入时需要转换为PKCS#8。
3.5 Android Keystore存储实现
API 级别要求
- RSA Key Pair:
KeyPairGeneratorSpec(API 18+),推荐使用KeyGenParameterSpec(API 23+)。 - SM2 Key Pair:API 28+ 可使用
KeyGenParameterSpec.Builder指定算法"EC",同时设置ECCurve为"secp256r1"(SM2曲线等价于secp256r1的变体,但Android原生SM2支持有限;建议使用Bouncy Castle或GMSSL接口)。
Android RSA密钥导入示例
// 假设已有PEM格式私钥字符串
String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n...";
// 加载PKCS8私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
Base64.decode(privateKeyPem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", ""), Base64.DEFAULT));
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(keySpec);
// 存储到Android Keystore
String alias = "payment_signing_key_rsa";
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(privateKey, null),
new KeyProtection.Builder(KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.build());
Android SM2密钥生成示例(原生KeyStore)
KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder("payment_sm2_key",
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) // 与SM2曲线同阶
.setDigests(KeyProperties.DIGEST_SM3) // Android 13+ 支持SM3
.build();
kpg.initialize(spec);
KeyPair keyPair = kpg.generateKeyPair();
3.6 iOS Keychain存储实现
密钥格式转换(DER / PEM)
iOS Keychain要求私钥为SecKey对象,通常从PKCS#12或DER格式导入。推荐先使用OpenSSL将PEM转为DER:
# RSA私钥DER
openssl pkcs8 -topk8 -inform PEM -outform DER -in private_rsa.pem -nocrypt -out private_rsa.der
# SM2私钥DER(EC私钥)
openssl ec -inform PEM -outform DER -in private_sm2.pem -out private_sm2.der
iOS导入RSA私钥到Keychain
// 加载DER数据
NSData *keyData = [NSData dataWithContentsOfFile:@"private_rsa.der"];
NSDictionary *options = @{
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
(__bridge id)kSecAttrKeySizeInBits: @4096
};
CFErrorRef error = NULL;
SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)keyData,
(__bridge CFDictionaryRef)options,
&error);
// 存储到Keychain
NSDictionary *addDict = @{
(__bridge id)kSecClass: (__bridge id)kSecClassKey,
(__bridge id)kSecAttrLabel: @"com.payment.signing.rsa",
(__bridge id)kSecValueRef: (__bridge id)privateKey,
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
};
SecItemAdd((__bridge CFDictionaryRef)addDict, NULL);
3.7 流程结构图(SVG)
3.8 调试与验证
Android侧验签测试
// 使用Keystore中的私钥签名
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PrivateKey privateKey = (PrivateKey) keyStore.getKey("payment_signing_key_rsa", null);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update("test_data".getBytes());
byte[] signature = signer.sign();
// 使用公钥验签
PublicKey publicKey = keyStore.getCertificate("payment_signing_key_rsa").getPublicKey();
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update("test_data".getBytes());
boolean valid = verifier.verify(signature); // true 表示存储正确
iOS侧验签测试
// 签名
SecKeyRef privateKey = ...; // 从Keychain取回
NSData *data = [@"test_data" dataUsingEncoding:NSUTF8StringEncoding];
CFErrorRef error = NULL;
NSData *sig = (NSData *)CFBridgingRelease(SecKeyCreateSignature(privateKey,
kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256,
(__bridge CFDataRef)data,
&error));
// 验签(需公钥)
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
Boolean ok = SecKeyVerifySignature(publicKey,
kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256,
(__bridge CFDataRef)data,
(__bridge CFDataRef)sig,
&error);
3.9 常见错误与解决方案
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
java.security.InvalidKeyException | 私钥格式不满足PKCS#8要求 | 使用openssl pkcs8 -topk8转换 |
SecItemAdd 返回 errSecMissingEntitlement | Keychain访问权限配置错误 | 在 Capabilities 中开启 Keychain Sharing |
SM2验签失败 | 曲线参数不一致(使用secp256r1代替SM2) | 确认Android设备原生支持SM2(Pixel 6+),否则集成Bouncy Castle |
KeyStore未找到别名 | 未正确调用setEntry或别名冲突 | 使用keyStore.containsAlias()检查,删除旧Key |
3.10 性能与稳定性优化
- 密钥生成频率:支付SDK应仅在首次启动或远程轮换指令触发时生成密钥对,避免在UI线程中执行。建议使用
AsyncTask或协程在后台线程生成。 - 缓存公钥:公钥可从Keychain/Keystore中导出并缓存到内存(
WeakReference或LruCache),减少每次签名前的证书链加载。 - Keystore初始化懒加载:不要在应用启动时加载所有密钥,按需获取(
keyStore.getEntry),避免首次解锁延迟。 - iOS Keychain性能:
SecItemCopyMatching查询应使用精确的kSecAttrLabel和kSecClass,避免遍历所有Keychain项。 - 硬件隔离降级策略:当设备不支持TEE或Secure Enclave(如低端Android 6.0设备),应回退到软件Keystore(
BKS),并用BIO加密存储私钥(但安全性降低,需交易风险)。
生产环境基线:所有支付关键私钥必须存储在硬件隔离区(Android StrongBox / iOS Secure Enclave),不允许使用纯软件方案存储签名私钥。公钥可明文存储,但需通过证书链校验防止替换攻击。