KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

Go webhook signature verifier

Run the complete Go raw-byte notification verifier with authenticated event and environment checks.

On this page
Updated 2026-10-18 UTCDownload Markdown

Authenticate KDPika Licensing notification bytes with the complete Go verifier.

What you will learn

  • Install and execute this exact sample.
  • Verify raw bytes, event metadata and the pinned environment.
  • Identify the boundary between authentication and durable business processing.

Prerequisites

Prepare a request

  1. Work inside the extracted webhooks directory, which contains contract.json.
  2. Set KDP_WEBHOOK_SECRET through your private process environment.
  3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox.
  4. Use Python 3.11+ to create a fresh private synthetic request.
bashbash
python3 make-test.py events/test-ping.json private/request-1

Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or license authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap.

Build and run

On Windows build with -o verify-go.exe and run verify-go.exe with the same two request file paths.

bashbash
go build -o verify-go verify.go
./verify-go private/request-1/body.json private/request-1/headers.json

Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed.

Complete source

verify.gogo
// Notification authenticity, not license validity. Go 1.24+, standard library.
// Build: go build -o verify-go verify.go; run from this folder with the two KDP_WEBHOOK environment variables.
// Run: ./verify-go body.json headers.json; expected verified=true JSON or verified=false / exit 1.
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "os"
    "regexp"
    "strconv"
    "strings"
    "time"
    "unicode/utf8"
)

type Contract struct {
    Events []string `json:"events"`
    Environments []string `json:"environments"`
    BodyLimit int64 `json:"body_limit"`
    Tolerance int64 `json:"tolerance_seconds"`
}
type Event struct {
    ID string `json:"id"`
    Name string `json:"event"`
    Environment string `json:"environment"`
    CreatedAt string `json:"created_at"`
    Data map[string]json.RawMessage `json:"data"`
}
func bounded(path string, limit int64) ([]byte, error) {
    file, err := os.Open(path); if err != nil { return nil, err }; defer file.Close()
    value, err := io.ReadAll(io.LimitReader(file, limit+1))
    if err != nil || int64(len(value)) > limit { return nil, errors.New("input unavailable or oversized") }
    return value, nil
}
func contains(values []string, candidate string) bool {
    for _, value := range values { if value == candidate { return true } }; return false
}
func verify(body []byte, headers map[string]string, secret, environment string, now int64, c Contract) (Event, error) {
    var event Event
    fail := errors.New("verification failed")
    if secret == "" || !contains(c.Environments, environment) || int64(len(body)) > c.BodyLimit { return event, fail }
    parts := regexp.MustCompile(`^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$`).FindStringSubmatch(headers["x-lmp-signature"])
    if parts == nil { return event, fail }
    stamp, err := strconv.ParseInt(parts[1],10,64); if err != nil || stamp < now-c.Tolerance || stamp > now+c.Tolerance { return event, fail }
    mac := hmac.New(sha256.New, []byte(secret)); mac.Write([]byte(parts[1]+".")); mac.Write(body)
    supplied, err := hex.DecodeString(parts[2]); if err != nil || !hmac.Equal(mac.Sum(nil),supplied) { return event, fail }
    if !utf8.Valid(body) || json.Unmarshal(body, &event) != nil { return event, fail }
    if !regexp.MustCompile(`^evt_[A-Za-z0-9_-]{1,100}$`).MatchString(event.ID) || !contains(c.Events,event.Name) || event.Environment != environment || event.ID != headers["x-lmp-event-id"] || event.Name != headers["x-lmp-event"] || event.Data == nil || event.CreatedAt == "" { return event, fail }
    return event, nil
}
func run() error {
    if len(os.Args) != 3 { return errors.New("two file paths required") }
    raw, err := bounded("contract.json",32768); if err != nil { return err }; var c Contract
    if json.Unmarshal(raw,&c) != nil || c.BodyLimit != 1048576 || c.Tolerance != 300 { return errors.New("invalid contract") }
    body, err := bounded(os.Args[1],c.BodyLimit); if err != nil { return err }
    rawHeaders, err := bounded(os.Args[2],32768); if err != nil { return err }; var headers map[string]string
    if json.Unmarshal(rawHeaders,&headers) != nil { return errors.New("invalid headers") }
    event, err := verify(body,headers,os.Getenv("KDP_WEBHOOK_SECRET"),strings.ToLower(os.Getenv("KDP_WEBHOOK_ENVIRONMENT")),time.Now().Unix(),c)
    if err != nil { return err }
    result := map[string]any{"verified":true,"event_id":event.ID,"event":event.Name,"environment":event.Environment}
    output, err := json.Marshal(result); if err != nil { return err }; fmt.Println(string(output)); return nil
}
func main() {
    if run() != nil { fmt.Println(`{"verified":false}`); os.Exit(1) }
}

Connect to an HTTP adapter

  1. Read a bounded raw HTTP body before JSON middleware changes it.
  2. Reject duplicate signature, event-ID and event-name header fields.
  3. Normalize header names to lowercase without changing their values.
  4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier.
  5. Atomically persist the verified event ID and work item before sending a 2xx response.

Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the local inbox example to test the durable acknowledgement boundary.

Expected result

All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above.

Common mistakes

  • Reading contract.json from a different working directory or copying only one source without its companion files.
  • Re-encoding JSON before HMAC verification.
  • Reusing an expired test signature instead of generating a current one.
  • Assuming successful authentication deduplicates an additive renewal.

Security notes

Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a developer API key or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate delivery inside its window; use a durable authenticated event-ID ledger and an independent business-operation key.

Troubleshooting

SymptomCauseFix
verified=false for an unchanged fixtureTimestamp expired, wrong secret or wrong environmentRegenerate the private request using the intended Sandbox configuration
Companion file cannot be foundWorking directory or extraction incompleteRestore the complete source archive and work inside webhooks
The same ID verifies againVerifier only authenticates bytesCommit a unique inbox record before applying an effect

Next steps

Run the local HTTP, replay and storage-failure tests.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings