phpnode.jsrsaverificationphpseclib

Verifying Signature in PHP


I need to verify a signature, I do have a sample code which I implemented in nodeJS and works like a charm.

const crypto = require('crypto');

app.post('/api/verify', (req, res) => {  
    const signature = req.headers['signature']
    let rawBody = req.body;
    rawBody = JSON.stringify(rawBody);
    const result = verifySignature(signature, rawBody)
    res.send(`Signature verification status: ${result}`); // Return True
});

const verifySignature = (signature, rawBody) => {
    const publicKey = fs.readFileSync('public.pem', 'utf8');
    const verifier = crypto.createVerify('RSA-SHA256')
    verifier.update(rawBody)
    return verifier.verify({ key: publicKey, padding: constants.RSA_PKCS1_PSS_PADDING }, signature, 'base64')
}

Now the issue is that I have to verify it in PHP (LARAVEL), for that I am using phpseclib.

use phpseclib\Crypt\RSA;

$signature = $request->header('signature');
$bodyContent = $request->getContent();
$result = $this->verify_signature($signature , $bodyContent); // Always False

protected function verify_signature($signature, $requestBody): bool
    {
        $rsa = new RSA();
        $rsa->setSignatureMode(RSA::SIGNATURE_PSS);
        $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1);
        $rsa->setHash("sha256");
        $public_key = <<<EOD
-----BEGIN PUBLIC KEY-----
XXXXXXXXXXX
-----END PUBLIC KEY-----
EOD;

        $rsa->loadKey($public_key);
        $result = $rsa->verify($requestBody , base64_decode($signature));

        if ($result) {
            return true;
        }
        return false;
    }

Could anyone please let me know what am I missing here!?


Solution

  • Both sides use different PSS parameters. However, for successful verification, the same PSS parameters must be applied. The default parameters used on the NodeJS side can be found in the NodeJS documentation:

    In the PHP code, SHA1 is used by default for both digests, i.e. the MGF1 hash must be explicitly specified. The default salt length is 0, i.e. must (probably) also be explicitly set. For the default values see the phpseclib, V2 documentation.

    Example: The following NodeJS code is executed successfully, i.e. the signature is successfully verified. The signature was also generated using NodeJS and the default value (i.e. the maximum salt length):

    const crypto = require('crypto');
    
    function verifySignature(signature, rawBody) {
        const publicKey = publicKeyPem = `-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunF5aDa6HCfLMMI/MZLT
    5hDk304CU+ypFMFiBjowQdUMQKYHZ+fklB7GpLxCatxYJ/hZ7rjfHH3Klq20/Y1E
    bYDRopyTSfkrTzPzwsX4Ur/l25CtdQldhHCTMgwf/Ev/buBNobfzdZE+Dhdv5lQw
    KtjI43lDKvAi5kEet2TFwfJcJrBiRJeEcLfVgWTXGRQn7gngWKykUu5rS83eAU1x
    H9FLojQfyia89/EykiOO7/3UWwd+MATZ9HLjSx2/Lf3g2jr81eifEmYDlri/OZp4
    OhZu+0Bo1LXloCTe+vmIQ2YCX7EatUOuyQMt2Vwx4uV+d/A3DP6PtMGBKpF8St4i
    GwIDAQAB
    -----END PUBLIC KEY-----`;
        const verifier = crypto.createVerify('RSA-SHA256');
        verifier.update(rawBody);
        return verifier.verify({ key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, signature, 'base64');
    }
    
    const signature = "LrQcFTFvgRxC/1aSnKn+lgHDmMNsv+F1E7/u6Y2KtbbzlUuO1ehls6F/sS+G9Q+oPuVVW/6xeMr++ygOJU5LlwwsliVxxs05OBZIvRrDwoNHqipakNN8SkZWSLdAuj1j9pv1bApwQjshFjOFPA11NgZGjdDvaB5Yayt4m3jewwR4Zxvtpqcd0MV1Ja22v3zfHQqca0Oahx4g6aLAy9WCLmVuFWZRauFKx0s+k+tPUs1ssTz/equl405T6PErQ1YbfApSqg4v0RAvHu7WWVG4s3lONU+jMUyqC4OHdf0mEgweNx9Kzy6gLA8nrrQGjxdomJmQuGxjX6BTlrRQjRecGw==";
    const msg = "The quick brown fox jumps over the lazy dog";
    const verified = verifySignature(signature, msg);
    console.log(verified); // true
    

    The PHP counterpart with phpseclib (V2) is:

    use phpseclib\Crypt\RSA;
    
    $publicKey = '-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunF5aDa6HCfLMMI/MZLT
    5hDk304CU+ypFMFiBjowQdUMQKYHZ+fklB7GpLxCatxYJ/hZ7rjfHH3Klq20/Y1E
    bYDRopyTSfkrTzPzwsX4Ur/l25CtdQldhHCTMgwf/Ev/buBNobfzdZE+Dhdv5lQw
    KtjI43lDKvAi5kEet2TFwfJcJrBiRJeEcLfVgWTXGRQn7gngWKykUu5rS83eAU1x
    H9FLojQfyia89/EykiOO7/3UWwd+MATZ9HLjSx2/Lf3g2jr81eifEmYDlri/OZp4
    OhZu+0Bo1LXloCTe+vmIQ2YCX7EatUOuyQMt2Vwx4uV+d/A3DP6PtMGBKpF8St4i
    GwIDAQAB
    -----END PUBLIC KEY-----';
    
    $rsa = new RSA();
    $rsa->loadKey($publicKey);
    $rsa->setSignatureMode(RSA::SIGNATURE_PSS);
    $rsa->setHash("sha256");
    $rsa->setMGFHash("sha256");     // set MGF digest explicitly
    $rsa->setSaltLength(256-32-2);  // set salt length explicitly
    
    $msg = "The quick brown fox jumps over the lazy dog";
    $signature = "LrQcFTFvgRxC/1aSnKn+lgHDmMNsv+F1E7/u6Y2KtbbzlUuO1ehls6F/sS+G9Q+oPuVVW/6xeMr++ygOJU5LlwwsliVxxs05OBZIvRrDwoNHqipakNN8SkZWSLdAuj1j9pv1bApwQjshFjOFPA11NgZGjdDvaB5Yayt4m3jewwR4Zxvtpqcd0MV1Ja22v3zfHQqca0Oahx4g6aLAy9WCLmVuFWZRauFKx0s+k+tPUs1ssTz/equl405T6PErQ1YbfApSqg4v0RAvHu7WWVG4s3lONU+jMUyqC4OHdf0mEgweNx9Kzy6gLA8nrrQGjxdomJmQuGxjX6BTlrRQjRecGw==";
    $result = $rsa->verify($msg , base64_decode($signature));
    
    echo $result ? 'verified' : 'unverified'; // verified
    

    which also successfully verifies the signature.