【问题标题】:Verifying Apple's x5c JWT signatures with Firebase php-jwt, how to decode?使用 Firebase php-jwt 验证 Apple 的 x5c JWT 签名,如何解码?
【发布时间】:2022-10-31 07:37:59
【问题描述】:

我正在尝试使用 Apple 的 App Store Server API 来获取有关在我的 iOS 应用中进行的应用内购买的交易信息。他们的服务器 api 使用 JWT 来传输/签署数据。我能够成功地从 Apple 获取包含一组签名交易 JWT 的数据:

{
    "status":0,
    "signedTransactions":[
        "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWF..."
    ]
}

到目前为止,一切都很好。但是,当我尝试使用 Firebase 的 php-jwt 库对签名的交易 JWT 进行解码时,我遇到了致命错误。我首先尝试了 Firebase 的 php-jwt 库中的示例代码:

$signedTransactionJWT = $response['signedTransactions'][0];
$privateKeyText = file_get_contents('/private/key/from/appstoreconnect.p8');
$decodedTransactionPayload = JWT::decode($signedTransactionJWT, new Key($privateKeyText, 'ES256'));

但这给了我:

openssl_verify(): supplied key param cannot be coerced into a public key

后来在网上搜索了一堆关于 Apple 公钥的信息,我尝试使用 Apple 网站上发布的 auth 密钥:

$signedTransactionJWT = $response['signedTransactions'][0];

$appleKeysText = file_get_contents('/file/downloaded/from https://appleid.apple.com/auth/keys');

$jwks = json_decode($appleKeysText, true);
$keyset = JWK::parseKeySet($jwks);
$decodedTransactionPayload = JWT::decode($signedTransactionJWT, $keyset);

...但它会出现以下错误:

Fatal error: Uncaught UnexpectedValueException: "kid" empty, unable to lookup correct key

我查看了 JWT::decode() 方法,它在已签名交易 JWT 的标头中寻找密钥 ID(“kid”),但 Apple 并未在已签名交易的标头中提供“kid”智威汤逊。标头的结构如下所示:

{
    "alg": "ES256",
    "x5c": [
        "MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1M...",
        "MIIDFjCCApygAwIBAgIUIsGhRwp0c2nvU4YSycafPTjzbNcwCgYIKoZIzj0EA...",
        "MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEA..."
    ]
}

这是我第一次使用 JWT,所以我正在尽我所能理解这里的各种交互部分。根据有关 Apple 的 App Store Server API 的 WWDC 视频,标头的“x5c”部分应该用于在没有任何其他外部 Web 调用的情况下验证交易。所以,我觉得我不需要从https://appleid.apple.com/auth/keys 获取那些 JWT 密钥。据我了解,这个想法是签名应该是独立的。

如何正确解码来自 Apple 的 JWT,以便使用 Firebase 的 php-jwt 库验证有效负载?

更新

根据 Gary 的回答,我需要使用 x5c 数组中的第一项作为公钥。他提供了一些非常有用的链接和示例。希望这会引导我找到正确的答案,但我仍然遇到问题:

list($headerb64, $bodyb64, $cryptob64) = explode('.', $jwt);
$headertext = JWT::urlsafeB64Decode($headerb64);
$header = JWT::jsonDecode($headertext);
$keytext = $header->x5c[0];
$wrappedkeytext = trim(chunk_split($keytext, 64));
$publickey = <<<EOD
-----BEGIN PUBLIC KEY-----
$wrappedkeytext
-----END PUBLIC KEY-----
EOD;
print "public key:\n$publickey\n";
$decoded = JWT::decode($jwt, new Key($publickey, $header->alg));

按照指示,我解码了标题,抓取了第一项,将其转换为公钥格式的字符串,然后尝试使用它来解码$jwt,但我收到了这个错误:

警告:openssl_verify():提供的密钥参数不能被强制转换为 公钥

致命错误:未捕获的 DomainException:OpenSSL 错误: 错误:0909006C:PEM 例程:get_name:没有起始行

我打印出公钥字符串,以便确保我的格式正确。它看起来对我来说是正确的,但我对此很陌生,所以我可能会遗漏一些微妙的问题。起初,我尝试将内容全部放在一行上,但出现上述错误。然后我将其拆分为 64 个字符长的行,因为我发现一些文档说这些文本块的长度应限制为 64 个字符。但我仍然收到相同的错误消息。

