Outbound Request Signing

When TengineAI calls your API on behalf of a tool, it can sign or authenticate the outbound request so your server can verify the request came from TengineAI — and not an arbitrary caller.

Three strategies are available, configured per tool via auth_strategy.


Decision Matrix

Strategyauth_strategyBody integrityReplay protectionUse when
NonenonePublic endpoints; network-layer trust (VPN, IP allowlist)
Static Bearerstatic_bearerThird-party webhooks; simple token-gated APIs
Request Signinghmac_signatureYour own APIs — recommended default

None

No authentication headers are added. The request is sent as-is.

Use only for public endpoints or when the network layer (VPN, IP allowlist) is the trust boundary.


Static Bearer

TengineAI adds a standard Authorization: Bearer <secret> header to every outbound request.

{
  "auth_strategy": "static_bearer",
  "encrypted_auth_secret": "your-api-token"
}

The secret is stored encrypted at rest and injected at execution time:

Authorization: Bearer <your-api-token>

Does not prevent replay. A captured request can be replayed indefinitely — the token never changes. To mitigate: check X-Tengine-Timestamp (reject requests older than ±5 minutes) and deduplicate on X-Tengine-Request-Id. Rotate the token regularly.


TengineAI computes an HMAC-SHA256 signature over a canonical string derived from the request and adds it as a header. Your server verifies the signature using the same shared secret.

{
  "auth_strategy": "hmac_signature",
  "encrypted_auth_secret": "your-shared-secret"
}

This provides:

  • Origin verification — only TengineAI (holding the secret) can produce a valid signature
  • Body integrity — the signature covers a SHA256 hash of the request body
  • Replay protection — the timestamp and request ID are included in the signed payload; reject requests outside your tolerance window

Headers Added

HeaderValue
X-Tengine-Signaturetng2=<hex-encoded HMAC-SHA256>
X-Tengine-TimestampUnix timestamp (seconds)
X-Tengine-Request-IdUUID — unique per request

These are added alongside the standard identity headers (X-Tengine-Project-Id, X-Tengine-Member-Id, etc.).


Canonical String Format

TengineAI signs the following canonical string — a single space-delimited line:

tng2 <timestamp> <request_id> <METHOD> <host> <path_and_query> <body_sha256> <project_id> <member_id>
ComponentDescription
tng2Fixed version prefix
<timestamp>Value of X-Tengine-Timestamp (Unix seconds, string)
<request_id>Value of X-Tengine-Request-Id (UUID string)
<METHOD>HTTP method, uppercased (GET, POST, etc.)
<host>Host + port from the request URL (e.g. api.example.com)
<path_and_query>Path and query string (e.g. /v1/users/42?env=prod)
<body_sha256>SHA256 hex digest of the JSON body (keys sorted, compact); empty string if no body
<project_id>TengineAI project ID (string); empty string if unavailable
<member_id>Member external ID from session; empty string if no member context

Body serialization:

json.dumps(body, sort_keys=True, separators=(",", ":"))

Signature computation: HMAC-SHA256, hex-encoded:

hmac.new(secret.encode("utf-8"), canonical_string.encode("utf-8"), hashlib.sha256).hexdigest()

Verifying the Signature

Complete Python middleware you can drop into your API:

import hashlib
import hmac
import json
import time
from typing import Optional

SHARED_SECRET = "your-shared-secret"
MAX_TIMESTAMP_DRIFT_SECONDS = 300  # Reject requests older than 5 minutes


def verify_tengine_signature(
    *,
    method: str,
    host: str,
    path_and_query: str,
    body: Optional[dict],
    x_tengine_signature: str,
    x_tengine_timestamp: str,
    x_tengine_request_id: str = "",
    x_tengine_project_id: str = "",
    x_tengine_member_id: str = "",
) -> bool:
    # 1. Reject stale requests
    try:
        request_time = int(x_tengine_timestamp)
    except ValueError:
        return False

    if abs(time.time() - request_time) > MAX_TIMESTAMP_DRIFT_SECONDS:
        return False

    # 2. Compute body SHA256
    if body:
        body_json = json.dumps(body, sort_keys=True, separators=(",", ":"))
        body_sha256 = hashlib.sha256(body_json.encode("utf-8")).hexdigest()
    else:
        body_sha256 = ""

    # 3. Build canonical string
    canonical = (
        f"tng2 {x_tengine_timestamp} {x_tengine_request_id} {method.upper()} "
        f"{host} {path_and_query} {body_sha256} {x_tengine_project_id} {x_tengine_member_id}"
    )

    # 4. Compute expected signature
    expected = hmac.new(
        SHARED_SECRET.encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    # 5. Parse received signature (format: "tng2=<hex>")
    if not x_tengine_signature.startswith("tng2="):
        return False
    received = x_tengine_signature[len("tng2="):]

    # 6. Constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, received)

FastAPI usage:

from fastapi import FastAPI, Header, Request, HTTPException

app = FastAPI()

@app.post("/v1/contacts/lookup")
async def handle_tool_call(
    request: Request,
    x_tengine_signature: str = Header(...),
    x_tengine_timestamp: str = Header(...),
    x_tengine_request_id: str = Header(default=""),
    x_tengine_project_id: str = Header(default=""),
    x_tengine_member_id: str = Header(default=""),
):
    body = await request.json()
    parsed = request.url

    valid = verify_tengine_signature(
        method=request.method,
        host=parsed.netloc,
        path_and_query=parsed.path + (f"?{parsed.query}" if parsed.query else ""),
        body=body,
        x_tengine_signature=x_tengine_signature,
        x_tengine_timestamp=x_tengine_timestamp,
        x_tengine_request_id=x_tengine_request_id,
        x_tengine_project_id=x_tengine_project_id,
        x_tengine_member_id=x_tengine_member_id,
    )

    if not valid:
        raise HTTPException(status_code=401, detail="Invalid signature")

    # x_tengine_member_id identifies which user TengineAI is acting on behalf of.
    # It is attribution from the session context — not an authentication credential.
    return {"status": "ok"}

Common Verification Mistakes

Wrong body serialization — The body must be serialized with sort_keys=True and compact separators (",", ":") before hashing. Any difference in serialization produces a different hash.

Missing path_and_query — If the URL has a query string, it must be included: /v1/users?active=true, not just /v1/users.

Missing request_id in canonical string — tng2 includes X-Tengine-Request-Id in the signed payload. Omitting it will cause every verification to fail.

Comparing with == — Always use hmac.compare_digest() to prevent timing attacks.

Not checking the timestamp — Without a drift window check, valid signatures can be replayed indefinitely.


Member Identity Headers — Attribution, Not Authentication

Every tool call includes X-Tengine-Project-Id, X-Tengine-Member-Id, and related headers. These are attribution — they tell your API which project and user the model is acting on behalf of, derived from the authenticated TengineAI session.

They are not authentication credentials. Do not use X-Tengine-Member-Id as a trust signal. Verify the request using the signature or bearer token first, then use the identity headers for business logic (e.g. "look up records for this member").


Next Steps