php app-store-server 苹果接口加解密操作,v2 通知回调
class AppleServiceSign { const CONSUMPTION_REQUEST = 'CONSUMPTION_REQUEST'; const DID_CHANGE_RENEWAL_PREF = 'DID_CHANGE_RENEWAL_PREF'; const DID_CHANGE_RENEWAL_STATUS = 'DID_CHANGE_RENEWAL_STATUS'; const DID_FAIL_TO_RENEW = 'DID_FAIL_TO_RENEW'; const DID_RENEW = 'DID_RENEW'; const EXPIRED = 'EXPIRED'; const GRACE_PERIOD_EXPIRED = 'GRACE_PERIOD_EXPIRED'; const OFFER_REDEEMED = 'OFFER_REDEEMED'; const PRICE_INCREASE = 'PRICE_INCREASE'; const REFUND = 'REFUND'; const REFUND_DECLINED = 'REFUND_DECLINED'; const RENEWAL_EXTENDED = 'RENEWAL_EXTENDED'; const REVOKE = 'REVOKE'; /** * sign * @param $payload * @param $header * @param $key * @return string * @throws Exception */ public static function sign($payload, $header, $key) { $segments = []; $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); $signing_input = implode('.', $segments); $signature = static::_sign($signing_input, $key); $segments[] = static::urlsafeB64Encode($signature); return implode('.', $segments); } /** * openssl_sign * @param $msg * @param $key * @return string * @throws Exception */ private static function _sign($msg, $key) { $key = openssl_pkey_get_private($key); if (!$key) { throw new \Exception(openssl_error_string()); } $signature = ''; $success = openssl_sign($msg, $signature, $key, OPENSSL_ALGO_SHA256); if (!$success) { throw new \Exception("OpenSSL unable to sign data"); } else { $signature = self::fromDER($signature, 64); return $signature; } } /** * jsonDecode * @param $input * @return mixed * @throws Exception */ private static function jsonDecode($input) { if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); } else { $max_int_length = strlen((string)PHP_INT_MAX) - 1; $json_without_bigints = preg_replace('/:\s*(-?\d{' . $max_int_length . ',})/', ': "$1"', $input); $obj = json_decode($json_without_bigints); } if (function_exists('json_last_error') && $errno = json_last_error()) { throw new \Exception(json_last_error_msg()); } elseif ($obj === null && $input !== 'null') { throw new \Exception('Null result with non-null input'); } return $obj; } /** * jsonEncode * @param $input * @return false|string * @throws Exception */ private static function jsonEncode($input) { $json = json_encode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { throw new \Exception(json_last_error_msg()); } elseif ($json === 'null' && $input !== null) { throw new \Exception('Null result with non-null input'); } return $json; } /** * urlsafeB64Decode * @param $input * @return false|string */ private static function urlsafeB64Decode($input) { $remainder = strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; $input .= str_repeat('=', $padlen); } return base64_decode(strtr($input, '-_', '+/')); } /** * urlsafeB64Encode * @param $input * @return mixed */ private static function urlsafeB64Encode($input) { return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); } /** * toDER * @param string $signature * @param int $partLength * @return string * @throws Exception */ private static function toDER(string $signature, int $partLength): string { $signature = \unpack('H*', $signature)[1]; if (\mb_strlen($signature, '8bit') !== 2 * $partLength) { throw new \Exception('Invalid length.'); } $R = \mb_substr($signature, 0, $partLength, '8bit'); $S = \mb_substr($signature, $partLength, null, '8bit'); $R = self::preparePositiveInteger($R); $Rl = \mb_strlen($R, '8bit') / 2; $S = self::preparePositiveInteger($S); $Sl = \mb_strlen($S, '8bit') / 2; $der = \pack('H*', '30' . ($Rl + $Sl + 4 > 128 ? '81' : '') . \dechex($Rl + $Sl + 4) . '02' . \dechex($Rl) . $R . '02' . \dechex($Sl) . $S ); return $der; } /** * toDER * @param string $der * @param int $partLength * @return string */ private static function fromDER(string $der, int $partLength): string { $hex = \unpack('H*', $der)[1]; if ('30' !== \mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE throw new \RuntimeException(); } if ('81' === \mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128 $hex = \mb_substr($hex, 6, null, '8bit'); } else { $hex = \mb_substr($hex, 4, null, '8bit'); } if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new \RuntimeException(); } $Rl = \hexdec(\mb_substr($hex, 2, 2, '8bit')); $R = self::retrievePositiveInteger(\mb_substr($hex, 4, $Rl * 2, '8bit')); $R = \str_pad($R, $partLength, '0', STR_PAD_LEFT); $hex = \mb_substr($hex, 4 + $Rl * 2, null, '8bit'); if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new \RuntimeException(); } $Sl = \hexdec(\mb_substr($hex, 2, 2, '8bit')); $S = self::retrievePositiveInteger(\mb_substr($hex, 4, $Sl * 2, '8bit')); $S = \str_pad($S, $partLength, '0', STR_PAD_LEFT); return \pack('H*', $R . $S); } /** * preparePositiveInteger * @param string $data * @return string */ private static function preparePositiveInteger(string $data): string { if (\mb_substr($data, 0, 2, '8bit') > '7f') { return '00' . $data; } while ('00' === \mb_substr($data, 0, 2, '8bit') && \mb_substr($data, 2, 2, '8bit') <= '7f') { $data = \mb_substr($data, 2, null, '8bit'); } return $data; } /** * retrievePositiveInteger * @param string $data * @return string */ private static function retrievePositiveInteger(string $data): string { while ('00' === \mb_substr($data, 0, 2, '8bit') && \mb_substr($data, 2, 2, '8bit') > '7f') { $data = \mb_substr($data, 2, null, '8bit'); } return $data; } static function verifyAppleToken($token) { $arr = explode('.',$token); if(count($arr) != 3){ return []; } $header = $arr[0]; $header = base64_decode($header); $header = json_decode($header,true); if(empty($header)){ return []; } if($header['alg'] != 'ES256'){ return []; } $data = $arr[1]; $data = base64_decode($data); $data = json_decode($data,true); if(empty($data)){ return []; } $sign = $arr[2]; if(empty($sign)){ return []; } return ['header'=>$header,'payload'=>$data,'sign'=>$sign]; } }
jwt类型的token
protected function createSign() { $kid = config('appleSecret')['yaoqu_ios']['kid']; $issuerId = config('appleSecret')['yaoqu_ios']['issuser_id']; $p8File = config('appleSecret')['yaoqu_ios']['p8_file']; $key = <<<EOF {$p8File} EOF; $header = [ 'alg' => 'ES256', 'kid' => $kid, 'typ' => 'JWT', ]; $payload = [ 'iss' => $issuerId, 'iat' => intval(time()), 'exp' => intval(time() + 3600), 'aud' => 'appstoreconnect-v1', 'bid' => config('appleSecret')['yaoqu_ios']['bid'], ]; $token = AppleServiceSign::sign($payload, $header, $key); return $token; }
解析notify v2
protected function checkAppleSign($jwt) { $data = AppleServiceSign::verifyAppleToken($jwt); Log::channel('daily')->info("apple-server-yuanshi-callback:" . $jwt); // dd($data); if (empty($data)) { throw new \Exception('check err'); } $applePemPath = base_path("assets/iosnotice/" . $this->project . "/AppleRootCA-G3.pem"); $pem = file_get_contents($applePemPath); $header = $data['header']; $algorithm = $header['alg']; $x5c = $header['x5c']; // array $certificate = $x5c[0]; $intermediate_certificate = $x5c[1]; $root_certificate = $x5c[2]; $certificate = "-----BEGIN CERTIFICATE-----\n" . $certificate . "\n-----END CERTIFICATE-----"; $intermediate_certificate = "-----BEGIN CERTIFICATE-----\n" . $intermediate_certificate . "\n-----END CERTIFICATE-----"; $root_certificate = "-----BEGIN CERTIFICATE-----\n" . $root_certificate . "\n-----END CERTIFICATE-----"; if (openssl_x509_verify($intermediate_certificate, $root_certificate) != 1) { throw new \Exception('Intermediate and Root certificate do not match'); } if (openssl_x509_verify($root_certificate, $pem) == 1) { $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']; $payload = $data['payload']; $notificationType = $payload['notificationType']; $transactionInfo = $payload['data']['signedTransactionInfo']; $signedRenewalInfo = $payload['data']['signedRenewalInfo']; $transactionDecodedData = JWT::decode($transactionInfo, new Key($publicKey, $algorithm)); $signedRenewalDecodedData = JWT::decode($signedRenewalInfo, new Key($publicKey, $algorithm)); $returnData = [ 'notificationType' => $data['payload']['notificationType'], 'notificationUUID' => $data['payload']['notificationUUID'], 'appAppleId' => $data['payload']['data']['appAppleId'], 'bundleId' => $data['payload']['data']['bundleId'], 'bundleVersion' => $data['payload']['data']['bundleVersion'], 'environment' => $data['payload']['data']['environment'],//通知适用的服务器环境,或者sandbox或production。 'signedTransactionInfo' => $transactionDecodedData,//交易信息 'signedRenewalInfo' => $signedRenewalDecodedData,//仅当为自动续订订阅发送的通知时,才会显示此字段 'status' => $data['payload']['data']['status'],//仅当为自动续订订阅发送的通知时,才会显示此字段 ]; return $returnData; } else { throw new \Exception('Header is not valid'); } }
生成 appleRootCa-G3.pem文件的方式
https://www.apple.com/certificateauthority/AppleRootCA-G3.cer 先下载 xxx.cer文件
openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem