前语: 有不少人反馈文章过于长,邮件内容不利于阅读连贯性。本文删去邮件内容,只留下疑点和解惑文字。
StoreKit2 收据更新,原先
appStoreReceiptURL
的验证逻辑不可用。但是咱们后台的发货需求依据验证收据的实在性,这就导致咱们要运用StoreKit2,就有必要更新咱们的发货逻辑,验证新的收据。
Apple官方能够给出的材料如下
苹果的签名校验示意图,视频并没有对此做出过多的解释
wwdc21-10174 在您的服务器上管理运用内购买
在上述视频中Apple解释了怎么验证签名,但是这在咱们了解以往验证流程看起来很是含糊。
其间关于JWS详细的数据格式我会在下面解释到。
在API文档上留下的一些参数
@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
数据做比对,或者和客户端数据做比对。
以上,谢谢观看。