package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
func verifyHmacSignature(payload map[string]interface{}, headerValue, apiToken, webhookUrl string, maxAgeSeconds int) (bool, error) {
// Parse the header value: signature,timestamp,nonce
parts := strings.Split(headerValue, ",")
if len(parts) != 3 {
return false, fmt.Errorf("invalid header format")
}
signaturePart, timestampStr, nonce := parts[0], parts[1], parts[2]
// Validate timestamp to prevent replay attacks
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return false, fmt.Errorf("invalid timestamp: %v", err)
}
currentTime := time.Now().Unix()
if abs(currentTime-timestamp) > int64(maxAgeSeconds) {
return false, fmt.Errorf("signature timestamp is too old")
}
// Parse the signature to extract algorithm and value
signatureParts := strings.SplitN(signaturePart, "=", 2)
if len(signatureParts) != 2 {
return false, fmt.Errorf("invalid signature format")
}
algorithm, signatureValue := signatureParts[0], signatureParts[1]
// Sort keys alphabetically
keys := make([]string, 0, len(payload))
for k := range payload {
keys = append(keys, k)
}
sort.Strings(keys)
// Create sorted map
sortedPayload := make(map[string]interface{})
for _, k := range keys {
sortedPayload[k] = payload[k]
}
// JSON encode the sorted payload
jsonPayload, err := json.Marshal(sortedPayload)
if err != nil {
return false, err
}
// Combine URL and payload as done in signature generation
signatureData := webhookUrl + "|" + string(jsonPayload)
// Get the hash function based on algorithm
var hashFunc func() hash.Hash
switch algorithm {
case "sha256":
hashFunc = sha256.New
default:
return false, fmt.Errorf("unsupported algorithm: %s", algorithm)
}
// Compute expected signature
mac := hmac.New(hashFunc, []byte(apiToken))
mac.Write([]byte(signatureData))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Compare signatures using timing-safe comparison
return hmac.Equal([]byte(expectedSignature), []byte(signatureValue)), nil
}
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
// Example usage
func main() {
payload := map[string]interface{}{
"domain": "example.com",
"action": "created",
}
headerValue := "sha256=a1b2c3d4e5f6...,1640995200,550e8400-e29b-41d4-a716-446655440000" // From X-NAMECOM-SIGNATURE header
apiToken := "your-api-token"
webhookUrl := "https://partner.example.com/webhook/domain-created" // The exact full webhook subscription URL
maxAgeSeconds := 300
valid, err := verifyHmacSignature(payload, headerValue, apiToken, webhookUrl, maxAgeSeconds)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if valid {
fmt.Println("Signature is valid")
} else {
fmt.Println("Signature is invalid")
}
}