sdk(P1.4): inbound webhook verification helper in all three SDKs
Customers receiving Attesto webhook deliveries can now verify them with one
call, mirroring backend/app/services/webhooks/signing.py exactly: read the
X-Attesto-Timestamp / X-Attesto-Signature headers (case-insensitive), reject
when abs(now - ts) > max_skew_s (300 s default, replay protection), recompute
hex(hmac_sha256(secret, f"{timestamp}.{body}")), constant-time compare
(hmac.compare_digest / charcode-XOR fold over equal-length hex / hmac.Equal).
- Python: attesto.verify_webhook(body=, headers=, secret=, max_skew_s=, now=)
- TypeScript: verifyWebhook({ body, headers, secret, maxSkewS, now }) via
WebCrypto HMAC (edge-safe)
- Go: VerifyWebhook(body, headers, secret, maxSkewS)
New corpus golden-vectors/sdk-parity/webhook.json (valid, within-skew,
skewed-timestamp, bad-signature, tampered-body, wrong-secret,
non-numeric-timestamp) with backend-derived signatures; all three SDKs agree
on every case. READMEs gain a "Receiving Attesto webhooks" example.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -439,6 +439,49 @@ func sameString(a, b any) bool {
|
||||
return a == nil && b == nil
|
||||
}
|
||||
|
||||
func webhookHeader(headers map[string]string, name string) (string, bool) {
|
||||
for key, value := range headers {
|
||||
if strings.EqualFold(key, name) {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// VerifyWebhook verifies an inbound webhook delivered by Attesto. It reads the
|
||||
// X-Attesto-Timestamp / X-Attesto-Signature headers (case-insensitive), rejects
|
||||
// when the timestamp is outside maxSkewS of now, recomputes
|
||||
// hmac_sha256(secret, "<ts>." + body), and compares in constant time. Mirrors
|
||||
// the backend webhook signing scheme.
|
||||
func VerifyWebhook(body []byte, headers map[string]string, secret string, maxSkewS int) bool {
|
||||
return verifyWebhookAt(body, headers, secret, maxSkewS, time.Now().Unix())
|
||||
}
|
||||
|
||||
func verifyWebhookAt(body []byte, headers map[string]string, secret string, maxSkewS int, now int64) bool {
|
||||
timestamp, okTS := webhookHeader(headers, "x-attesto-timestamp")
|
||||
signature, okSig := webhookHeader(headers, "x-attesto-signature")
|
||||
if !okTS || !okSig {
|
||||
return false
|
||||
}
|
||||
ts, err := strconv.ParseInt(strings.TrimSpace(timestamp), 10, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
skew := now - ts
|
||||
if skew < 0 {
|
||||
skew = -skew
|
||||
}
|
||||
if skew > int64(maxSkewS) {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(strconv.FormatInt(ts, 10)))
|
||||
mac.Write([]byte("."))
|
||||
mac.Write(body)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
||||
func SignConnectorWebhookPayload(secret string, body []byte, timestamp int64) (string, string) {
|
||||
if timestamp == 0 {
|
||||
timestamp = time.Now().Unix()
|
||||
|
||||
Reference in New Issue
Block a user