苹果内购JWS回调解析

JWS简介

JWS 也就是 Json Web Signature,是构造 JWT 的基础结构(JWT 其实涵盖了 JWS 和 JWE 两类,其中 JWT 的载荷还可以是嵌套的 JWT),包括三部分 JOSE Header、JWS Payload、JWS Signature。
这里的 Signature 可以有两种生成方式,一种是标准的签名,使用非对称加密,因为私钥的保密性,能够确认签名的主体,同时能保护完整性;另一种是消息认证码 MAC(Message Authentication Code),使用对称秘钥,该秘钥需要在签发、验证的多个主体间共享,因此无法确认签发的主体,只能起到保护完整性的作用。
JWS 最终有两种序列化的表现形式,一种是 JWS Compact Serialization,为一串字符;另一种是 JWS JSON Serialization,是一个标准的 Json 对象,允许为同样的内容生成多个签名/消息认证码。

JWS Compact Serialization,各部分以 ‘.’ 分隔。

BASE64URL(UTF8(JWS Protected Header)) || ’.’ ||
BASE64URL(JWS Payload) || ’.’ ||
BASE64URL(JWS Signature)

JWS Json Serialization 还可以分为两种子格式:通用、扁平。
通用格式,最外层为 payload、signatures。signatures 中可以包含多个 json 对象,内层的 json 对象由 protected、header、signature 组成。不同的 protected header 生成不同的 Signature。

{
  "payload": "<payload contents>",
  "signatures": [
    {
      "protected": "<integrity-protected header 1 contents>",
      "header": "<non-integrity-protected header 1 contents>",
      "signature": "<signature 1 contents>"
    },
    {
      "protected": "<integrity-protected header N contents>",
      "header": "<non-integrity-protected header 1 contents>",
      "signature": "<signature N contents>"
    }
    ......
  ]
}

扁平格式,就是为只有一个 signature/mac 准备的。

{
  "payload": "<payload contents>",
  "protected": "<integrity-protected header contents>",
  "header": "<non-integrity-protected header contents>",
  "signature": "<signature contents>"
}

JOSE Header:Json Object Signing and Encryption Header。描述加密行为及其他用到的参数,是 JWS Protected/Unprotected Header 的合集。
JWS Protected Header,有完整性保护的头参数。
JWS Unprotected Header,无完整性保护头参数,仅出现在 JWS Json Serialization 格式中。
以下列出了 JOSE Header 中的规定参数,各参数详细信息请参考 rfc7515

头参数 全称 解释 必选
alg algorithm 指定签名算法,为 none 时,表示不使用签名来保护完整性 是
jku JWK set URL 签名所用 key 对应公匙所在的 URI 否
jwk json web key 签名所用 key 对应的公匙 否
kid key id 签名所用 key 的 id 否
typ Type 指明整个 jws 的媒体类型,JOSE 意味着为compact,JOSE+JSON意味着为json 否
cty Content Type 载荷的媒体类型 否
crit Critical 此字段列出的扩展头参数必须被接收者理解并处理,否则该 jws 无效,该字段为数组格式 否
x5u X.509 URL – 否
x5c X.509 Certificate Chain – 否
x5t X.509 Certificate SHA-1 Thumbprint – 否
x5t#S256 X.509 Certificate SHA-256 Thumbprint – 否

验证苹果JWS

主要要做两件事情1验证头部证书2验证签名

JWS头内容

array:2 [
  "alg" => "ES256"    // 加密算法
  "x5c" => array:3 [    //证书链 用于链状证书验证
    0 => "MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTIxMDgyNTAyNTAzNFoXDTIzMDkyNDAyNTAzM1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOoTcaPcpeipNL9eQ06tCu7pUcwdCXdN8vGqaUjd58Z8tLxiUC0dBeA+euMYggh1/5iAk+FMxUFmA2a1r4aCZ8SjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig97bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFCOCmMBq//1L5imvVmqX1oCYeqrMMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNoADBlAjEAl4JB9GJHixP2nuibyU1k3wri5psGIxPME05sFKq7hQuzvbeyBu82FozzxmbzpogoAjBLSFl0dZWIYl2ejPV+Di5fBnKPu8mymBQtoE/H2bES0qAs8bNueU3CBjjh1lwnDsI="
    1 => "MIIDFjCCApygAwIBAgIUIsGhRwp0c2nvU4YSycafPTjzbNcwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMjEwMzE3MjAzNzEwWhcNMzYwMzE5MDAwMDAwWjB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbsQKC94PrlWmZXnXgtxzdVJL8T0SGYngDRGpngn3N6PT8JMEb7FDi4bBmPhCnZ3/sq6PF/cGcKXWsL5vOteRhyJ45x3ASP7cOB+aao90fcpxSv/EZFbniAbNgZGhIhpIo4H6MIH3MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUu7DeoVgziJqkipnevr3rr9rLJKswRgYIKwYBBQUHAQEEOjA4MDYGCCsGAQUFBzABhipodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLWFwcGxlcm9vdGNhZzMwNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5hcHBsZS5jb20vYXBwbGVyb290Y2FnMy5jcmwwHQYDVR0OBBYEFD8vlCNR01DJmig97bB85c+lkGKZMA4GA1UdDwEB/wQEAwIBBjAQBgoqhkiG92NkBgIBBAIFADAKBggqhkjOPQQDAwNoADBlAjBAXhSq5IyKogMCPtw490BaB677CaEGJXufQB/EqZGd6CSjiCtOnuMTbXVXmxxcxfkCMQDTSPxarZXvNrkxU3TkUMI33yzvFVVRT4wxWJC994OsdcZ4+RGNsYDyR5gmdr0nDGg="
    2 => "MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA=="
  ]
]

