Webhook Signature
Webhook Signature for the Webhook API
To make your webhooks extra secure, you can verify that they originated from our side by generating an HMAC SHA-256 hash code using your Authentication Token and request body. You can get the signing secret through the AvaCloud portal or Glacier API.
Find your signing secret
Using the portal
Navigate to the webhook section and click on Generate Signing Secret. Create the secret and copy it to your code.
Using Data API
The following endpoint retrieves a shared secret:
curl --location 'https://glacier-api.avax.network/v1/webhooks:getSharedSecret' \
--header 'x-glacier-api-key: <YOUR_API_KEY>' \Validate the signature received
Every outbound request will include an authentication signature in the header. This signature is generated by:
- Canonicalizing the JSON Payload: This means arranging the JSON data in a standard format.
- Generating a Hash: Using the HMAC SHA256 hash algorithm to create a hash of the canonicalized JSON payload.
To verify that the signature is from us, follow these steps:
- Generate the HMAC SHA256 hash of the received JSON payload.
- Compare this generated hash with the signature in the request header. This process, known as verifying the digital signature, ensures the authenticity and integrity of the request.
Example Request Header
Content-Type: application/json;
x-signature: your-hashed-signatureExample Signature Validation Function
This Node.js code sets up an HTTP server using the Express framework. It listens for POST requests sent to the /callback endpoint. Upon receiving a request, it validates the signature of the request against a predefined signingSecret. If the signature is valid, it logs match; otherwise, it logs no match. The server responds with a JSON object indicating that the request was received.
Node (JavaScript)
const express = require('express');
const crypto = require('crypto');
const { canonicalize } = require('json-canonicalize');
const app = express();
app.use(express.json({limit: '50mb'}));
const signingSecret = 'c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53';
function isValidSignature(signingSecret, signature, payload) {
const canonicalizedPayload = canonicalize(payload);
const hmac = crypto.createHmac('sha256', Buffer.from(signingSecret, 'hex'));
const digest = hmac.update(canonicalizedPayload).digest('base64');
console.log("signature: ", signature);
console.log("digest", digest);
return signature === digest;
}
app.post('/callback', express.json({ type: 'application/json' }), (request, response) => {
const { body, headers } = request;
const signature = headers['x-signature'];
// Handle the event
switch (body.evenType) {
case 'address\*activity':
console.log("\*\** Address*activity \*\*\*");
console.log(body);
if (isValidSignature(signingSecret, signature, body)) {
console.log("match");
} else {
console.log("no match");
}
break;
// ... handle other event types
default:
console.log(`Unhandled event type ${body}`);
}
// Return a response to acknowledge receipt of the event
response.json({ received: true });
});
const PORT = 8000;
app.listen(PORT, () => console.log(`Running on port ${PORT}`));Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
import json
app = Flask(__name__)
SIGNING_SECRET = 'c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53'
def canonicalize(payload):
"""Function to canonicalize JSON payload"""
# In Python, canonicalization can be achieved by using sort_keys=True in json.dumps
return json.dumps(payload, separators=(',', ':'), sort_keys=True)
def is_valid_signature(signing_secret, signature, payload):
canonicalized_payload = canonicalize(payload)
hmac_obj = hmac.new(bytes.fromhex(signing_secret), canonicalized_payload.encode('utf-8'), hashlib.sha256)
digest = base64.b64encode(hmac_obj.digest()).decode('utf-8')
print("signature:", signature)
print("digest:", digest)
return signature == digest
@app.route('/callback', methods=['POST'])
def callback_handler():
body = request.json
signature = request.headers.get('x-signature')
# Handle the event
if body.get('eventType') == 'address_activity':
print("*** Address_activity ***")
print(body)
if is_valid_signature(SIGNING_SECRET, signature, body):
print("match")
else:
print("no match")
else:
print(f"Unhandled event type {body}")
# Return a response to acknowledge receipt of the event
return jsonify({"received": True})
if __name__ == '__main__':
PORT = 8000
print(f"Running on port {PORT}")
app.run(port=PORT)Go (net/http)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
)
const signingSecret = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53"
// Canonicalize function sorts the JSON keys and produces a canonicalized string
func Canonicalize(payload map[string]interface{}) (string, error) {
var sb strings.Builder
var keys []string
for k := range payload {
keys = append(keys, k)
}
sort.Strings(keys)
sb.WriteString("{")
for i, k := range keys {
v, err := json.Marshal(payload[k])
if err != nil {
return "", err
}
sb.WriteString(fmt.Sprintf("\"%s\":%s", k, v))
if i < len(keys)-1 {
sb.WriteString(",")
}
}
sb.WriteString("}")
return sb.String(), nil
}
func isValidSignature(signingSecret, signature string, payload map[string]interface{}) bool {
canonicalizedPayload, err := Canonicalize(payload)
if err != nil {
fmt.Println("Error canonicalizing payload:", err)
return false
}
key, err := hex.DecodeString(signingSecret)
if err != nil {
fmt.Println("Error decoding signing secret:", err)
return false
}
h := hmac.New(sha256.New, key)
h.Write([]byte(canonicalizedPayload))
digest := h.Sum(nil)
encodedDigest := base64.StdEncoding.EncodeToString(digest)
fmt.Println("signature:", signature)
fmt.Println("digest:", encodedDigest)
return signature == encodedDigest
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
var body map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
fmt.Println("Error decoding body:", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
signature := r.Header.Get("x-signature")
eventType, ok := body["eventType"].(string)
if !ok {
fmt.Println("Error parsing eventType")
http.Error(w, "Invalid event type", http.StatusBadRequest)
return
}
switch eventType {
case "address_activity":
fmt.Println("*** Address_activity ***")
fmt.Println(body)
if isValidSignature(signingSecret, signature, body) {
fmt.Println("match")
} else {
fmt.Println("no match")
}
default:
fmt.Printf("Unhandled event type %s\n", eventType)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
http.HandleFunc("/callback", callbackHandler)
fmt.Println("Running on port 8000")
http.ListenAndServe(":8000", nil)
}Rust (actix-web)
use actix_web::{web, App, HttpServer, HttpResponse, Responder, post};
use serde::Deserialize;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use base64::encode;
use std::collections::BTreeMap;
type HmacSha256 = Hmac<Sha256>;
const SIGNING_SECRET: &str = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53";
#[derive(Deserialize)]
struct EventPayload {
eventType: String,
// Add other fields as necessary
}
// Canonicalize the JSON payload by sorting keys
fn canonicalize(payload: &BTreeMap<String, serde_json::Value>) -> String {
serde_json::to_string(payload).unwrap()
}
fn is_valid_signature(signing_secret: &str, signature: &str, payload: &BTreeMap<String, serde_json::Value>) -> bool {
let canonicalized_payload = canonicalize(payload);
let mut mac = HmacSha256::new_from_slice(signing_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(canonicalized_payload.as_bytes());
let result = mac.finalize();
let digest = encode(result.into_bytes());
println!("signature: {}", signature);
println!("digest: {}", digest);
digest == signature
}
#[post("/callback")]
async fn callback(body: web::Json<BTreeMap<String, serde_json::Value>>, req: web::HttpRequest) -> impl Responder {
let signature = req.headers().get("x-signature").unwrap().to_str().unwrap();
if let Some(event_type) = body.get("eventType").and_then(|v| v.as_str()) {
match event_type {
"address_activity" => {
println!("*** Address_activity ***");
println!("{:?}", body);
if is_valid_signature(SIGNING_SECRET, signature, &body) {
println!("match");
} else {
println!("no match");
}
}
_ => {
println!("Unhandled event type: {}", event_type);
}
}
} else {
println!("Error parsing eventType");
return HttpResponse::BadRequest().finish();
}
HttpResponse::Ok().json(serde_json::json!({ "received": true }))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(callback)
})
.bind("0.0.0.0:8000")?
.run()
.await
}TypeScript (ChainKit SDK)
import { isValidSignature } from '@avalanche-sdk/chainkit/utils';
import express from 'express';
const app = express();
app.use(express.json());
const signingSecret = 'your-signing-secret'; // Replace with your signing secret
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.body;
if (isValidSignature(signingSecret, signature, payload)) {
console.log('Valid signature');
// Process the request
} else {
console.log('Invalid signature');
}
res.json({ received: true });
});
app.listen(8000, () => console.log('Server running on port 8000'));Is this guide helpful?