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