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>' \
--data ''

Validate the signature received

Every outbound request will contain a hashed authentication signature in the header computed by concatenating your auth token and request body and then generating a hash using the HMAC SHA256 hash algorithm.
To verify this signature came from AvaCloud, you must generate the HMAC SHA256 hash and compare it with the signature received. This procedure is commonly known as verifying the digital signature.

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();
}