I'm attempting to verify a webhook signature from Patreon using Node.js. Here is my code:
const crypto = require("crypto");
...
function validateJsonWebhook(request) {
const secret = SECRET_KEY_GIVEN_BY_PATREON;
const hash = crypto.createHmac("md5", secret)
.update(JSON.stringify(request.body))
.digest("hex");
if (request.header("x-patreon-signature") === hash) {
return true;
} else {
return false;
}
}
Patreon webhooks use MD5 - see https://docs.patreon.com/#webhooks.
I've verified the secret key multiple times so I know that's not the issue.
Both "request.header("x-patreon-signature")" and "hash" are returning the correct format (i.e. they're both a 32 digit letter-number combination) but they're just not matching.
Any idea about what's going on?
So #gaiazov's comment led me to do some Googling which led me to the first two comments on Stripe verify web-hook signature HMAC sha254 HAPI.js by Karl Reid which led me to https://github.com/stripe/stripe-node/issues/331#issuecomment-314917167.
For anyone who finds this in the future: DON'T use JSON.stringify(request.body) - use request.rawBody instead, as the signature is calculated based on the raw JSON. I feel like this should be emphasized in Patreon's documentation, as all the examples I found used the code I originally posted. My new, working code is as follows (I cleaned up the "if (request.header("x-patreon-signature") === hash)" part at the end):
const crypto = require("crypto");
...
function validateJsonWebhook(request) {
// Secret key given by Patreon.
const secret = patreonSecret;
const hash = crypto.createHmac("md5", secret)
.update(request.rawBody)
.digest("hex");
return (request.header("x-patreon-signature") === hash);
}
Related
I have difficulty manipulating the Jose Node.JS documentation to chain the creation of a JWS and JWE. I cannot find the proper constructor for encryption. It looks like I can only encrypt a basic payload not a signed JWS.
Here is the code sample I try to fix to get something that would look like
const jws = await createJWS("myUserId");
const jwe = await encryptAsJWE(jws);
with the following methods
export const createJWS = async (userId) => {
const payload = {
}
payload['urn:userId'] = userId
// importing key from base64 encrypted secret key for signing...
const secretPkcs8Base64 = process.env.SMART_PRIVATE_KEY
const key = new NodeRSA()
key.importKey(Buffer.from(secretPkcs8Base64, 'base64'), 'pkcs8-private-der')
const privateKey = key.exportKey('pkcs8')
const ecPrivateKey = await jose.importPKCS8(privateKey, 'ES256')
const assertion = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'RS256' })
.setIssuer('demolive')
.setExpirationTime('5m')
.sign(ecPrivateKey)
return assertion
}
export const encryptAsJWE = async (jws) => {
// importing key similar to createJWS key import
const idzPublicKey = process.env.IDZ_PUBLIC_KEY //my public key for encryption
...
const pkcs8PublicKey = await jose.importSPKI(..., 'ES256')
// how to pass a signed JWS as parameter?
const jwe = await new jose.CompactEncrypt(jws)
.encrypt(pkcs8PublicKey)
return jwe
}
The input to the CompactEncrypt constructor needs to be a Uint8Array, so just wrapping the jws like so (new TextEncoder().encode(jws)) will allow you to move forward.
Moving forward then:
You are also missing the JWE protected header, given you likely use an EC key (based on the rest of your code) you should a) choose an appropriate EC-based JWE Key Management Algorithm (e.g. ECDH-ES) and put that as the public key import algorithm, then proceed to call .setProtectedHeader({ alg: 'ECDH-ES', enc: 'A128CBC-HS256' }) on the constructed object before calling encrypt.
Here's a full working example https://github.com/panva/jose/issues/112#issue-746919790 using a different combination of algorithms but it out to help you get the gist of it.
Here is the scenrio, Id like to utilize https://npm.io/package/otplib to generate a TOTP code and verify it with the user input. The issue is that I am unable to generate a code using multiple authy apps that matches the one the totp.generate() generates. I think the issue might be either due to me passing an invalid secretKey format/type into totp.generate(). Or the issue might me due to the configuration of the totp component(maybe using the wrong encryption type(i.e sha2)) when compared to the authy app.
Here is my code sample following the guide from: https://npm.io/package/otplib
const generateSecretKey = (size=16) => {
const val = crypto.randomBytes(size).toString('hex').slice(0, size).toUpperCase()
return val;
}
const generateTotp = (secret) => {
const token = totp.generate(secret)
return token;
}
const authChallenge = (token, secret) =>{
const isValid = totp.check(token, secret);
return isValid
}
let secret = generateSecretKey()
console.log("secret => " + secret)
let token = generateTotp(secret)
console.log(`generateTotp => token ${token}`)
let authChallengeResponse = authChallenge(token, secret)
The returned value is
It seems the package is able to generate the code, the issue is it is not the same code as the ones in the authy app. Could this be due to me providing an invalid key type?
I've seen example of signing https://www.pdftron.com/documentation/nodejs/guides/features/signature/sign-pdf
signOnNextSave uses PKCS #12 certificate, but I use Google KMS for asymmetric signing to keep private keys safe.
Here is example of signing and verifying by Google Cloud KMS
I tried to implement custom SignatureHandler but Node.JS API is different from Java or .NET
https://www.pdftron.com/api/pdfnet-node/PDFNet.SignatureHandler.html
How can I implement custom signing and verifying logic?
const data = Buffer.from('pdf data')
// We have 2048 Bit RSA - PSS Padding - SHA256 Digest key in Google Cloud KMS
const signAsymmetric = async () => {
const hash = crypto.createHash('sha256')
hash.update(data)
const digest = hash.digest()
const digestCrc32c = crc32c.calculate(digest)
// Sign the data with Cloud KMS
const [signResponse] = await client.asymmetricSign({
name: locationName,
digest: {
sha256: digest
},
digestCrc32c: {
value: digestCrc32c
}
})
if (signResponse.name !== locationName) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (!signResponse.verifiedDigestCrc32c) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (
crc32c.calculate(signResponse.signature) !==
Number(signResponse.signatureCrc32c.value)
) {
throw new Error('AsymmetricSign: response corrupted in-transit')
}
// Returns signature which is buffer
const encoded = signResponse.signature.toString('base64')
console.log(`Signature: ${encoded}`)
return signResponse.signature
}
// Verify data with public key
const verifyAsymmetricSignatureRsa = async () => {
const signatureBuffer = await signAsymmetric()
const publicKeyPem = await getPublicKey()
const verify = crypto.createVerify('sha256')
verify.update(data)
verify.end()
const key = {
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
}
// Verify the signature using the public key
const verified = verify.verify(key, signatureBuffer)
return verified
}
At this time, the PDFTron SDK only supports custom handlers on C++, Java, and C# (there are more plans to include additional languages in the future).
On a different platform like C++, you would extend the custom handler functions by putting hash.update(data) into SignatureHandler::AppendData, and the rest of signAsymmetric would go into SignatureHandler::CreateSignature. A name would be given to the custom handler for interoperability like Adobe.PPKLite (we do not yet support custom handler SubFilter entries, only Filter -- see PDF standard for the difference -- but this won't matter so long as you use a verification tool that supports Filter Adobe.PPKLite). Please see the following link for a concrete example:
https://www.pdftron.com/documentation/samples/cpp/DigitalSignaturesTest
As for verification, our code can already do this for you if your signatures fulfill the following conditions:
they use a standard digest algorithm
they use RSA to sign
they use the correct data formats according to the PDF standard (i.e. detached CMS, digital signature dictionary)
If you have more questions or require more details, please feel free to reach out to PDFTron support at support#pdftron.com
I am using Bold Commerce Webhooks to subscribe to subscription events in my store. They give documentation about how their request signatures are generated in PHP:
$now = time(); // current unix timestamp
$json = json_encode($payload, JSON_FORCE_OBJECT);
$signature = hash_hmac('sha256', $now.'.'.$json, $signingKey);
I'm trying to recreate the hash on my side in node.js. From my research I've figured out the following so far, which I believe is pretty close, but doesn't match yet:
const hash = request.header("X-Bold-Signature")!;
const SECRET = "my-secret-api-key";
const body = request.body;
const time = request.header("timestamp")!;
const mySignature = crypto.createHmac('sha256', SECRET).update(time + '.' + body).digest("hex");
if (mySignature !== request.header("X-Bold-Signature")!) {
//...
}
I've also tried using JSON.stringify(body) which changes the hash but still doesn't match.
It matched with this code.
const hash = crypto
.createHmac('sha256', secretKey)
.update(timestamp + '.' + JSON.stringify(body))
.digest('hex');
Note that unlike shopify, do not use rawbody.
I am trying to set up a webhook in Xero. I have created an endpoint which Xero hits and send some header and payload. I extract the hash from the header and match with the hash of payload but i never get the same hash. I am using the below code to do that.
router.post('/weebhook', function(req, res, next) {
console.log(req.headers)
console.dir(req.body);
try {
var xero_signature = req.headers['x-xero-signature']
var encoded_data = encodePayload(req.body)
console.log(xero_signature)
console.log(encoded_data)
if (encoded_data == xero_signature) {
res.status(200).json();
} else {
res.status(401).json();
}
}catch(eror) {
console.log(eror)
}
});
function encodePayload(payload) {
console.log(JSON.stringify(payload))
const secret = 'TbJjeMSPAvJiMiD2WdHbjP20iodKCA3bL5is8vo47/pCcuGCsjtUDb7cBnWo20e0TBwZsQ/lPM41QgypzZE6lQ==';
const hash = crypto.createHmac('sha256',secret,true)
.update(JSON.stringify(payload))
.digest().toString('base64');
return hash
}
Xero hash - NzQOq6yw6W6TKs1sQ1AJtMWX24uzzkyvh92fMxukreE=
my hash - L74zFdcuRsK3zHmzu9K37Y1mAVIAIsDgneAPHaJ+vI4=
Please let me know what is the issue ?
There's a typescript sample application provided by Xero that implements the webhooks signature verification.
Does the code in here help you at all? https://github.com/XeroAPI/XeroWebhooksReceiver-Node/blob/master/src/server/server.ts#L58L59
Also, please delete and recreate your webhook as you've just provided everyone with your secret webhooks key.
Change .update(JSON.stringify(payload)) to .update(payload.toString())