Проверка подписи

Как убедиться, что уведомление пришло от нас.

Секрет уведомлений (whsec_…) показывается один раз — при создании ключа или когда вы просите новый.

Подпись — HMAC-SHA256 от строки <t>.<тело запроса как есть> вашим секретом, в шестнадцатеричном виде:

  1. возьмите из заголовка Golosar-Signature значения t и v1;
  2. посчитайте HMAC-SHA256 от t + "." + тело секретом;
  3. сравните с v1 безопасным сравнением;
  4. проверьте, что t не старше 30 минут — так старое уведомление не повторят.

Подпись считается от тела как оно пришло — не разбирайте JSON и не собирайте его заново перед проверкой.

import hashlib, hmac, os, time

def verify(secret: str, body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > 1800:
        return False
    mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts["v1"])

# Flask
from flask import Flask, request, abort
app = Flask(__name__)

@app.post("/golosar/webhook")
def hook():
    if not verify(os.environ["GOLOSAR_WHSEC"], request.get_data(), request.headers["Golosar-Signature"]):
        abort(400)
    event = request.get_json()
    print(event["type"], event["data"]["job"]["id"])
    return "", 204
import crypto from "node:crypto";
import express from "express";

export function verify(secret, rawBody, header) {
  const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 1800) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${p.t}.`).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(p.v1));
}

const app = express();
app.post("/golosar/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(process.env.GOLOSAR_WHSEC, req.body, req.get("Golosar-Signature"))) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.job.id);
  res.sendStatus(204);
});