在后端验证Apple StoreKit2应用内购买收据jwsRepresentation(理想情况下是节点,但任何东西都可以)

问题描述 投票:0回答:4
如何在 Node 后端验证来自 StoreKit2 的应用内购买 JWS 表示?

解码有效负载很容易,但我无法在任何地方找到 Apple 用于签署这些 JWS/JWT 的公钥。在我使用 JWT 的任何其他时间,您只需使用节点

jsonwebtoken

 库并传入签名者公钥或共享密钥,无论是配置还是从 JWK 获取。

我可以使用

node-jose

 
j.JWS.createVerify().verify(jwsString, {allowEmbeddedKey: true}).then(r => obj = r)
 轻松解码 JWS,这给了我一个像这样的对象:

{ protected: [ 'alg', 'x5c' ], header: { alg: 'ES256', x5c: [ 'MIIEMDueU3...', 'MII..., 'MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0...' ] }, payload: <Buffer 7b 22 74 72 61 6e 73 61 63 74 69 6f 6e 49 64 22 3a 22 31 30 30 30 30 30 30 38 38 36 39 31 32 38 39 30 22 2c 22 6f 72 69 67 69 6e 61 6c 54 72 61 6e 73 ... 420 more bytes>, signature: <Buffer f8 85 65 79 a1 dc 74 dd 90 80 0a a4 08 85 30 e7 22 80 4c 20 66 09 0b 84 fc f4 e5 57 53 da d5 6f 13 c6 8f 56 e8 29 67 5c 95 a6 27 33 47 1e fe e9 6e 41 ... 14 more bytes>, key: JWKBaseKeyObject { keystore: JWKStore {}, length: 256, kty: 'EC', kid: 'Prod ECC Mac App Store and iTunes Store Receipt Signing', use: '', alg: '' } }
并且很容易 JSON.parse 有效负载并获取我想要的数据。  但是,我如何使用 

x5c

 字段中的证书链来验证其真实性

谢谢!

ios jwt in-app-purchase storekit
4个回答
9
投票
将所有信息拼凑在一起是相当具有挑战性的,但以下是如何在 NodeJS 中做到这一点。请注意,最新的 Node 支持内置加密,这使得它变得更加容易。这是我的代码以及必要的注释。

const jwt = require('jsonwebtoken'); const fs = require('fs'); const {X509Certificate} = require('crypto'); async function decode(signedInfo) { // MARK: - Creating certs using Node's new build-in crypto function generateCertificate(cert) { // MARK: - A simple function just like the PHP's chunk_split, used in generating pem. function chunk_split(body, chunklen, end) { chunklen = parseInt(chunklen, 10) || 76; end = end || '\n'; if (chunklen < 1) {return false;} return body.match(new RegExp(".{0," + chunklen + "}", "g")).join(end); } return new X509Certificate(`-----BEGIN CERTIFICATE-----\n${chunk_split(cert,64,'\n')}-----END CERTIFICATE-----`); } // MARK: - Removing the begin/end lines and all new lines/returns from pem file for comparison function getPemContent(path) { return fs.readFileSync(path) .toString() .replace('-----BEGIN CERTIFICATE-----', '') .replace('-----END CERTIFICATE-----', '') .replace(/[\n\r]+/g, ''); } // MARK: - The signed info are in three parts as specified by Apple const parts = signedInfo.split('.'); if (parts.length !== 3) { console.log('The data structure is wrong! Check it! '); return null; } // MARK: - All the information needed for verification is in the header const header = JSON.parse(Buffer.from(parts[0], "base64").toString()); // MARK: - The chained certificates const certificates = header.x5c.map(cert => generateCertificate(cert)); const chainLength = certificates.length; // MARK: - Leaf certificate is the last one const leafCert = header.x5c[chainLength-1]; // MARK: - Download .cer file at https://www.apple.com/certificateauthority/. Convert to pem file with this command line: openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem const AppleRootCA = getPemContent('AppleRootCA-G3.pem'); // MARK: - The leaf cert should be the same as the Apple root cert const isLeafCertValid = AppleRootCA === leafCert; if (!isLeafCertValid) { console.log('Leaf cert not valid! '); return null; } // MARK: If there are more than one certificates in the chain, we need to verify them one by one if (chainLength > 1) { for (var i=0; i < chainLength - 1; i++) { const isCertValid = certificates[i].verify(certificates[i+1].publicKey); if (!isCertValid) { console.log(`Cert ${i} not valid! `); return null; } } } return jwt.decode(signedInfo); }
祝你好运!


7
投票
终于想通了。事实证明,我们需要一个“硬编码”证书来检查。

Apple 在

其网站上提供了所需的证书。您已经下载了根证书(因为这是签署整个链的证书),但您也可以获得中间证书。

下载后,将其转换为

.pem

:

$ openssl x509 -inform der -in apple_root.cer -out apple_root.pem
然后您需要做的就是对照 JWS 中的内容验证它们(以下内容在 

PHP

 中,但您应该了解要点):

if (openssl_x509_verify($jws_root_cert, $downloaded_apple_root_cert) == 1){ //valid }
希望这对其他人有帮助!


7
投票
您需要使用

header

 验证 
payload
sign
,如 WWDC 视频中所述:

https://developer.apple.com/videos/play/wwdc2022/10040/ https://developer.apple.com/videos/play/wwdc2021/10174/

enter image description here

但是,如果您不了解 JWT,那么执行此操作比您想象的要复杂,因为 Apple 没有提供任何文档来执行此操作,他们只对您说“使用您最喜欢的加密库来验证数据”。

经过大量研究,终于找到了使用

PHP 8.1

Laravel
 的解决方案。

首先你需要安装这个库

https://github.com/firebase/php-jwt:

composer require firebase/php-jwt
然后您需要实现以下方法来验证交易中的

JWT

use Firebase\JWT\JWT; use Firebase\JWT\Key; ... public function validateJwt($jwt) { $components = explode('.', $jwt); if (count($components) !== 3) { throw new \Exception('JWS string must contain 3 dot separated component.'); } $header = base64_decode($components[0]); $headerJson = json_decode($header,true); $this->validateAppleRootCA($headerJson); $jwsParsed = (array) $this->decodeCertificate($jwt, $headerJson, 0); for ($i = 1; $i < count($headerJson) - 1; $i++) { $this->decodeCertificate($jwt, $headerJson, $i); } // If the signature and the jws is invalid, it will thrown an exception // If the signature and the jws is valid, it will create the $decoded object // You can use the $decoded object as an array if you need: $transactionId = $jwsParsed['transactionId']; } private function validateAppleRootCA($headerJson) { $lastIndex = array_key_last($headerJson['x5c']); $certificate = $this->getCertificate($headerJson, $lastIndex); // As Oliver Zhang says in their NodeJS example, download the .cer file at https://www.apple.com/certificateauthority/. Convert to pem file with this command line: openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem // In Laravel, this location is at storage/keys/AppleRootCA-G3.pem $appleRootCA = file_get_contents(storage_path('keys/AppleRootCA-G3.pem')); if ($certificate != $appleRootCA) { throw new \Exception('jws invalid'); } } private function getCertificate($headerJson, $certificateIndex) { $certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL; $certificate .= chunk_split($headerJson['x5c'][$certificateIndex],64,PHP_EOL); $certificate .= '-----END CERTIFICATE-----'.PHP_EOL; return $certificate; } private function decodeCertificate($jwt, $headerJson, $certificateIndex) { $certificate = $this->getCertificate($headerJson, 0); $cert_object = openssl_x509_read($certificate); $pkey_object = openssl_pkey_get_public($cert_object); $pkey_array = openssl_pkey_get_details($pkey_object); $publicKey = $pkey_array['key']; $jwsParsed = null; try { $jwsDecoded = JWT::decode($jwt, new Key($publicKey, 'ES256')); $jwsParsed = (array) $jwsDecoded; } catch (SignatureInvalidException $e) { throw new \Exception('signature invalid'); } return $jwsParsed; }
要调用该函数,您需要从交易中传递

jwt

$jwt = 'eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUl...'; validateJwt($jwt);
    

0
投票
在 NodeJS 中

import { readFileSync } from 'fs'; import { join as pathJoin } from 'path'; import { X509Certificate } from 'crypto'; import { verify as verityJwt, JwtPayload } from 'jsonwebtoken'; /** * More info: https://developer.apple.com/videos/play/wwdc2022/10040/ */ async function verifyIosSubscription({ token }: { token: string }): Promise<boolean> { console.log('[IAP_SERVICE] verifyIosSubscription start'); // Decode header const jwsHeader = JSON.parse( Buffer.from(token.split('.')[0], 'base64url').toString(), ); // Create JWS certificates (last is root, first is leaf) const jwsCertificates: X509Certificate[] = jwsHeader.x5c.map( (certStr) => new X509Certificate( `-----BEGIN CERTIFICATE-----\n${certStr}\n-----END CERTIFICATE-----`, ), ); const jwsRootCertificate = jwsCertificates[jwsCertificates.length - 1]; const jwsLeafCertificate = jwsCertificates[0]; // Get Apple Root Certificate (downloaded here: https://www.apple.com/certificateauthority/), convert using "openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem" const appleRootCertificate = new X509Certificate( readFileSync( pathJoin( __dirname, '..', 'assets', 'certificates', 'AppleRootCA-G3.pem', ), 'utf8', ), ); // Root certificate should match Apple certificate if ( appleRootCertificate.fingerprint512 !== jwsRootCertificate.fingerprint512 ) { console.log( '[IAP_SERVICE] jws root certificate do not match apple root certificate', ); return false; } // Verify certification chained signature for (let i = 0; i < jwsCertificates.length - 1; i++) { // i should be signed by i+1 if (!jwsCertificates[i].verify(jwsCertificates[i + 1].publicKey)) { console.log( '[IAP_SERVICE] jws chained certificate not correctly signed', i, 'was not signed by', i + 1, ); return false; } } // get jws payload using leaf certificate const jwsPayload: JwtPayload | undefined = verityJwt( token, jwsLeafCertificate.publicKey, ) as JwtPayload | undefined; // No jwsPayload if (!jwsPayload) { console.log('[IAP_SERVICE] undefined jwsPayload'); return false; } // Verify app bundle if (jwsPayload.bundleId !== appBundleId) { console.log('[IAP_SERVICE] wrong bundleId', jwsPayload.bundleId); return false; } console.log('[IAP_SERVICE] verifyIosSubscription end'); return true; }
    
© www.soinside.com 2019 - 2024. All rights reserved.