Apple, войдите в "invalid_client", подпишите JWT для аутентификации, используя PHP и openSSL - PullRequest
2 голосов
/ 14 января 2020

Я пытаюсь реализовать вход Apple в приложение Android, используя эту библиотеку . Основной поток описан в документации: библиотека возвращает код авторизации на стороне Android. Этот код авторизации должен быть отправлен на мой бэкэнд, который, в свою очередь, отправляет его на серверы Apple, чтобы получить токен доступа.

Как описано здесь и здесь , чтобы получить токен доступа, нам нужно отправить в Apple API список параметров, код авторизации и подпись JWT. В частности, JWT должен быть подписан с помощью алгоритма ES256 с использованием закрытого ключа .p8, который должен быть сгенерирован и загружен с портала разработчиков Apple. Apple do c

Вот мой PHP скрипт:

<?php

$authorization_code = $_POST('auth_code');

$privateKey = <<<EOD
-----BEGIN PRIVATE KEY-----
my_private_key_downloaded_from_apple_developer_portal (.p8 format)
-----END PRIVATE KEY-----
EOD;

$kid = 'key_id_of_the_private_key'; //Generated in Apple developer Portal
$iss = 'team_id_of_my_developer_profile';
$client_id = 'identifier_setted_in_developer_portal'; //Generated in Apple developer Portal

$signed_jwt = $this->generateJWT($kid, $iss, $client_id, $privateKey);

$data = [
            'client_id' => $client_id,
            'client_secret' => $signed_jwt,
            'code' => $authorization_code,
            'grant_type' => 'authorization_code'
        ];
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://appleid.apple.com/auth/token');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$serverOutput = curl_exec($ch);

curl_close ($ch);

var_dump($serverOutput);

function generateJWT($kid, $iss, $sub, $key) {

    $header = [
        'alg' => 'ES256',
        'kid' => $kid
    ];
    $body = [
        'iss' => $iss,
        'iat' => time(),
        'exp' => time() + 3600,
        'aud' => 'https://appleid.apple.com',
        'sub' => $sub
    ];

    $privKey = openssl_pkey_get_private($key);
    if (!$privKey) return false;

    $payload = $this->encode(json_encode($header)).'.'.$this->encode(json_encode($body));
    $signature = '';
    $success = openssl_sign($payload, $signature, $privKey, OPENSSL_ALGO_SHA256);
    if (!$success) return false;

    return $payload.'.'.$this->encode($signature);
}

function encode($data) {
    $encoded = strtr(base64_encode($data), '+/', '-_');
    return rtrim($encoded, '=');
}

?>

Проблема в том, что ответ от Apple всегда:

{"error":"invalid_client"}

Чтение здесь кажется, что проблема может быть связана с openSSL, который генерирует подпись, которая не является корректной для Apple (" Результат подписи OpenSSL ES256 является структурой ASN.1 в кодировке DER (его размер превышает 64). (не необработанное значение R || S)").

Есть ли способ получить правильную подпись с использованием openSSL?

Является ли формат p8 правильным вводом для функций openssl_sign и openssl_pkey_get_private? (Я заметил, что предоставленный ключ .p8 не работает, если используется в jwt.io для того, чтобы вычислите подписанный jwt.)

В документации openSSL я прочитал, что должен быть предоставлен ключ pem, как я могу преобразовать .p8 в ключ .pem?

Я также попробовал с некоторыми PHP библиотеками, которые в основном используют те же самые шаги, описанные выше, как firebase / php -jwt и lcobucci / jwt , но ответ Apple по-прежнему "недопустимый клиент".

Заранее благодарен за помощь,

РЕДАКТИРОВАТЬ

Я попытался полностью удалить openSSL из уравнения. Используя ключ .pem, сгенерированный из ключа .p8, я создал подписанный JWT с jwt.io. С этим подписанным JWT Apple API отвечает правильно. На данный момент я почти уверен, что это проблема подписи openSSL. Ключевая проблема заключается в том, как получить правильную подпись ES256, используя PHP и openSSL.

1 Ответ

5 голосов
/ 21 января 2020

Как указано здесь , проблема фактически в сигнатуре, сгенерированной openSSL.

Используя ES256, цифровая подпись представляет собой объединение двух целых чисел без знака, обозначаемых как R и S, которые являются результатом алгоритма Ellipti c Curve (E C). Длина R || S равно 64.

Функция openssl_sign генерирует сигнатуру, представляющую собой структуру ASN.1 в кодировке DER (с размером> 64).

Решение состоит в том, чтобы преобразовать сигнатуру в кодировке DER в необработанная конкатенация значений R и S. В этой библиотеке присутствует функция " fromDER ", которая выполняет такое преобразование:

    /**
     * @param string $der
     * @param int    $partLength
     *
     * @return string
     */
    public static function fromDER(string $der, int $partLength)
    {
        $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);
    }
    /**
     * @param string $data
     *
     * @return string
     */
    private static function preparePositiveInteger(string $data)
    {
        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;
    }
    /**
     * @param string $data
     *
     * @return string
     */
    private static function retrievePositiveInteger(string $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;
    }

Другой момент заключается в том, что ключ .pem должен быть предоставлен функция open_ssl_sign. Начиная с ключа .p8, загруженного от разработчика Apple, я сгенерировал файл .pem, используя openSSL:

openssl pkcs8 -in AuthKey_KEY_ID.p8 -nocrypt -out AuthKey_KEY_ID.pem

В следующем моем новом коде функции generateJWT , использующем .pem ключ и функция fromDER для преобразования подписи, сгенерированной openSSL:

    function generateJWT($kid, $iss, $sub) {

        $header = [
            'alg' => 'ES256',
            'kid' => $kid
        ];
        $body = [
            'iss' => $iss,
            'iat' => time(),
            'exp' => time() + 3600,
            'aud' => 'https://appleid.apple.com',
            'sub' => $sub
        ];

        $privKey = openssl_pkey_get_private(file_get_contents('AuthKey_.pem'));
        if (!$privKey){
           return false;
        }

        $payload = $this->encode(json_encode($header)).'.'.$this->encode(json_encode($body));

        $signature = '';
        $success = openssl_sign($payload, $signature, $privKey, OPENSSL_ALGO_SHA256);
        if (!$success) return false;

        $raw_signature = $this->fromDER($signature, 64);

        return $payload.'.'.$this->encode($raw_signature);
    }

Надеюсь, это поможет

...