Webhook Signature and Security

To make your webhooks extra secure, you can verify that they originated from AvaCloud 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 AvaCloud portal

Navigate to the webhook section and click on Generate Signing Secret. Create the secret and copy it to your code.

Using Glacier API

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:

  1. Canonicalizing the JSON Payload: This means arranging the JSON data in a standard format.
  2. 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 AvaCloud, follow these steps:

  1. Generate the HMAC SHA256 hash of the received JSON payload.
  2. 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-signature

Example 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.


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}`));
from flask import Flask, request, jsonify
import hashlib
import hmac
import json

app = Flask(__name__)

signing_secret = b'c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53'

def is_valid_signature(signing_secret, signature, payload):
    canonicalized_payload = json.dumps(payload, separators=(',', ':'), sort_keys=True)
    hmac_digest = hmac.new(signing_secret, canonicalized_payload.encode(), hashlib.sha256).digest()
    calculated_signature = hmac_digest.encode('base64').strip()
    return signature == calculated_signature

@app.route('/callback', methods=['POST'])
def callback():
    body = request.json
    signature = request.headers.get('x-signature')

    # Handle the event
    event_type = body.get('evenType')
    if event_type == '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 {event_type}")

    # Return a response to acknowledge receipt of the event
    return jsonify({'received': True})

if __name__ == '__main__':
    app.run(port=8000)

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
)

const (
	signingSecret = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53"
)

func isValidSignature(signingSecret, signature string, payload interface{}) bool {
	canonicalizedPayload, err := json.Marshal(payload)
	if err != nil {
		fmt.Println("Error marshaling payload:", err)
		return false
	}

	h := hmac.New(sha256.New, []byte(signingSecret))
	h.Write(canonicalizedPayload)
	digest := h.Sum(nil)

	return signature == base64.StdEncoding.EncodeToString(digest)
}

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)
		return
	}

	signature := r.Header.Get("x-signature")
	eventType, ok := body["eventType"].(string)
	if !ok {
		fmt.Println("Error parsing eventType")
		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)
}

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

use rocket::Data;
use rocket::http::Status;
use rocket::response::content::Json;
use serde_json::Value;
use sha2::{Digest, Sha256};
use base64::encode;
use json_canonicalize::json_canonicalize;

const SIGNING_SECRET: &str = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53";

fn is_valid_signature(signing_secret: &str, signature: &str, payload: &Value) -> bool {
    let canonicalized_payload = json_canonicalize(payload).expect("Failed to canonicalize payload");
    let mut hmac = Sha256::new();
    hmac.update(canonicalized_payload.as_bytes());
    let digest = encode(hmac.finalize().as_slice());
    println!("signature: {}", signature);
    println!("digest: {}", digest);
    signature == digest
}

#[post("/callback", format = "json", data = "<body>")]
fn callback(body: Data, headers: rocket::http::Headers) -> Json<Value> {
    let signature = headers.get_one("x-signature").unwrap_or("");
    let body_str = std::str::from_utf8(body.open().take(50 * 1024).collect::<Vec<_>>().concat().as_slice()).unwrap();
    let body_json: Value = serde_json::from_str(body_str).unwrap();
    let event_type = body_json.get("evenType").and_then(|v| v.as_str()).unwrap_or("");

    match event_type {
        "address_activity" => {
            println!("*** Address_activity ***");
            println!("{}", body_str);
            if is_valid_signature(SIGNING_SECRET, signature, &body_json) {
                println!("match");
            } else {
                println!("no match");
            }
        },
        _ => println!("Unhandled event type {}", event_type),
    }

    Json(json!({"received": true}))
}

fn main() {
    rocket::ignite().mount("/", routes![callback]).launch();
}