最近很流行自签证书进行 HTTPS 解密,然后就有不少人通过修改苹果的内购回执实现对 App 的破解。实际上验证购买应该是 App 连接服务器,服务器来和苹果的服务器进行通信,然后将结果发送给 App 的——但不少开发者(包括个人开发者以及企业开发者)懒得去专门维护服务器,所以直接用 App 和苹果的服务器进行通信,这就给中间人攻击提供了机会。
中间人攻击,就是说当 A 和 B 进行通信时,C 对 A 假装自己是 B,对 B 假装自己是 A,这样通信过程就从 A <—> B 变成了 A <–> C <–> B, 此时 A 和 B 都还以为对方是可信的,可通信的内容早不知道被 C 改了多少了。
通常来说我们使用 HTTPS 加密传输就已经能够很好的对抗中间人攻击了,但对于这种特殊情况,用户主动去信任一个“不安全”(自己签名)的证书进行 HTTPS 解密,那 HTTPS 加密在客户端就成了不可信的。
这种情况下,我们就需要自己实现对数据的保护。
安全性和完整性
对数据进行保护,主要是两点,“安全性”和“完整性”,前者保护数据不被第三方窥探,后者则保证你的数据在通信的过程当中不会被篡改,当然,通常来讲加密也一定程度保证了数据的完整性,毕竟如果被篡改了,可能也就无法解密了。
但只依靠加密,就给中间人攻击提供了机会。所以,我们要在 HTTPS 的安全性前提下,给自身代码添加完整性验证。
签名
所谓“签名”,和现实中的签名不同,这里的签名是指通过一些特殊的算法,将数据的特征提取出来,算法保证了一旦数据被改变,哪怕是一个符号,那么提取的特征就完全不同了,这样,我们只要在发送数据时包含这个特征,在收到后重新验证它,就能确保数据在传输过程中没有被篡改。
重签名
有一个问题就是如果中间人在改了数据之后重新签名,我们假定选择的特征提取算法被泄露了,中间人在更改了数据之后使用相同的算法重新签名(也就是提取特征),那客户端依然会认为数据是完整的——所以我们要在客户端和服务端约定一个“盐”值,你也可以理解为密码。
它不能让你把数据从特征值里恢复出来(都说了是提取特征,并不是加密,这个过程是不可逆的),但双方约定了密码之后在签名时将这个“密码”追加或者前置在数据中,这样一旦中间人更改了数据,他即使可以使用相同的算法重新签名,由于不知道我们预先设置的密码,他签出来的结果还是不同,这样客户端就知道这个数据是被篡改过的了。
重放攻击
既然无法对数据进行修改,那么骇客还可以抓取原本生效的数据报,重新再来发送,这样服务器或者客户端验证了签名和加密,数据完全正确,但实际上却是来自另一个版本客户端的传输数据——要应对这个情况,就要在之前数据结构的基础上,再增加一个概念——“时间戳”,通常我们使用 utc 的时间戳,其实就是从格林威治时间1970年01月01日到现在的总计秒数——当然你也可以用其他的能精确到秒(甚至毫秒)的时间格式——把时间戳也追加在数据里一起进行签名,避免时间戳被篡改,然后客户端和服务端进行通信时,检查时间戳是否在允许范围内,比如1秒内,或者30秒内等等,如果间隔太久,即时签名验证通过,也不能信任这个数据了,这样,就避免了重放攻击。
代码
签名的算法有很多,比如常见的 MD5、SHA1 等等等等,这些算法都可以直接拿来签名使用,这里我使用了 RSA 证书签名,实际上就是用更长更复杂的密码。
我们把要传输的数据和时间戳放在一起生成一个字符串,然后对字符串进行 SHA256 提取特征,然后将特征用 RSA 证书私钥签名;
等到客户端收到数据,就将数据和时间戳用相同的方式生成字符串,进行 SHA256 提取特征,然后用公钥进行签名验证。
这样公钥私钥的配对,好处在于,不像约定密码那样容易泄露——毕竟密码就写在程序中,很容易被找到——公钥可以随意传播,它的作用仅仅是验证签名而不能对数据进行签名,无需担心泄露问题。
生成密钥
打开任意你喜欢的终端,输入命令 openssl ,然后再输入命令 genrsa -out private.pem 1024 来生成私钥,后边的数字越大就越安全,但你也要权衡签名的时间成本,通常来说,1024 足够了。
最后输入命令 rsa -in private.pem -pubout -out public.pem 生成公钥,这个公钥就是你要放在客户端中分发到最终用户手中的密钥了。(使用命令 exit 来退出 openssl)
实际上,生成的密钥文件其实就是一个纯文本文档,密钥本质上是二进制的,但它们都被 base64 编码成文本保存。
使用命令 cat private.pem 来查看你的私钥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdT UA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3 QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etRIYdYV3QpYohFszH3wQIDAQAB gjIJ/dmBAkEA0QarqdWXZYbse1XIrQgBYTdVH9fNyLs1e1sBmNxlo4QMm/Le5a5L XenorEjnpjw5YpEJFDS4ijUI3dSzylC+QQJARqcD6TGbUUioobWB4L9GD7SPVFxZ AoGAFhKqkw/ztK6biWClw8iKkyX3LURjsMu5F/TBK3BFb2cYe7bv7lhjSBVGPL+c TfBU0IvvGXrhLXBb4jLu0w67Xhggwwfc86vlZ8eLcrmYVat7N6amiBmYsw20GVi LhF7zbYPIPGbHw+crP13THiYIYkHKJWsQDr8SXoNQ96TQsInTXUAmF2gzs/AwdQg UFmePbo1G2BXqMA43JxqbIQwOLZ03zdw6GHj6EVlx369IAECQQD4K2R3K8ah50Yz c3+EgcxRoO4bNuCFDA8VO/InP1ONMFuXLt1MbCj0ru1yFCyamc63NEUDAQJBALt7 PjGgsKCRuj6NnOcGDSbDWIitKZhnwfqYkAApfsiBQkYGO0LLaDIeAWG2KoCB9/6e lAQZnYPpOcCubWyDq4ECQQCrRDf0gVjPtipnPPS/sGN8m1Ds4znDDChhRlw74MI5 FydvHFumChPe1Dj2I/BWeG1gA4ymXV1tE9phskV3XZfa -----END RSA PRIVATE KEY----- |
使用命令 cat public.pem 来查看公钥:
1 2 3 4 5 6 |
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKoeRzRVf8WoRSDYYqUzThpYCr 90jfdFwTSXHJ526K8C6TEwdTUA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9 ilLb49Mqk2CvDt/yK32lgHv3QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etR IYdYV3QpYohFszH3wQIDAQAB -----END PUBLIC KEY----- |
Python
为了方便使用,我们直接把上面私钥的代码复制,用一个变量来保存这个私钥:
1 2 3 4 5 6 7 8 |
#私钥文件 priKey = '''-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdT UA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3 …… lAQZnYPpOcCubWyDq4ECQQCrRDf0gVjPtipnPPS/sGN8m1Ds4znDDChhRlw74MI5 FydvHFumChPe1Dj2I/BWeG1gA4ymXV1tE9phskV3XZfa -----END RSA PRIVATE KEY-----''' |
然后是签名代码:
1 2 3 4 5 6 |
def sign(data): key = RSA.importKey(priKey) h = SHA256.new(data.encode('utf8')) signer = PKCS1_v1_5.new(key) signature = signer.sign(h) return base64.b64encode(signature).decode('utf8') |
这里 sign() 直接传入字符串即可,它会将你传入的字符串转换成 UTF-8 编码的二进制数据进行 SHA256 摘要,然后将摘要进行签名,最终将签名结果转换为 base64 编码的字符串输出。
Swift
这里我们同样,将公钥保存到变量中,注意只复制 base64 编码部分,不要开头和结尾的 -----BEGIN PUBLIC KEY----- 和 -----END PUBLIC KEY----- :
1 |
private var publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdTUA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etRIYdYV3QpYohFszH3wQIDAQAB" |
上文中输出的密钥都是一行一行的,这里我们将它们无缝拼接在一起,形成一个长串。
记得引用框架 import CommonCrypto
然后我们写一个函数来生成二进制公钥:
1 2 3 4 5 6 7 |
private func getPublicSecKey() -> SecKey? { let keyBase64 = Data(base64Encoded: publicKey, options: .ignoreUnknownCharacters)! as CFData let sec = SecKeyCreateWithData(keyBase64, [kSecAttrType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as NSDictionary, nil) return sec } |
最终来对数据进行签名验证:
1 2 3 4 5 6 7 |
func sign(content:String, sign:String) ->Bool { let sha256Data = sha256(data: content.data(using: .utf8)!) let encryptedData = Data(base64Encoded: sign) let pubSecKey = getPublicSecKey() let verified = SecKeyVerifySignature(pubSecKey!, .rsaSignatureDigestPKCS1v15SHA256, sha256Data as CFData, encryptedData! as CFData, nil) return verified } |
只要传入被签名的字符串,再传入签名的 base64 编码,函数就会自动对字符串进行 SHA256 摘要,然后签名恢复成二进制数据,最后通过系统内置算法,使用你的公钥,对摘要和签名进行验证,验证通过,返回 true 。
后记
使用不同的代码在不同的平台进行互通是一件很困难的事情,简单的加密算法和方式往往会在不同的平台得出不一样的结果,同一个语言,在同一个平台,是最容易的,但不同的语言就很复杂,具体实现时一定要注意这个问题。在尝试了无数种方式后,最终我还是使用 RSA 签名实现了跨语言平台互通,希望这篇文章对你有用。
参考文献
本文由 落格博客 原创撰写:落格博客 » Swift Python 互通 Json 数据签名
转载请保留出处和原文链接:https://www.logcg.com/archives/3318.html