Search code examples
javascriptphphtmlwebnotifications

Web Push Notifications InvalidSignature


I cannot seem to get push notifications to work whatever I try...

This is the error code. {"code":401,"errno":109,"error":"Unauthorized","message":"InvalidSignature","more_info":"http://autopush.readthedocs.io/en/latest/http.html#error-codes"}

It appears that the issue has something to do with ether a key mismatch or invalid signature.

Here are some of the resources I was using: https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/

https://autopush.readthedocs.io/en/latest/http.html#error-codes

https://datatracker.ietf.org/doc/rfc8292/

I'm generating the public/private keys as so:

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

    function generateVapidKeys(){
        if(file_exists('vapid.json')){
            $vapidKeys = json_decode(file_get_contents('vapid.json'));
            return base64url_encode(hex2bin('04'.$vapidKeys->x.$vapidKeys->y));

        }else{
            $keyPair = openssl_pkey_new([
                'digest_alg' => 'sha256',
                'private_key_type' => OPENSSL_KEYTYPE_EC,
                'curve_name' => 'prime256v1', // P-256 curve
            ]);
        }

        $privateKeyDetails = openssl_pkey_get_details($keyPair);

        $x = str_pad(bin2hex($privateKeyDetails['ec']['x']), 64, '0', STR_PAD_LEFT);
        $y = str_pad(bin2hex($privateKeyDetails['ec']['y']), 64, '0', STR_PAD_LEFT);
        $d = str_pad(bin2hex($privateKeyDetails['ec']['d']), 64, '0', STR_PAD_LEFT);

        file_put_contents('vapid.json', json_encode([
            'x' => $x,
            'y' => $y,
            'd' => $d,
        ], JSON_PRETTY_PRINT));

        return base64url_encode(hex2bin('04'.$x.$y));
    }

    $publicKey = generateVapidKeys();

And finally here is my Notification send:

