StoreKit2【干货版】 X.509证书链验证之路

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

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

但是我们后台的发货需要依据验证收据的真实性,这就导致我们要使用StoreKit2,就必须更新我们的发货逻辑,验证新的收据。

Apple官方可以给出的资料如下

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

企业微信截图_d683a1a5-4cb3-4b1a-8ff8-21bdd5c56eac.png

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

在上述视频中Apple解释了如何验证签名,但是这在我们熟悉以往验证流程看起来很是模糊。

image.png

其中关于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找到答案。

image.png

  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证书链可信呢?

我们在回顾一下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数据做比对,或者和客户端数据做比对。

以上,谢谢观看。

猜你喜欢

转载自juejin.im/post/7039970403770433544