Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apple Sign In "invalid_client", signing JWT for authentication using PHP and openSSL

I'm trying to implement Apple sign in into an Android App using this library. The main flow is described in the documentation: the library returns an authorization code on the Android side. This authorization code has to be sent to my backend which, in turn, sends it to the Apple servers in order to get back an access token.

As described here and here, in order to obtain the access token we need to send to the Apple API a list of parameters, the authorization code and a signed JWT. In particular, JWT needs to be signed with a ES256 algorithm using a private .p8 key which has to be generated and downloaded from the Apple developer portal. Apple doc

Here is my PHP script:

<?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, '=');
}

?>

The problem is that the response from Apple is always:

{"error":"invalid_client"}

Reading here it seems that the problem could be related to openSSL which generates a signature which is not correct for Apple ("OpenSSL's ES256 signature result is a DER-encoded ASN.1 structure (it's size exceed 64). (not a raw R || S value)").

Is there a way to obtain the correct signature using openSSL?

Is the p8 format the correct input for the openssl_sign and openssl_pkey_get_private functions? (I noticed that the provided .p8 key does not work if used in jwt.io in order to compute the signed jwt.)

In the openSSL documentation I read that a pem key should be provided, how can I convert a .p8 in a .pem key?

I also tried with some PHP libraries which basically use the same steps described above like firebase/php-jwt and lcobucci/jwt but the Apple response is still "invalid client".

Thank you in advance for your help,

EDIT

I tried to completely remove openSSL from the equation. Using the .pem key generated from the .p8 one I have generated a signed JWT with jwt.io. With this signed JWT the Apple API replies correctly. At this point I'm almost sure it's an openSSL signature problem. The key problem is how to obtain a proper ES256 signature using PHP and openSSL.

like image 735
Andrea Gorrieri Avatar asked Dec 22 '22 19:12

Andrea Gorrieri


1 Answers

As indicated here, the problem is actually in the signature generated by openSSL.

Using ES256, the digital signature is the concatenation of two unsigned integers, denoted as R and S, which are the result of the Elliptic Curve (EC) algorithm. The length of R || S is 64.

The openssl_sign function generates a signature which is a DER-encoded ASN.1 structure (with size > 64).

The solution is to convert the DER-encoded signature into a raw concatenation of the R and S values. In this library a function "fromDER" is present which perform such a conversion:

    /**
     * @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;
    }

Another point is that a .pem key should be provided to the open_ssl_sign function. Starting from the .p8 key downloaded from the Apple developer I have created the .pem one by using openSSL:

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

In the following my new generateJWT function code which uses the .pem key and the fromDER function to convert the signature generated by 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);
    }

Hope it helps

like image 178
Andrea Gorrieri Avatar answered Dec 25 '22 08:12

Andrea Gorrieri