-----BEGIN PUBLIC KEY-----
MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1MUQw
QgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0
aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxl
IEluYy4xCzAJBgNVBAYTAlVTMB4XDTIxMDgyNTAyNTAzNFoXDTIzMDkyNDAyNTAz
M1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5l
cyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lk
ZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYD
VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOoTcaPcpeipNL9eQ06t
Cu7pUcwdCXdN8vGqaUjd58Z8tLxiUC0dBeA+euMYggh1/5iAk+FMxUFmA2a1r4aC
Z8SjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig9
7bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2Nl
cnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2Nz
cC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggEN
BgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRo
aXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBv
ZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRp
b25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9u
IHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFw
cGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFCOCmMBq//1L
5imvVmqX1oCYeqrMMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAK
BggqhkjOPQQDAwNoADBlAjEAl4JB9GJHixP2nuibyU1k3wri5psGIxPME05sFKq7
hQuzvbeyBu82FozzxmbzpogoAjBLSFl0dZWIYl2ejPV+Di5fBnKPu8mymBQtoE/H
2bES0qAs8bNueU3CBjjh1lwnDsI=
-----END PUBLIC KEY-----

【问题讨论】:

    标签: php firebase jwt app-store-connect


    【解决方案1】:

    我在加里的回答提供的出色帮助下想通了。我之所以能弄明白,是因为他不仅提供了一个例子来说明他的意思,而且他还与实际标准联系起来,一些详细的阅读帮助我找出了哪里出了问题。

    x5c 数组中的第一项不是公钥,而是证书持有公钥。因此,当我尝试将这些数据放入 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----- 块时,它不起作用。

    x5c 数组中的证书是 DER 证书,但 openssl 在进行验证时需要 PEM 证书。据我所知,将 DER 证书转换为 PEM 证书只涉及获取 DER 数据,对其进行 base64 编码,将其限制为每行 64 个字符宽,然后将其包装在 -----BEGIN CERTIFICATE----------END CERTIFICATE----- 中。但是 x5c 数组中的 DER 数据已经是 base64 编码的,所以我们可以跳过这一步。

    list($headerb64, $bodyb64, $cryptob64) = explode('.', $jwt);
    $headertext = JWT::urlsafeB64Decode($headerb64);
    $header = JWT::jsonDecode($headertext);
    $dercertificateb64 = $header->x5c[0];
    $wrappedcertificatetext = trim(chunk_split($dercertificateb64, 64));
    $certificate = <<<EOD
    -----BEGIN CERTIFICATE-----
    $wrappedcertificatetext
    -----END CERTIFICATE-----
    EOD;
    print "cert:
    $certificate
    ";
    

    Firebase 的 php-jwt 库将 OpenSSLAsymmetricKey 对象作为关键数据,openssl_pkey_get_public() 将返回该类型的对象。您可以将 PEM 证书字符串传递给该函数,它会解析并提取公钥:

    $publickey = openssl_pkey_get_public($certificate);
    $decoded = JWT::decode($jwt, new Key($publickey, $header->alg));
    

    【讨论】:

      【解决方案2】:

      正如this section of RFC7515 中所述,Apple 正在提供独立的 JWT。要验证 JWT,请执行以下步骤:

      • 在不验证的情况下解码 JWT,以读取 x5c 数组中的第一个值,这是一个包含令牌签名公钥的 base64 编码的 DER 证书。我认为使用这个库,您可以只调用单参数版本的解码,而无需密钥对象。

      • 接下来形成一个公钥对象,然后使用两个参数调用 decode,如this Firebase example。您可能需要添加周围的 BEGIN / END 行,并且您不需要使用任何私钥。一些库可能对 DER / PEM 格式有特殊要求,或者要求编码的证书在一行中表示。

      $publicKey = <<<EOD
      -----BEGIN CERTIFICATE-----
      MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
      4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
      0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
      ehde/zUxo6UvS7UrBQIDAQAB
      -----END CERTIFICATE-----
      EOD;
      
      • 最后,您需要根据受信任颁发者的白名单(在您的案例中是 Apple 的根 CA)验证 x5c 阵列中的信任链。这可以防止恶意方向您发送带有不受信任的 x5c 数组的 JWT。我相信PHP answer 解决了这一点。

      【讨论】:

      • 非常感谢您的回答。这是我第一次读到一些关于这个问题的东西,实际上有点道理。 :) 我尝试了第 1 步和第 2 步,但得到了Warning: openssl_verify(): supplied key param cannot be coerced into a public keyFatal error: Uncaught DomainException: OpenSSL error: error:0909006C:PEM routines:get_name:no start line。我已经用代码和这些错误消息更新了我的帖子。如果您有任何见解,将不胜感激。
      • 嗯 - 也许试试 BEGIN CERTIFICATE 看看这是否能让你更进一步,因为这样更准确。另请参阅 Firebase 使用的密钥对象及其所需的确切输入,例如 DER v PEM 证书。我已经在其他技术堆栈中做到了这一点,但不是你正在使用的那个。
      • 是的,就是这样。在阅读了 RFC 的链接后,我想通了,并提出了一个解决方案。我现在正在写出所有的信息和步骤。
      猜你喜欢
      • 2020-12-04
      • 2021-06-25
      • 2018-09-16
      • 2016-06-12
      • 2020-08-07
      • 2017-03-06
      • 2018-12-13
      • 2016-12-16
      相关资源
      最近更新 更多