前言: 有不少人反馈文章过于长,邮件内容不利于阅读连贯性。本文删除邮件内容,只留下疑点和解惑文字。
StoreKit2 收据更新,原先
appStoreReceiptURL
的验证逻辑不可用。但是我们后台的发货需要依据验证收据的真实性,这就导致我们要使用StoreKit2,就必须更新我们的发货逻辑,验证新的收据。
Apple官方可以给出的资料如下
苹果的签名校验示意图,视频并没有对此做出过多的解释
wwdc21-10174 在您的服务器上管理应用内购买
在上述视频中Apple解释了如何验证签名,但是这在我们熟悉以往验证流程看起来很是模糊。
其中关于JWS具体的数据格式我会在下面解释到。
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension VerificationResult where SignedType == Transaction {
/// The raw JSON web signature for the signed value.
public var jwsRepresentation: String { get }
...
}
其中在WWDC中获取到jws数据格式如下:
Base64(header) + "." + Base64(payload) + "." + sign( Base64(header) + "." + Base64(payload) )
//苹果实际返回的字符大概五千多个字符。以下篇幅有限,省略一波
eyJh--很长很长很长--bGci0.eyJ0cmFu-很长很长-kxNH0.-ewQDL-也不短-WbDXMg
- 大致就是 Header + Payload + Signture
- Header = Base64.decode(header),解析base64你会得到
header: {
alg: 'ES256',//alg 声明知道我们使用了什么签名算法
x5c: [ //x5c 声明中数组中的证书链
'MIIEMDueU3...',
'MII...‘, 'xxx...xx'
]
}
- Payload = Base64.decode(payload),解析出来将会得到完整可读的json
//jwsRepresentationJWS解析后
{
"transactionId":"1000000916922942",
"originalTransactionId":"1000000916922942",
"bundleId":"com.xxx.ios",
"productId":"king.xxxx.60",
"purchaseDate":1637723816809,
"originalPurchaseDate":1637723816809,
"quantity":1,
"type":"Consumable",
"deviceVerification":"qVh9F+9eGf9KQxxxxxxpPrfcGdlJyht775ID9ytSQCWItx",
"deviceVerificationNonce":"a8735bcf-825f-4aeb-b99c-6f866cadc96e",
"appAccountToken":"397711d6-61b8-bfb7-c94f-8xxxxxdb7b7",
"inAppOwnershipType":"PURCHASED",
"signedDate":1637723816914
}
- 而 Signture是由 [( Base64(header)+ “.” + Base64(payload) ), private key ] 签名而成
- 使用 header 获取 alg 算法以及 x5c 证书用来解密 signture 签名 (可以使用你最喜欢的密码库来解密交易信息的签名)
总结一下,如果你只是想获取交易中的具体参数,你直接base64 Decode Payload参数就行了,但是如果你需要验证签名,则必须使用到Signture, Header
但是存在几个疑惑点,一直查不到相关的信息:
- alg 和 x5c 我们获取到了,但是最喜欢的密码库是什么意思?我们到底该如何解密?
- 查阅资料发现 x5c 验证需要一个根证书,苹果并未提供。
- 或许是出于推荐客户端自我验证的方式,苹果除了提供以上信息, 并未透露其他服务器验证签名的信息以及示例代码。 归纳一下: 我需要:
- 称手的武器: 解密工具包
- 解密所需要的信息: 公钥,算法等…
- 验证公钥可信的证书
首先不确定使用哪一个根证书进行验证公钥颗心,于是联系Apple PKI团队(发送内容省略,大概内容就是我们需要一个证书验证storekit2公钥)
PS:(因为这不是一个可沟通的渠道,时间不可知,先行沟通) 以下是邮件回复内容: 2021-12-03
Hello Ray,
Our certificates are located here- www.apple.com/certificate…
Apple Root G3 Apple Root CA – G3 Root.
Regards, Apple PKI
目前我们通过邮件联系的方式,获取了CA,可以验证公钥的来源可信。 我需要:
- 称手的武器: 解密工具包
- 解密所需要的信息: 公钥,算法等…
验证公钥可信的证书
如何解密jws字符串呢?
苹果的回复:
我收到了 StoreKit 工程方面的回复。关于了解已签名交易签名的验证,一个不错的起点是JSON Web Tokens web site
从苹果回复来看,我们可以在JSON Web Tokens web site找到答案。
- 进入资料库
- 选择你编写的语言
- 选择你符合你算法的copy库
这里我选择的是: PHP firebase/php-jwt
但是我选择了这个库之后,并依照文档所要求的安装好了 composer 以及按照相应方法,还是无法解密sign
向技术支持人员求教之后,获得一份RFC 7515文档。
其中RFC 7515中写道:
4.16章节
“x5c”(X.509 证书链)头参数包含 X.509 公钥证书或证书链 RFC5280,对应于用于对 JWS 进行数字签名的密钥。证书或证书链表示为证书值字符串的 JSON 数组。数组中的每个字符串都是 base64 编码(RFC4648 的第 4 节——不是base64url 编码)获得的DER ITU.X690.2008 PKIX 证书值。
包含与用于对 JWS 进行数字签名的密钥相对应的公钥的证书必须是第一个证书
。这可以跟随着额外的证书,每个后续的证书都是用来证明前一个证书的。接收方必须根据RFC5280验证证书链,如果发生任何验证失败,则认为该证书或证书链无效。此标题参数的使用是可选的。
我需要:
称手的武器: 解密工具包解密所需要的信息: 公钥,算法等…验证公钥可信的证书
能获取到的东西都获取了,接下里主要是使用工具、利用合适的逻辑、完成想要的东西。
按照文档所写的,我使用x5c数组中第一个数据,转换成公钥后,成功解密获得如下数据。
Array
(
[transactionId] => 1000000916922942
[originalTransactionId] => 1000000916922942
[bundleId] => com.jp.hime.ios
[productId] => king.test.gold.60
[purchaseDate] => 1637723816809
[originalPurchaseDate] => 1637723816809
[quantity] => 1
[type] => Consumable
[deviceVerification] => qVh9F+9eGf9KQh+B3fIAoQKL8Kz0CkmVGfUiwpPrfcGdlJyht775ID9ytSQCWItx
[deviceVerificationNonce] => a8735bcf-825f-4aeb-b99c-6f866cadc96e
[appAccountToken] => 397711d6-61b8-bfb7-c94f-8ea670fdb7b7
[inAppOwnershipType] => PURCHASED
[signedDate] => 1637723816914
)
解密完成之后,迷惑的几个点
我们从 x5c 获取的公钥能否保证真实不被篡改?
既然公钥不可信,解密出来的数据当然也不可信。
截取你的数据,修改你的数据,用中间人私钥加密篡改的数据,再生成新的公钥数组发送给你。
按照目前流程,你会相信这是一份真实的签名后的信息。
综上,我们不能相信这一份数据,即使完整解密出来, 它也可能是别人想让你看到的
。
那我们该如何保证解密出来的数据可信呢?
唯一的答案:保证x5c第一个数据公钥匙可信。
那该如何保证x5c证书链可信呢?
- 不了解X.509证书的可以上知乎X.509 数字证书的基本原理及应用和百科
我们在回顾一下RFC 7515 v4.16内容 其中一句话写到:
每个后续的证书都是用来证明前一个证书的。接收方必须根据RFC5280验证证书链,如果发生任何验证失败,则认为该证书或证书链无效
- 从以上内容,我们得知该如何验证验证证书链,是通过这种方式RFC5280验证。
并且我查阅网上资料获得:
证书链最后一个证书为苹果签发,需要使用苹果CA证书验证。(最重要的一步:可证实来源可信。)
而RFC中说到证书链第一个证书作为签名解密的证书。
- 有同学说:
我直接生成私钥篡改加密数据,生产公钥替换伪造证书链第一个公钥,不动你其他公钥,你也可以解密数据,还可以验证公钥可信。
确实,如果我们只是用CA去验证证书链最后一个证书,黑产依旧可以篡改数据。但是,
我: 证书链第二个就验证不通过了
- 还有同学说:
我直接给你做一套证书链,这样没办法了吧?
我: 到最后一个证书的时候,CA会验证你来源不可信的。
所以应该是用第二个检验第一个,第三个检验第二个,以次类推...确保证书链整链可信,到最后一个时,使用苹果CA证书校验。确保公钥来源可信
话不多说,上代码:
<?php //php版本 >= 7.4
use Firebase\JWT\JWT;
use Firebase\JWT\Key; //composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';
error_reporting(E_ALL ^ E_WARNING ^ E_NOTICE);
$jws = 'eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMTAwMDAwMDkxNjkyMjk0MiIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjEwMDAwMDA5MTY5MjI5NDIiLCJidW5kbGVJZCI6ImNvbS5qcC5oaW1lLmlvcyIsInByb2R1Y3RJZCI6ImtpbmcudGVzdC5nb2xkLjYwIiwicHVyY2hhc2VEYXRlIjoxNjM3NzIzODE2ODA5LCJvcmlnaW5hbFB1cmNoYXNlRGF0ZSI6MTYzNzcyMzgxNjgwOSwicXVhbnRpdHkiOjEsInR5cGUiOiJDb25zdW1hYmxlIiwiZGV2aWNlVmVyaWZpY2F0aW9uIjoicVZoOUYrOWVHZjlLUWgrQjNmSUFvUUtMOEt6MENrbVZHZlVpd3BQcmZjR2RsSnlodDc3NUlEOXl0U1FDV0l0eCIsImRldmljZVZlcmlmaWNhdGlvbk5vbmNlIjoiYTg3MzViY2YtODI1Zi00YWViLWI5OWMtNmY4NjZjYWRjOTZlIiwiYXBwQWNjb3VudFRva2VuIjoiMzk3NzExZDYtNjFiOC1iZmI3LWM5NGYtOGVhNjcwZmRiN2I3IiwiaW5BcHBPd25lcnNoaXBUeXBlIjoiUFVSQ0hBU0VEIiwic2lnbmVkRGF0ZSI6MTYzNzcyMzgxNjkxNH0.-ewQD6FbwdY_ycMHISNY7rp6VesmoJH_IURsX18JAVbb49CqUnjXHzxMHwTv_Pgs59DUIsUY1rt8cQWLWbDXMg';
$components = explode('.', $jws);
$header = base64_decode($components[0]);
$payload = base64_decode($components[1]);
$signature = base64_decode($components[2]);
$headerJson = json_decode($header, true);
$algorithm = $headerJson['alg'];
$x5cArray = $headerJson['x5c'];
foreach ($x5cArray as $X5C) {
$certificate = '-----BEGIN CERTIFICATE-----' . PHP_EOL;
$certificate .= chunk_split($X5C, 64, PHP_EOL);
$certificate .= '-----END CERTIFICATE-----' . PHP_EOL;
$certificates[] = openssl_x509_read($certificate);//OpenSSLCertificate
}
$applePemString = file_get_contents('/Users/Ray/PhpstormProjects/jws/AppleRootCA-G3.pem');
$applePem = openssl_x509_read($applePemString);
/* * * @param OpenSSLCertificate|string|resource $certificate * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key * @return int Returns 1 if the signature is correct, 0 if it is incorrect, and -1 on error. */
$nextCode = openssl_x509_verify($certificates[0], $certificates[1]);
printf("第2个证书给第1证书验证结果为:%s\n",$nextCode);
if ($nextCode == 1) {
$finalCode = openssl_x509_verify($certificates[1], $certificates[2]);
printf("第3个证书给第2个证书验证结果为:%s\n",$finalCode);
//如果验证正确的话,则用苹果签发的CA证书验证最后一个证书。
if ($finalCode == 1) {
$code = openssl_x509_verify($certificates[2],$applePem);
printf("根验证结果为:%s\n",$code);
if ($code == 1) {
//第一个证书是签署jws的证书
$pkey_object = openssl_pkey_get_public($certificates[0]);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array['key'];
//传入jws以及公钥匙以及加密算法
$decoded = JWT::decode($jws, new Key($publicKey, $algorithm));
//序列化解密后参数
$decoded_array = (array) $decoded;
echo "解密后的参数:\n" . print_r($decoded_array, true) . "\n";
}
}
}
/* 第一个证书给第二证书验证结果为:1 第二个证书给第三个证书验证结果为:1 根验证结果为:1 解密后的参数: Array ( ... ) */
这里主要做了这样几个事情:
-
获取JWS内的 header 内的alg算法以及x5c证书链。
-
通过x5c证书数组获取其第一个参数,通过 openssl 命令生成公钥证书按顺序压入数组。
-
获取苹果G3证书(此前由.cer转换.pem),通过 openssl 转换成 x509 证书对象。
-
按照证书链验证逻辑: 依次从后向前验证,完成苹果CA验证后,
验证证书链验证可信
。 -
通过 Firebase 的 JWT 库传入 jws 、alg、public key进行解析。
以上,我们需要验证公钥的真实性
,然后通过验证后的第一个公钥解密数据,然后和decoded Payload
数据做比对,或者和客户端数据做比对。
以上,谢谢观看。
今天的文章StoreKit2【附源码】JWS X.509证书链验证分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21054.html