cancel
Showing results for 
Search instead for 
Did you mean: 

Webhook verification - Checkout SDK - NodeJS

Options
lifespoiler
Contributor
Posted on

Hi to All,

the new nodejs sdk (@paypal/checkout-server-sdk) doesn't have the method to verify incoming webhooks, so I tried to write it by myself. The docs stay unclear in some cases so please help me understand it better and finally get through the verification process.
KB: https://developer.paypal.com/docs/api-basics/notifications/webhooks/notification-messages/


CRC32:

 

let crcHash = Crc32.createHash( "crc32" ); // crc32crypto package
crcHash.update(Buffer.from(JSON.stringify(req.body)));
let crc32 = crcHash.digest('hex');

 

What format should I use: hex (upper/lower case?), decimal, base64?

INPUT STRING:

 

// <transmissionId>|<timeStamp>|<webhookId>|<crc32>
let inputString = [
	req.headers['paypal-transmission-id'],
	req.headers['paypal-transmission-time'],
	config.webhook.webhookId,
	crc32.toLowerCase(),			
].join('|');

 

 

CERTIFICATE & PUBLIC KEY:

 

 

let rawCert = (await axios.get( req.headers['paypal-cert-url'] )).data;
let publicKey = crypto.createPublicKey(rawCert).export({type:'pkcs1', format:'pem'})

 

Is this the way it should be done? Tried 'pkcs1' as well as 'spki' type.

 

Basing on certificate url:

 

https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-7a8abba8

 

I got:

 

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAmYy88GplOqnwcS0pOnJ1aTf7KsjxEDuZ1tQ0E/JT8Odma2Oh9DS2
4yra1LTFdxooB7tP6l/QjrwbZHuonJB9SBRKCI6tH9led/u5Ay7rP8DbY4J6k6hq
HTrp+Su55RpZKaYT4sbDyzGwVQQGw3IKmGUeCIu1Rm3rAZ+kn+93zH6JdcIFrsuP
Z1Sr6LorOeK+Q6vcs4i4Q1K2guVggM9+NcsG1rJ2c+b1h+J5ydc+M2TDvdqXZNTA
RQMc5yXMXwI8PclIQ/rTzed8HTKRcdjE1vqDMSe5i7hRA5OdJCBd2FJy9ZC3YoqC
LKv9Le/631VDrIM+twMmzWpJHyOJYJoRUwIDAQAB
-----END RSA PUBLIC KEY-----

 


VERIFICATION:

 

 

const verify = crypto.createVerify('RSA-SHA256');
verify.update( inputString );
let verified = verify.verify(publicKey, req.headers['paypal-transmission-sig'], 'base64');

 

 

Despite many tries, changing crc32 case, public key type, encodings, etc... I wasn't able to verify the webhook.

Please help!
Thanks

1 REPLY 1

Webhook verification - Checkout SDK - NodeJS

Options
lifespoiler
Contributor
answering my own question... I thought someone might find it useful. My way of thinking was a good one, but it lacked knowledge of how some things should be settled: crc, cert... All credit should go to PaulS (https://stackoverflow.com/a/70844011)
Thanks for shedding light on dark places of paypal's documentation.
 

 

const CRC32 = require('CRC32');
const crypto = require('crypto');

async isValidNotification(req, res){
	try {
		const url = new URL(req.headers['paypal-cert-url']);
		if (!url.hostname.endsWith('.paypal.com')) throw new Error('Cert compromised!')

		const signature = req.headers['paypal-transmission-sig'];
		const bodyCrc32 = CRC32.str(JSON.stringify(req.body));  
		const unsigned_crc = bodyCrc32 >>> 0;    
		let inputString = [
			req.headers['paypal-transmission-id'],
			req.headers['paypal-transmission-time'],
			config.webhook.webhookId,
			unsigned_crc
		].join('|');			

		const publicKey = await this.getCertPublicKeyPEM(url.href));
		const verifier 	= crypto.createVerify('RSA-SHA256');
		verifier.update(inputString); verifier.end();
		return verifier.verify(publicKey, signature, 'base64');
	} catch(e) { 
		return false 
	}
}


const forge = require('node-forge');
const axios = require('axios');

// don't fetch it everytime, store it
let _certificates={}

async getCertPublicKeyPEM(certUrl) {
	if (_certificates[certUrl]) return _certificates[certUrl];
	
	let certRaw 		= (await axios.get( certUrl )).data ;
	let cert 			= forge.pki.certificateFromPem(certRaw);
	let publicKeyPem 	= forge.pki.publicKeyToPem(cert.publicKey)

	_certificates[certUrl] = publicKeyPem;
	return _certificates[certUrl];
}	

 

Haven't Found your Answer?

It happens. Hit the "Login to Ask the community" button to create a question for the PayPal community.