前语: 有不少人反馈文章过于长,邮件内容不利于阅读连贯性。本文删去邮件内容,只留下疑点和解惑文字。

StoreKit2 收据更新,原先appStoreReceiptURL的验证逻辑不可用。

但是咱们后台的发货需求依据验证收据的实在性,这就导致咱们要运用StoreKit2,就有必要更新咱们的发货逻辑,验证新的收据。

Apple官方能够给出的材料如下

苹果的签名校验示意图,视频并没有对此做出过多的解释

StoreKit2【附源码】JWS X.509证书链验证

wwdc21-10174 在您的服务器上管理运用内购买

在上述视频中Apple解释了怎么验证签名,但是这在咱们了解以往验证流程看起来很是含糊。

StoreKit2【附源码】JWS X.509证书链验证

其间关于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

但是存在几个疑惑点,一向查不到相关的信息:

  1. algx5c 咱们获取到了,但是最喜欢的暗码库是什么意思?咱们到底该怎么解密?
  2. 查阅材料发现 x5c 验证需求一个根证书,苹果并未供给。
  3. 或许是出于推荐客户端自我验证的方式,苹果除了供给以上信息, 并未泄漏其他服务器验证签名的信息以及示例代码。 归纳一下: 我需求:
  • 称手的兵器: 解密工具包
  • 解密所需求的信息: 公钥,算法等…
  • 验证公钥可信的证书

首先不确定运用哪一个根证书进行验证公钥颗心,所以联络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找到答案。

StoreKit2【附源码】JWS X.509证书链验证

  1. 进入材料库
  2. 挑选你编写的言语
  3. 挑选你符合你算法的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
(
    ...
)
*/

这里主要做了这样几个工作:

  1. 获取JWS内的 header 内的alg算法以及x5c证书链。

  2. 经过x5c证书数组获取其第一个参数,经过 openssl 命令生成公钥证书按顺序压入数组。

  3. 获取苹果G3证书(此前由.cer转换.pem),经过 openssl 转换成 x509 证书目标。

  4. 依照证书链验证逻辑: 依次从后向前验证,完结苹果CA验证后,验证证书链验证可信

  5. 经过 Firebase 的 JWT 库传入 jws 、alg、public key进行解析。

以上,咱们需求验证公钥的实在性,然后经过验证后的第一个公钥解密数据,然后和decoded Payload数据做比对,或者和客户端数据做比对。

以上,谢谢观看。