DEVekspertiz.app

02 · İmza Örnekleri

Her dilde çalışan, kopyala-yapıştır referans implementasyonları. Hepsi aynı kanonik string formatını üretir; uygun olanı projenize taşıyın.

curl + bash

#!/usr/bin/env bash
set -euo pipefail

KEY_ID="${TMK_KEY_ID:?}"
SECRET="${TMK_SECRET:?}"
METHOD="${1:-GET}"
PATH_AND_QUERY="${2:-/api/v1/partner/health}"
BODY="${3:-}"

BASE="https://api.ekspertiz.app"

PATH_ONLY="${PATH_AND_QUERY%%\?*}"
if [[ "$PATH_AND_QUERY" == *"?"* ]]; then
  RAW_QUERY="${PATH_AND_QUERY#*\?}"
  CANONICAL_QUERY=$(echo "$RAW_QUERY" \
    | tr '&' '\n' \
    | sort \
    | paste -sd '&' -)
else
  CANONICAL_QUERY=""
fi

TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_SHA=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')

CANONICAL="${METHOD}
${PATH_ONLY}
${CANONICAL_QUERY}
${TS}
${NONCE}
${BODY_SHA}"

SIG=$(printf '%s' "$CANONICAL" \
  | openssl dgst -sha256 -hmac "$SECRET" -hex \
  | awk '{print $2}')

curl -sS "${BASE}${PATH_AND_QUERY}" \
  -X "$METHOD" \
  -H "X-Trameks-Key-Id: $KEY_ID" \
  -H "X-Trameks-Timestamp: $TS" \
  -H "X-Trameks-Nonce: $NONCE" \
  -H "X-Trameks-Signature: sha256=$SIG" \
  ${BODY:+-d "$BODY"} \
  ${BODY:+-H "Content-Type: application/json"}

Not: Bash'in sort'u tam RFC 3986 değil — production'da Node/Python/PHP örneklerini tercih edin.

Node.js (ESM)

import crypto from "node:crypto";

const BASE = process.env.TMK_BASE ?? "https://api.ekspertiz.app";

function canonicalizeQuery(rawQuery) {
  const q = rawQuery.startsWith("?") ? rawQuery.slice(1) : rawQuery;
  if (!q) return "";
  const pairs = [];
  for (const seg of q.split("&")) {
    if (!seg) continue;
    const i = seg.indexOf("=");
    const k = i === -1 ? seg : seg.slice(0, i);
    const v = i === -1 ? "" : seg.slice(i + 1);
    pairs.push([rfc3986(decodeURIComponent(k)), rfc3986(decodeURIComponent(v))]);
  }
  pairs.sort((a, b) =>
    a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : a[0] < b[0] ? -1 : 1
  );
  return pairs.map(([k, v]) => `${k}=${v}`).join("&");
}

function rfc3986(s) {
  return encodeURIComponent(s)
    .replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
    .replace(/%[0-9a-f]{2}/g, (m) => m.toUpperCase());
}

function sha256Hex(buf) {
  return crypto.createHash("sha256").update(buf).digest("hex");
}

export async function partnerRequest({ method = "GET", path, body = "" }) {
  const keyId = process.env.TMK_KEY_ID;
  const secret = process.env.TMK_SECRET;
  if (!keyId || !secret) throw new Error("TMK_KEY_ID / TMK_SECRET env yok");

  const qIdx = path.indexOf("?");
  const pathOnly = qIdx === -1 ? path : path.slice(0, qIdx);
  const queryRaw = qIdx === -1 ? "" : path.slice(qIdx + 1);

  const ts = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString("hex");
  const bodyHash = sha256Hex(body);

  const canonical = [
    method.toUpperCase(),
    pathOnly,
    canonicalizeQuery(queryRaw),
    ts,
    nonce,
    bodyHash,
  ].join("\n");

  const signature =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(canonical).digest("hex");

  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      "X-Trameks-Key-Id": keyId,
      "X-Trameks-Timestamp": ts,
      "X-Trameks-Nonce": nonce,
      "X-Trameks-Signature": signature,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body || undefined,
  });
  return { status: res.status, body: await res.json(), headers: res.headers };
}

// Kullanım:
const r = await partnerRequest({ path: "/api/v1/partner/health" });
console.log(r.status, r.body);

Python (requests)

import hashlib
import hmac
import os
import secrets
import time
import urllib.parse

import requests

BASE = os.environ.get("TMK_BASE", "https://api.ekspertiz.app")

def rfc3986(s: str) -> str:
    return urllib.parse.quote(s, safe="-._~")

