5 min read
Overview
Streaming blockchain data in real-time can often be daunting due to the complexity of infrastructure setup and the ongoing management required. QuickNode addresses this challenge with Streams, a blockchain data streaming solution that allows you to easily stream both historical and real-time data directly to your applications or services.
This guide will take you through the process of verifying HMAC signatures for incoming messages from Streams webhook services. Ensuring the integrity and authenticity of these messages is crucial for securing your applications against tampering and forgery. By the end of this guide, you'll be equipped with the knowledge to implement signature verification using Python effectively.
What You Will Do
- Retrieve necessary headers (
nonce
,signature
,timestamp
) from incoming HTTP requests - Prepare the raw JSON payload for signature verification
- Write and execute a Python script to verify the HMAC signature
- Discuss best security practices to enhance the safety of your verification process
What You Will Need
- A Stream created on QuickNode account
- A code editor (e.g., VSCode)
- Python installed on your system
- Basic familiarity with handling HTTP requests in your server environment
- Access to the secret key used for HMAC signatures, found in your webhook stream settings
If you haven't created any Streams yet and want to start streaming blockchain data effortlessly, check out our comprehensive guide. This guide will walk you through the simple steps to set up your Streams with QuickNode.
Verify HMAC Signatures
Retrieving Necessary Headers
Each message from our webhook service includes three critical headers that are explained below. Extracting these values from the headers of incoming HTTP requests is the first step in the verification process.
- X-QN-Nonce: A unique string that is used once per signature to prevent replay attacks. (e.g., 216820ba6d45d2271eb80a0afe957cc7)
- X-QN-Signature: The HMAC signature you need to verify. (e.g., 39eb54760585319d29b645da8d67590b28287536daa0a7da8d9472966a2d58d5)
- X-QN-Timestamp: The timestamp when the message was signed, which you can use to prevent replay attacks. (e.g., 1713367463)
Preparing the Payload
When verifying the HMAC signature, it's crucial to use the uncompressed payload data. Even if the webhook message is received in a compressed format (e.g., gzip), you must first decompress it before using it for signature verification. This is because the signature was generated using the uncompressed data on the server side.
Getting a Security Token
Navigate to the Streams section in the QuickNode Dashboard, and locate the security token in the Settings section. Make sure to copy this token, as you will need it for the steps that follow.
Verifying the HMAC Signature
We now have all the information to verify the HMAC signature. Now, create a Python file (i.e., verify-signature.py) and open it with your code editor. Then, add the following code to the file.
After these steps, replace your_nonce_here
with the nonce, your_given_signature_here
with the signature, your_timestamp_here
with the timestamp from your headers, and your_secret_key_here
with the actual secret key provided in your Stream settings, as detailed in the previous section. Ensure these values are precisely copied to maintain the integrity of the verification process.
In each of these examples below:
- We first decompress the received payload (assuming it's compressed with gzip).
- We then use the uncompressed payload for signature verification.
- The verifySignature function receives the uncompressed payload along with other pieces of data required to verify the signature.
NodeJS example
const crypto = require('crypto');
const zlib = require('zlib');
function verifySignature(secretKey, uncompressedPayload, nonce, timestamp, givenSignature) {
const message = nonce + timestamp + uncompressedPayload;
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'hex'),
Buffer.from(givenSignature, 'hex')
);
}
// Example usage
const secretKey = 'your_security_token_here';
const compressedPayload = /* Your received compressed payload */;
const nonce = 'your_nonce_here';
const timestamp = 'your_timestamp_here';
const givenSignature = 'your_given_signature_here';
zlib.gunzip(compressedPayload, (err, uncompressedPayload) => {
if (err) {
console.error('Error decompressing payload:', err);
return;
}
try {
const isValid = verifySignature(secretKey, uncompressedPayload.toString(), nonce, timestamp, givenSignature);
console.log('Signature is valid:', isValid);
} catch (error) {
console.error('Error verifying signature:', error);
}
});
Python example
import hmac
import hashlib
import gzip
def verify_signature(secret_key, uncompressed_payload, nonce, timestamp, given_signature):
message = nonce + timestamp + uncompressed_payload
computed_signature = hmac.new(
secret_key.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_signature, given_signature)
# Example usage
secret_key = 'your_security_token_here'
compressed_payload = b'your_compressed_payload_here' # Received as bytes
nonce = 'your_nonce_here'
timestamp = 'your_timestamp_here'
given_signature = 'your_given_signature_here'
try:
# Decompress the payload
uncompressed_payload = gzip.decompress(compressed_payload).decode('utf-8')
is_valid = verify_signature(secret_key, uncompressed_payload, nonce, timestamp, given_signature)
print('Signature is valid:', is_valid)
except Exception as e:
print('Error verifying signature:', str(e))
Go example
package main
import (
"bytes"
"compress/gzip"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
)
func verifySignature(secretKey, uncompressedPayload, nonce, timestamp, givenSignature string) (bool, error) {
message := nonce + timestamp + uncompressedPayload
h := hmac.New(sha256.New, []byte(secretKey))
_, err := h.Write([]byte(message))
if err != nil {
return false, err
}
computedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(computedSignature), []byte(givenSignature)), nil
}
func main() {
secretKey := "your_security_token_here"
compressedPayload := []byte("your_compressed_payload_here")
nonce := "your_nonce_here"
timestamp := "your_timestamp_here"
givenSignature := "your_given_signature_here"
// Decompress the payload
reader, err := gzip.NewReader(bytes.NewReader(compressedPayload))
if err != nil {
fmt.Println("Error creating gzip reader:", err)
return
}
defer reader.Close()
uncompressedPayload, err := ioutil.ReadAll(reader)
if err != nil {
fmt.Println("Error decompressing payload:", err)
return
}
isValid, err := verifySignature(secretKey, string(uncompressedPayload), nonce, timestamp, givenSignature)
if err != nil {
fmt.Println("Error verifying signature:", err)
return
}
fmt.Println("Signature is valid:", isValid)
}
Best Security Practices
While implementing the HMAC signature verification, ensure the following practices to secure your application:
- Always keep the security token confidential and securely stored, not hardcoded in publicly accessible areas of your code.
- Log all validation attempts, both successful and failed, to aid in auditing and troubleshooting.
- Implement additional checks like timestamp validation to prevent replay attacks.
Conclusion
Congratulations! You've successfully learned how to validate incoming Streams webhook messages by verifying HMAC signatures. This practice is vital for protecting your applications from external threats and ensuring data integrity.
If you have questions, please contact us directly. If you have any ideas or suggestions, such as new destinations, features, metrics, or datasets, you want us to support.
Also, stay up to date with the latest by following us on Twitter and joining our Discord and Telegram announcement channel.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.