解析过程

<?php
require_once "vendor/autoload.php";
$signedPayload = 'xxxxx.xxxxx.xxxx';

$components = explode('.', $signedPayload);

// 得到头
$headerJson = json_decode(base64_decode($components[0]),true);
//得到负载
$body = base64_decode($components[0]);
//得到签名
$signature = base64Url_decode($components[2]);

// 读取头部x5c证书
foreach ($headerJson['x5c'] as $x5c){
    $c = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
    $c .= chunk_split($x5c,64,PHP_EOL);
    $c .= '-----END CERTIFICATE-----'.PHP_EOL;
    $certificates[] = openssl_x509_read($c);
}

// 苹果root证书 从https://www.apple.com/certificateauthority/ 下载
// 个人理解:第一个签署 JWS,第二个签署第一个,第三个(也是最后一个)签署签署第二个。最后,因为最后一个是root,所以我们使用下载的那个来确保它是自己签名的。
$root = openssl_x509_read('-----BEGIN CERTIFICATE-----
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y
aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49
AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf
TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517
IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA
MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4
at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
6BgD56KyKA==
-----END CERTIFICATE-----');

// 开始循环循环验证书
for($i = 0; $i < count($certificates); $i++){
    if ($i == count($certificates) - 1){
        if (openssl_x509_verify($certificates[$i], $root) != 1){
            throw new Exception("Invalid Root Certificate");
        }
    }
    else{
        if (openssl_x509_verify($certificates[$i], $certificates[$i+1]) != 1){
            throw new Exception("Invalid Certificate");
        }
    }
}

//  载入第一张证书 指定算法
$key = new Key($certificates[0],'ES256');
$parsed_token = (array) \Firebase\JWT\JWT::decode($signedPayload, $key]);
dd($parsed_token);

JWT包decode方法 内部已经帮助咱们验签了

public static function decode(
    string $jwt,
    $keyOrKeyArray
): stdClass {
    // Validate JWT
    $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;

    if (empty($keyOrKeyArray)) {
        throw new InvalidArgumentException('Key may not be empty');
    }
    $tks = \explode('.', $jwt);
    if (\count($tks) !== 3) {
        throw new UnexpectedValueException('Wrong number of segments');
    }
    list($headb64, $bodyb64, $cryptob64) = $tks;
    $headerRaw = static::urlsafeB64Decode($headb64);
    if (null === ($header = static::jsonDecode($headerRaw))) {
        throw new UnexpectedValueException('Invalid header encoding');
    }
    $payloadRaw = static::urlsafeB64Decode($bodyb64);
    if (null === ($payload = static::jsonDecode($payloadRaw))) {
        throw new UnexpectedValueException('Invalid claims encoding');
    }
    if (\is_array($payload)) {
        // prevent PHP Fatal Error in edge-cases when payload is empty array
        $payload = (object) $payload;
    }
    if (!$payload instanceof stdClass) {
        throw new UnexpectedValueException('Payload must be a JSON object');
    }
    $sig = static::urlsafeB64Decode($cryptob64);
    if (empty($header->alg)) {
        throw new UnexpectedValueException('Empty algorithm');
    }
    if (empty(static::$supported_algs[$header->alg])) {
        throw new UnexpectedValueException('Algorithm not supported');
    }

    $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);

    // Check the algorithm
    if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
        // See issue #351
        throw new UnexpectedValueException('Incorrect key for this algorithm');
    }
    if ($header->alg === 'ES256' || $header->alg === 'ES384') {
        // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
        $sig = self::signatureToDER($sig);
    }
    if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
        throw new SignatureInvalidException('Signature verification failed');
    }

    // Check the nbf if it is defined. This is the time that the
    // token can actually be used. If it's not yet that time, abort.
    if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
        throw new BeforeValidException(
            'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
        );
    }

    // Check that this token has been created before 'now'. This prevents
    // using tokens that have been created for later use (and haven't
    // correctly used the nbf claim).
    if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
        throw new BeforeValidException(
            'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
        );
    }

    // Check if this token has expired.
    if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
        throw new ExpiredException('Expired token');
    }

    return $payload;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