这是《百图解码支付系统设计与实现》专栏系列文章中的第(4)篇。也是支付安全系列的第(1)篇。
专栏地址:http://t.csdnimg.cn/2L7Mg
本文主要讲清楚支付系统中为什么要做签名验签,哪些是安全的算法,哪些是不安全的算法,以及对应的核心代码实现。
通过这篇文章,你可以了解到:
在电子支付的万亿市场中,安全无疑是核心中的核心。有一种称之为“签名验签”的技术在支付安全领域发挥着至关重要的作用。那什么是签名验签呢?
签名验签是数字加密领域的两个基本概念。
签名:发送者将数据通过特定算法和密钥转换成一串唯一的密文串,也称之为数字签名,和报文信息一起发给接收方。
验签:接收者根据接收的数据、数字签名进行验证,确认数据的完整性,以证明数据未被篡改,且确实来自声称的发送方。如果验签成功,就可以确信数据是完好且合法的。
假设被签名的数据(m),签名串(Σ),散列函数(H),私钥(Pr),公钥(Pu),加密算法(S),解密算法(S^),判断相等(eq)。
简化后的数学公式如下:
签名:Σ=S[H(m), Pr]。
验签:f(v)=[H(m) eq S^(Σ, Pu)]。
流程如下:
签名流程:
把数字签名(Σ)和原始消息(m)一起发给接收方。
验签流程:
如果两个散列值相等,那么验签成功,消息(m)被认为是完整的,且确实来自声称的发送方。如果不一致,就是验签失败,消息可能被篡改,或者签名是伪造的。
现实中的算法会复杂非常多,比如RSA,ECDSA等,还涉及到填充方案,随机数生成,数据编码等。
银行怎么判断扣款请求是从确定的支付平台发出来的,且数据没有被篡改?商户不承认发送过某笔交易怎么办?这都是签名验签技术的功劳。
签名验签主要解决3个问题:
如果无法做身份验证,支付宝就无法知道针对你的账户扣款99块的请求是真实由你楼下小卖部发出去的,还是我冒充去扣的款。
如果无法校验完整性,那么我在公共场景安装一个免费WIFI,然后截获你的微信转账请求,把接收者修改成我的账号,再转发给微信,微信就有可能会把钱转到我的账号里。
比如微信支付调用银行扣款100块,银行返回成功,商户也给用户发货了,几天后银行说这笔扣款成功的消息不是他们返回的,他们没有扣款。而签名验签就能让银行无法抵赖。
流程:
安全一直是一个相对的概念,很多曾经是安全的算法,随着计算机技术的发展,已经不安全了,以后到了量子计算的时代,现在大部分的算法都将不再安全。
一般而言,安全同时取决于算法和密钥长度。比如SHA-256就比MD5更安全,RSA-2048就比RSA-1024更安全。
已经被认为不安全的算法有MD5、SHA-1等算法,容易受到碰撞攻击,不应该在支付系统中使用。
仍然被认为是安全的算法有:SHA-256,SHA-3, RSA-1024,RSA-2048,ECDSA等。
当前最常见推荐的算法是RSA-2048。RSA-1024以前使用得多,但因为密钥长度较短,也已经不再推荐使用。
SHA-256只是一种单纯的散列算法,其实是不适合做签名验签算法的,因为需要双方共用一个API密钥,一旦泄露,无法确认是哪方被泄露,也就是只解决了完整性校验,无法解决身份验证和防抵赖性。但因为使用简单,国内外仍然有不少的支付公司公司在大量使用。
下面以RSA(SHA256withRSA)为例,示例代码如下:
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class RSASignatureUtil {
// 使用私钥对数据进行签名
public static byte[] sign(byte[] data, byte[] privateKey) throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(priKey);
signature.update(data);
return signature.sign();
}
// 使用公钥验证签名
public static boolean verify(byte[] data, byte[] publicKey, byte[] signatureBytes) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(signatureBytes);
}
}
签名输出是字节码,还需要编码,一般是base64。
如果使用SHA-256(很多公司仍在使用,但不推荐),如下:
import java.security.MessageDigest;
public class SHA256Util {
// 使用SHA-256对数据进行散列
public static byte[] hash(byte[] data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
}
}
这里data已经是加了API密钥(也称为API KEY)。所谓的API密钥,就是交易双方共享的一个密钥,这样双方生成的哈希值才会一致。
不管是与商户的联调,还是与支付渠道(或银行)之间的联调,签名验签都是非常耗费精力的环节。验签不通过通常有以下几个情况:
解决上述问题的最好办法,就是让服务提供方提供一段示例代码,以及示例报文+示例签名,然后在本地使用main方法先跑成功,再移植到项目代码中。
本章主要讲了签名验签名的概念,对于支付系统的重要性,以及常见签名验签名算法及JAVA代码实现。
但是还有一个同样非常重要的问题没有讲:如何安全储存密钥?如果密钥放在代码里或数据库里,开发人员是可以直接获得的,如果不小心泄露出去怎么办?
应对的解决方案就是创建一个密钥中心专门负责密钥的管理,无论加密解密还是签名验签,全部调用密钥中心来处理,业务系统不接触密钥明文。
那又来了一个新的问题:这个密钥中心如何设计和实现,才能既保证很高的安全性,又能有非常高的性能表现呢?
后面有机会再开一个密钥中心的设计和实现专题来聊。