<?php
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);

    header('Content-Type: application/json; charset=utf-8');

    function generate_jwt($headers, $payload, $privateKey){
        $headers_encoded = base64url_encode(json_encode($headers));
        $payload_encoded = base64url_encode(json_encode($payload));
        
        //$signature = hash_hmac('SHA256', "$headers_encoded.$payload_encoded", $secret, true);
        openssl_sign("$headers_encoded.$payload_encoded", $signature, $privateKey, OPENSSL_ALGO_SHA256);
        $signature_encoded = base64url_encode($signature);
        
        return "$headers_encoded.$payload_encoded.$signature_encoded";
    }

    function is_jwt_valid($jwt, $publicKey){
        $tokenParts = explode('.', $jwt);
    
        // check the expiration time - note this will cause an error if there is no 'exp' claim in the jwt
        $expires = json_decode(base64_decode($tokenParts[1]))->exp < time();//($expires - time()) < 0;

        $signature = openssl_verify($tokenParts[0].'.'.$tokenParts[1], base64_decode($tokenParts[2]), $publicKey, OPENSSL_ALGO_SHA256);
        
        if($expires || !$signature){
            return false;
        }
        return true;
    }


    function generateVapidToken($url, $privateKey) {
    
        $expiration = time() + (12 * 60 * 60);  // 12 hours
    
        $header = [
            'alg' => 'ES256',
            'typ' => 'JWT',
        ];

        $body = [
            'aud' => $url,
            'exp' => $expiration,
            'sub' => 'mailto:[email protected]',
        ];

        return generate_jwt($header, $body, $privateKey);
    }
    
    function base64url_encode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }


    // Assuming you have a database connection established

    // Function to send a push notification
    function sendPushNotification($subscription, $payload)
    {

        $parse = parse_url($subscription->endpoint);
        $url = $parse['scheme'].'://'.$parse['host'];//.pathinfo(parse_url($parse['path'], PHP_URL_PATH))['dirname'];
        echo $url.PHP_EOL.PHP_EOL;

        $vapidKeys = json_decode(file_get_contents('vapid.json'));

        //print_r(json_encode($vapidKeys, JSON_PRETTY_PRINT));


        $keyPair = openssl_pkey_new([
            'ec' => [
                'digest_alg' => 'sha256',
                'private_key_type' => OPENSSL_KEYTYPE_EC,
                'curve_name' => 'prime256v1', // P-256 curve
                'x' => hex2bin($vapidKeys->x),
                'y' => hex2bin($vapidKeys->y),
                'd' => hex2bin($vapidKeys->d)
            ]
        ]);

        $privateKeyDetails = openssl_pkey_get_details($keyPair);

        openssl_pkey_export($keyPair, $privateKey);
        $token = generateVapidToken($url, $privateKey);
        //openssl_sign('HELLO WORLD', $signature, $privateKey, OPENSSL_ALGO_SHA256);
        echo $token;

        echo PHP_EOL;
        echo PHP_EOL;

        $publicKey = openssl_pkey_get_public($privateKeyDetails['key']);
        $verified = is_jwt_valid($token, $publicKey);
        //$verified = openssl_verify('HELLO WORLD', $signature, $publicKey, OPENSSL_ALGO_SHA256);


        echo 'Token Valid: '.(($verified) ? "TRUE" : "FALSE");
        echo PHP_EOL;
        echo PHP_EOL;


        $publicKey = base64url_encode(hex2bin('04'.$vapidKeys->x.$vapidKeys->y));
        echo $publicKey;


        echo PHP_EOL;
        echo PHP_EOL;

        $headers = [
            //'Authorization: WebPush '.$token,
            'Authorization: vapid t='.$token.',k='.$publicKey,
            //'Authorization: key=' . $subscription->keys->auth,
            //'Crypto-Key: p256ecdsa='.$publicKey.';dh='.$subscription->keys->auth,//$subscription->keys->p256dh,
            'Content-Type: application/json',
        ];

        /*
        $notification = [
            'title' => 'Your Notification Title',
            'body' => 'Your Notification Body',
            'icon' => 'path/to/icon.png',
        ];
        */

        $data = [
            'notification' => $payload,
            //'applicationServerKey' => $vapidKeys->publicKey
        ];

        $options = [
            CURLOPT_URL => $subscription->endpoint,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_RETURNTRANSFER => true,
        ];

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $result = curl_exec($ch);
    
        if ($result === false) {
            echo 'Error: ' . curl_error($ch) . PHP_EOL;
        } else {
            echo 'Push notification sent successfully!' . PHP_EOL;
        }


        print_r($result);
    }

    // Example payload
    $notificationPayload = [
        'title' => 'New Notification',
        'body' => 'This is the body of the notification.',
        'icon' => 'icon.png'
    ];

    if(file_exists('endpoints.json')){
        $subscriptions = json_decode(file_get_contents('endpoints.json'));

        // Send push notifications to all stored subscriptions
        foreach ($subscriptions as $subscription) {
            sendPushNotification($subscription, $notificationPayload);
        }
    }

?>

Solution

  • The answer for this specific issue was the way I was generating the JWT.I needed to modify the signature differently.

        function generate($headers, $payload, $privateKey){
            $message = self::base64url_encode(json_encode($headers)).'.'.self::base64url_encode(json_encode($payload));
            openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256);
    
            $components = [];
            $pos = 0;
            $size = strlen($signature);
            while ($pos < $size) {
                $constructed = (ord($signature[$pos]) >> 5) & 0x01;
                $type = ord($signature[$pos++]) & 0x1f;
                $len = ord($signature[$pos++]);
                if ($len & 0x80) {
                    $n = $len & 0x1f;
                    $len = 0;
                    while ($n-- && $pos < $size) $len = ($len << 8) | ord($signature[$pos++]);
                }
        
                if ($type == 0x03) {
                    $pos++;
                    $components[] = substr($signature, $pos, $len - 1);
                    $pos += $len - 1;
                } else if (! $constructed) {
                    $components[] = substr($signature, $pos, $len);
                    $pos += $len;
                }
            }
            foreach ($components as &$c) $c = str_pad(ltrim($c, "\x00"), 32, "\x00", STR_PAD_LEFT);
        
            return $message . '.' . self::base64url_encode(implode('', $components));
            
            //return "$headers_encoded.$payload_encoded.$signature_encoded";
        }