Xác minh chữ ký

Kiểm tra header X-Nex-Signature bằng HMAC-SHA256 trên thân thô, kèm mã mẫu Node.js, Python, C#, PHP.

Chữ ký là HMAC-SHA256(secret, "{t}." + raw_body), in ra dạng hex chữ thường.

  • secret là bí mật ký NexERP đã cấp.

  • t là số giây Unix lấy từ header.

  • raw_body là các byte thô nhận được. Đừng đọc JSON rồi dựng lại chuỗi, vì khoảng trắng và thứ tự khóa sẽ khác và chữ ký sai.

Các bước kiểm tra

  1. Tách t và v1

    Đọc header X-Nex-Signature, tách hai giá trị t và v1.

  2. Chống phát lại

    Từ chối nếu giờ hiện tại lệch t quá 300 giây.

  3. Tính HMAC

    Tính HMAC-SHA256 trên "{t}." + raw_body, khóa là bí mật ký.

  4. So sánh chữ ký

    So với v1 bằng hàm so sánh thời gian hằng số. Không dùng ==.

  5. Loại trùng

    Cùng X-Nex-Event-Id đến nhiều lần thì chỉ xử lý một lần.

Node.js

const crypto = require("crypto");

// rawBody phải là Buffer. Với Express: app.post("/hook", express.raw({ type: "application/json" }), handler)
function verifySignature(secret, header, rawBody, toleranceSec = 300) {
  const parts = {};
  for (const part of String(header || "").split(",")) {
    const i = part.indexOf("=");
    if (i > 0) parts[part.slice(0, i)] = part.slice(i + 1);
  }
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${t}.`), rawBody]))
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hashlib
import hmac
import time


def verify_signature(secret: str, signature_header: str, raw_body: bytes, tolerance: int = 300, now: float | None = None) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in signature_header.split(","))
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs((time.time() if now is None else now) - t) > tolerance:
        return False
    expected = hmac.new(secret.encode("utf-8"), str(t).encode("ascii") + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode("ascii"), v1.encode("utf-8", "ignore"))

# Flask: verify_signature(SECRET, request.headers["X-Nex-Signature"], request.get_data())

C#

using System;
using System.Security.Cryptography;
using System.Text;

public static class NexSignature
{
    // rawBody: các byte thô của thân request (đọc Request.Body vào byte[] trước khi parse JSON)
    public static bool Verify(string secret, string header, byte[] rawBody, int toleranceSec = 300)
    {
        long t = 0;
        string v1 = null;
        foreach (var part in (header ?? "").Split(','))
        {
            var kv = part.Split('=', 2);
            if (kv.Length != 2) continue;
            if (kv[0] == "t") long.TryParse(kv[1], out t);
            else if (kv[0] == "v1") v1 = kv[1];
        }
        if (t == 0 || v1 == null) return false;
        if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > toleranceSec) return false;

        var prefix = Encoding.UTF8.GetBytes(t + ".");
        var data = new byte[prefix.Length + rawBody.Length];
        Buffer.BlockCopy(prefix, 0, data, 0, prefix.Length);
        Buffer.BlockCopy(rawBody, 0, data, prefix.Length, rawBody.Length);

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var expected = Convert.ToHexString(hmac.ComputeHash(data)).ToLowerInvariant();
        return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(v1));
    }
}

PHP

<?php
function verify_signature(string $secret, string $header, string $rawBody, int $tolerance = 300): bool
{
    $t = null;
    $v1 = null;
    foreach (explode(",", $header) as $part) {
        $kv = explode("=", $part, 2);
        if (count($kv) !== 2) continue;
        if ($kv[0] === "t") $t = $kv[1];
        elseif ($kv[0] === "v1") $v1 = $kv[1];
    }
    if ($t === null || !ctype_digit($t) || $v1 === null) return false;
    if (abs(time() - (int)$t) > $tolerance) return false;
    $expected = hash_hmac("sha256", $t . "." . $rawBody, $secret);
    return hash_equals($expected, $v1);
}

// $ok = verify_signature($secret, $_SERVER["HTTP_X_NEX_SIGNATURE"] ?? "", file_get_contents("php://input"));

Lưu ý

  • Mọi mẫu trên tính cùng một thứ: HMAC-SHA256 của các byte "{t}." nối với thân thô, khóa là bí mật ký.

  • Đồng hồ máy bạn lệch quá 300 giây thì chữ ký đúng vẫn bị từ chối. Hãy đồng bộ giờ máy.