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