def canonicalize_query(raw: str) -> str:
    if not raw:
        return ""
    if raw.startswith("?"):
        raw = raw[1:]
    pairs = []
    for seg in raw.split("&"):
        if not seg:
            continue
        if "=" in seg:
            k, v = seg.split("=", 1)
        else:
            k, v = seg, ""
        k = rfc3986(urllib.parse.unquote(k))
        v = rfc3986(urllib.parse.unquote(v))
        pairs.append((k, v))
    pairs.sort()
    return "&".join(f"{k}={v}" for k, v in pairs)

def sha256_hex(b: bytes) -> str:
    return hashlib.sha256(b).hexdigest()

def partner_request(method: str, path: str, body: str = "") -> requests.Response:
    key_id = os.environ["TMK_KEY_ID"]
    secret = os.environ["TMK_SECRET"]

    if "?" in path:
        path_only, raw_query = path.split("?", 1)
    else:
        path_only, raw_query = path, ""

    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    body_hash = sha256_hex(body.encode("utf-8"))

    canonical = "\n".join([
        method.upper(),
        path_only,
        canonicalize_query(raw_query),
        ts,
        nonce,
        body_hash,
    ])

    sig = hmac.new(
        secret.encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    headers = {
        "X-Trameks-Key-Id": key_id,
        "X-Trameks-Timestamp": ts,
        "X-Trameks-Nonce": nonce,
        "X-Trameks-Signature": f"sha256={sig}",
    }
    if body:
        headers["Content-Type"] = "application/json"

    return requests.request(
        method,
        f"{BASE}{path}",
        headers=headers,
        data=body or None,
        timeout=30,
    )

# Kullanım:
r = partner_request("GET", "/api/v1/partner/health")
print(r.status_code, r.json())

PHP

<?php

function rfc3986(string $s): string {
    return strtr(rawurlencode($s), ['%7E' => '~']);
}

function canonicalizeQuery(string $raw): string {
    if ($raw === '' || $raw === '?') return '';
    if ($raw[0] === '?') $raw = substr($raw, 1);
    $pairs = [];
    foreach (explode('&', $raw) as $seg) {
        if ($seg === '') continue;
        $eq = strpos($seg, '=');
        if ($eq === false) {
            $k = $seg; $v = '';
        } else {
            $k = substr($seg, 0, $eq);
            $v = substr($seg, $eq + 1);
        }
        $pairs[] = [rfc3986(urldecode($k)), rfc3986(urldecode($v))];
    }
    usort($pairs, fn($a, $b) =>
        $a[0] === $b[0] ? strcmp($a[1], $b[1]) : strcmp($a[0], $b[0])
    );
    return implode('&', array_map(fn($p) => "{$p[0]}={$p[1]}", $pairs));
}

function partnerRequest(string $method, string $path, string $body = ''): array {
    $base = getenv('TMK_BASE') ?: 'https://api.ekspertiz.app';
    $keyId = getenv('TMK_KEY_ID');
    $secret = getenv('TMK_SECRET');
    if (!$keyId || !$secret) {
        throw new RuntimeException('TMK_KEY_ID / TMK_SECRET env yok');
    }

    $qIdx = strpos($path, '?');
    $pathOnly = $qIdx === false ? $path : substr($path, 0, $qIdx);
    $queryRaw = $qIdx === false ? '' : substr($path, $qIdx + 1);

    $ts = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $bodyHash = hash('sha256', $body);

    $canonical = implode("\n", [
        strtoupper($method),
        $pathOnly,
        canonicalizeQuery($queryRaw),
        $ts,
        $nonce,
        $bodyHash,
    ]);

    $sig = 'sha256=' . hash_hmac('sha256', $canonical, $secret);

    $headers = [
        "X-Trameks-Key-Id: $keyId",
        "X-Trameks-Timestamp: $ts",
        "X-Trameks-Nonce: $nonce",
        "X-Trameks-Signature: $sig",
    ];
    if ($body !== '') $headers[] = 'Content-Type: application/json';

    $ch = curl_init("$base$path");
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_POSTFIELDS => $body !== '' ? $body : null,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ['status' => $status, 'body' => json_decode($body, true)];
}

// Kullanım:
$r = partnerRequest('GET', '/api/v1/partner/health');
print_r($r);

Yaygın hatalar

BelirtiSebep
invalid_signature her seferindeBody byte'ı serialize ederken farklılaşıyor; HTTP client'ın gönderdiği byte'ı hash'leyin
expired_timestampSunucu saati NTP ile senkronize değil
invalid_signature query'li endpoint'lerdeQuery canonicalize doğru değil (sıralama / encoding)
replay_detected retry'daNonce'u retry'da yeniden üretin, asla aynı nonce'u tekrar göndermeyin

Sonraki: 03 · Rate limit.