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

 

 
posted @ 2023-09-04 09:46  专心写代码  阅读(15)  评论(0编辑  收藏  举报