blockcypher / explorer

Block explorer showcasing the BlockCypher APIs.
https://live.blockcypher.com
Apache License 2.0
1.09k stars 726 forks source link

How do I verify the signature from a BlockCypher Webhook? #532

Open Olsm opened 6 months ago

Olsm commented 6 months ago

Hi there.

I have created webhooks with the attribute "signkey" added with the value "preset" so that BlockCypher will create a signature for the requests. This is described here: https://www.blockcypher.com/dev/bitcoin/#webhook-signing

I am trying to verify the signature when receiving webhook requests and I cannot find any documentation that shows how it is done. This is the current function I have created in Node.js and it is returning false:


function verifySignature(req) {
    // Get the signature value from the headers
    if (!('signature' in req.headers)) {
        throw new Error('Signature is missing in the headers');
    }
    const signatureHeader = req.headers['signature'];

    // Extract signature value from the signature header
    const signatureMatch = /signature="(.*?)"/.exec(signatureHeader);
    if (!signatureMatch || signatureMatch.length < 2) {
        throw new Error('Invalid signature header format');
    }
    const signature = signatureMatch[1];

    // Parse the provided public key into a format compatible with crypto module
    const publicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4
rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==
-----END PUBLIC KEY-----`;

    // Extract the required headers from the signature header
    const headersMatch = /headers="(.*?)"/.exec(signatureHeader);
    if (!headersMatch || headersMatch.length < 2) {
        throw new Error('Invalid signature header format: missing headers');
    }
    const requiredHeaders = headersMatch[1].split(' ');

    // Construct the content to be verified including the required headers
    const content = requiredHeaders.map(header => {
        if (header === '(request-target)') {
            // Extract the request-target from the original request
            return `${header}: ${req.method.toLowerCase()} ${req.url}`;
        } else {
            // Use the corresponding header value from the original request
            return `${header.toLowerCase()}: ${req.headers[header]}`;
        }
    }).join('\n');

    // Verify signature using the parsed public key and the constructed content
    const verifier = crypto.createVerify('sha256');
    verifier.update(content);

    return verifier.verify(publicKey, signature, 'base64');
}
Olsm commented 6 months ago

I found the solution here: https://crypto.stackexchange.com/a/109446

Please improve your documentation and add a description and example code for how to verify the signature. The format "ieee-p1363" must be used when verifying the signature and this should be mentioned in the documentation.

Here is the updated code and it works for me now:


function verifySignature(req) {
    // Get the signature value from the headers
    if (!('signature' in req.headers)) {
        throw new Error('Signature is missing in the headers');
    }
    const signatureHeader = req.headers['signature'];

    // Extract signature value from the signature header
    const signatureMatch = /signature="(.*?)"/.exec(signatureHeader);
    if (!signatureMatch || signatureMatch.length < 2) {
        throw new Error('Invalid signature header format');
    }
    const signature = signatureMatch[1];

    // Parse the provided public key into a format compatible with crypto module
    const publicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4
rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==
-----END PUBLIC KEY-----`;

    // Extract the required headers from the signature header
    const headersMatch = /headers="(.*?)"/.exec(signatureHeader);
    if (!headersMatch || headersMatch.length < 2) {
        throw new Error('Invalid signature header format: missing headers');
    }
    const requiredHeaders = headersMatch[1].split(' ');

    // Construct the content to be verified including the required headers
    const content = requiredHeaders.map(header => {
        if (header === '(request-target)') {
            // Extract the request-target from the original request
            return `${header}: ${req.method.toLowerCase()} ${req.url}`;
        } else {
            // Use the corresponding header value from the original request
            return `${header}: ${req.headers[header]}`;
        }
    }).join('\n');

    // Verify signature using the parsed public key and the constructed content
    const verifier = crypto.createVerify('sha256');
    verifier.update(content);

    return verifier.verify(
        {key: publicKey, dsaEncoding: "ieee-p1363" },
        Buffer.from(signature, 'base64')
    )
}
gissellestarch commented 6 months ago

Hi there.

I have created webhooks with the attribute "signkey" added with the value "preset" so that BlockCypher will create a signature for the requests. This is described here: https://www.blockcypher.com/dev/bitcoin/#webhook-signing

I am trying to verify the signature when receiving webhook requests and I cannot find any documentation that shows how it is done. This is the current function I have created in Node.js and it is returning false:

function verifySignature(req) {
    // Get the signature value from the headers
    if (!('signature' in req.headers)) {
        throw new Error('Signature is missing in the headers');
    }
    const signatureHeader = req.headers['signature'];

    // Extract signature value from the signature header
    const signatureMatch = /signature="(.*?)"/.exec(signatureHeader);
    if (!signatureMatch || signatureMatch.length < 2) {
        throw new Error('Invalid signature header format');
    }
    const signature = signatureMatch[1];

    // Parse the provided public key into a format compatible with crypto module
    const publicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4
rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==
-----END PUBLIC KEY-----`;

    // Extract the required headers from the signature header
    const headersMatch = /headers="(.*?)"/.exec(signatureHeader);
    if (!headersMatch || headersMatch.length < 2) {
        throw new Error('Invalid signature header format: missing headers');
    }
    const requiredHeaders = headersMatch[1].split(' ');

    // Construct the content to be verified including the required headers
    const content = requiredHeaders.map(header => {
        if (header === '(request-target)') {
            // Extract the request-target from the original request
            return `${header}: ${req.method.toLowerCase()} ${req.url}`;
        } else {
            // Use the corresponding header value from the original request
            return `${header.toLowerCase()}: ${req.headers[header]}`;
        }
    }).join('\n');

    // Verify signature using the parsed public key and the constructed content
    const verifier = crypto.createVerify('sha256');
    verifier.update(content);

    return verifier.verify(publicKey, signature, 'base64');
}