3、密码学基础(下):非对称加密算法(RSA、SM2)与哈希算法(SHA-256、SM3),数字签名与身份认证在票务系统中的应用
3.1 非对称加密:为什么我们需要两把钥匙?
对称加密虽然快,但有个致命问题——密钥分发。你想想看,地铁票务系统每天要处理上千万笔交易,如果每台闸机和中央服务器都用同一个密钥,一旦某台设备被物理破解,整个网络就完蛋了。
非对称加密解决了这个痛点。它用一对密钥:公钥公开,私钥保密。公钥加密的数据只能用私钥解密,反过来私钥签名的数据可以用公钥验证。我在做某城市地铁票务系统改造时,就遇到过这样的场景:自动售票机需要向中央系统上传交易数据,但又不能直接存明文密钥在机器里——万一被拆机呢?
当时我的方案是:每台售票机只存中央系统的公钥,用它加密交易报文。中央系统用自己的私钥解密。这样即使机器被物理破解,攻击者也拿不到解密密钥,最多只能拿到加密用的公钥——这玩意儿本来就是公开的。
3.2 RSA算法:老牌劲旅,但要注意密钥长度
RSA是目前应用最广泛的非对称算法。它的数学基础是大整数分解难题——两个大质数相乘很容易,但反过来分解很难。
我个人的习惯是:在票务系统中,RSA密钥长度至少2048位。1024位的RSA在2010年左右就被认为不够安全了。曾经有客户问我:「1024位够用了吧?性能好一些。」我直接告诉他:「别省这点性能,一旦被破解,整个票务系统的信任体系就崩塌了。」
RSA的典型应用场景是密钥交换和数字签名。在票务系统中,我们通常用它来加密对称密钥(比如AES的会话密钥),而不是直接加密大量交易数据——因为RSA太慢了。
// RSA密钥生成示例(伪代码)
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048); // 密钥长度2048位
KeyPair keyPair = generator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 加密:用公钥加密对称密钥
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedKey = cipher.doFinal(symmetricKey);
// 解密:用私钥解密
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedKey = cipher.doFinal(encryptedKey);
避坑指南:我曾经在项目中看到有人直接用RSA加密整个交易报文,结果性能惨不忍睹。正确的做法是「混合加密」:用RSA加密AES密钥,用AES加密实际数据。这样既保证了安全性,又兼顾了性能。
3.3 SM2算法:国密标准,自主可控
SM2是国家密码管理局发布的椭圆曲线公钥密码算法。相比RSA,SM2在同等安全强度下密钥更短、计算更快。比如SM2的256位密钥相当于RSA的3072位。
在轨道交通票务系统中,SM2越来越重要。特别是涉及政府项目或关键信息基础设施时,国密算法是硬性要求。我记得有个项目,客户明确要求「必须用SM2,不能用RSA」。当时我们团队对SM2不太熟悉,踩了不少坑。
SM2的核心优势在于:
- 密钥短:256位即可达到高安全等级
- 性能好:签名速度比RSA快很多
- 自主可控:不依赖国外算法,符合等保要求
// SM2密钥生成示例(使用BouncyCastle)
SM2KeyPairGenerator generator = new SM2KeyPairGenerator();
generator.init(new ECKeyGenerationParameters(SM2P256V1.getInstance(), new SecureRandom()));
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
// SM2签名
SM2Signer signer = new SM2Signer();
signer.init(true, privateKey);
signer.update(message, 0, message.length);
byte[] signature = signer.generateSignature();
个人经验:SM2的密钥对生成比RSA快得多。在票务系统的密钥管理平台中,我们每天需要生成大量临时密钥对,用SM2明显比RSA更高效。但要注意,SM2的签名结果不是固定长度的,这点和RSA不同。
3.4 哈希算法:数据的「指纹」
哈希算法能把任意长度的数据映射成固定长度的摘要。这个摘要就像数据的「指纹」——内容不同,指纹就不同;内容相同,指纹一定相同。而且从指纹反推原始数据在计算上是不可行的。
在票务系统中,哈希算法最常见的用途是:
- 数据完整性校验:交易数据在传输过程中有没有被篡改?算一下哈希值对比就知道了。
- 密码存储:用户的支付密码不能存明文,存哈希值更安全。
- 数字签名的基础:先对数据做哈希,再对哈希值签名,效率更高。
3.5 SHA-256 vs SM3:选哪个?
SHA-256是国际通用的哈希算法,输出256位摘要。SM3是国密哈希算法,同样输出256位。从安全强度上看,两者相当。但在实际项目中,我建议这样选:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 国际项目或兼容性要求高 | SHA-256 | 全球通用,库支持广泛 |
| 国内政府项目、等保合规 | SM3 | 国密要求,自主可控 |
| 混合场景 | 两者都支持 | 根据对接方要求灵活切换 |
我曾经在项目中遇到过一个问题:用SHA-256计算交易数据的哈希值,然后传给第三方系统做校验。结果对方只支持SM3,导致对接失败。后来我们改成了「双哈希」方案——同时计算SHA-256和SM3,根据对方要求选择传递哪个。虽然多了一点计算开销,但兼容性大大提升。
// SHA-256示例
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(data);
// SM3示例(使用BouncyCastle)
SM3Digest sm3 = new SM3Digest();
sm3.update(data, 0, data.length);
byte[] hash = new byte[sm3.getDigestSize()];
sm3.doFinal(hash, 0);
3.6 数字签名:证明「你是谁」和「你没改过」
数字签名结合了非对称加密和哈希算法。它的核心作用是两个:
- 身份认证:证明消息确实来自声称的发送方
- 完整性验证:证明消息在传输过程中没有被篡改
在票务系统中,数字签名无处不在。比如乘客用手机APP扫码进站时,APP会生成一个签名过的乘车码。闸机验证签名通过后,才放行。这个过程中,签名保证了乘车码是官方APP生成的,不是伪造的。
签名流程是这样的:
- 发送方对数据做哈希,得到摘要
- 用私钥对摘要加密,得到签名
- 将数据和签名一起发送给接收方
- 接收方用公钥解密签名,得到摘要A
- 接收方对收到的数据做哈希,得到摘要B
- 对比摘要A和摘要B,一致则验证通过
关键点:数字签名不是加密数据,而是加密数据的哈希值。这样做的好处是:签名速度快(哈希计算很快),而且签名长度固定(不管数据多大,签名长度都一样)。
3.7 身份认证在票务系统中的实战
身份认证是票务系统的第一道防线。我参与过的某一线城市地铁票务系统,每天有超过500万笔交易。如果身份认证出问题,后果不堪设想。
常见的身份认证方式包括:
- 基于数字证书:每个终端设备(闸机、售票机)都颁发一个数字证书,证书里包含公钥和身份信息。设备之间通信时,先验证对方证书的有效性。
- 基于挑战-响应:服务器发送一个随机数(挑战),客户端用私钥签名后返回(响应)。服务器用公钥验证签名,确认客户端身份。
- 基于会话令牌:用户登录后获得一个签名的令牌,后续请求携带令牌即可。JWT(JSON Web Token)就是典型实现。
我个人比较推荐「证书+挑战-响应」的组合方案。为什么呢?因为纯证书方案有个问题:证书本身是静态的,如果私钥泄露,攻击者可以一直用这个证书伪装身份。而挑战-响应每次的随机数都不同,即使攻击者截获了一次响应,也无法重放。
// 挑战-响应认证示例
// 服务端生成挑战
byte[] challenge = generateRandomBytes(32);
// 客户端用私钥签名挑战
Signature signature = Signature.getInstance("SM3withSM2");
signature.initSign(privateKey);
signature.update(challenge);
byte[] response = signature.sign();
// 服务端用公钥验证
signature.initVerify(publicKey);
signature.update(challenge);
boolean isValid = signature.verify(response);
我曾经踩过的坑:在某项目中,我们直接用设备的序列号作为身份标识,没有做签名验证。结果有人伪造了序列号,冒充合法设备接入网络。从那以后,我坚持所有设备接入必须经过双向签名认证——设备验证服务器的身份,服务器也验证设备的身份。
3.8 小结:票务系统中的密码学组合拳
在实际的票务系统中,很少只用一种密码学技术。通常的组合方式是:
- 用SM2/RSA做密钥交换和数字签名
- 用SM3/SHA-256做数据完整性校验
- 用SM4/AES做实际数据的对称加密
- 用数字签名+挑战-响应做身份认证
这套组合拳打下来,基本能覆盖票务系统的主要安全需求。当然,具体选型还要看项目要求、合规性、性能指标等因素。但核心原则不变:永远不要自己发明密码算法,永远使用经过验证的标准实现。
下一章我会讲对称加密算法在票务数据存储中的应用,包括AES和SM4的实战对比。到时候再聊。