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!?
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:
verify()
applies RSA_PSS_SALTLEN_AUTO
by default, which automatically determines the salt length from the PSS block structure. phpseclib does not support this as far as I know, so so you have to find out the salt length. If signing was also done with NodeJS and the default salt length was used, the maximum possible salt length is applied (sign()
uses RSA_PSS_SALTLEN_MAX_SIGN
), which is:signature length (bytes) - digest output length (bytes) - 2 = 256 - 32 - 2
.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.