# 서명 검증 (/docs/webhooks/signature-verification)

한국AI통신은 모든 웹훅 요청에 `X-Signature` 헤더를 포함합니다. HMAC-SHA256 기반으로 요청의 무결성을 검증할 수 있습니다.

프로덕션 환경에서는 반드시 서명을 검증하세요. 서명 검증을 하지 않으면 외부에서 위조된 요청을 보낼 수 있습니다.

이 서명 검증 방법은 [수신 전화(VoiceML)](/docs/build/voiceml), [Status Callback](/docs/webhooks/status), [Message Webhook](/docs/webhooks/message) 모두에 동일하게 적용됩니다.

## 서명 생성 알고리즘

1. 웹훅 URL 문자열로 시작
2. 파라미터를 키 사전순(알파벳)으로 정렬
3. 각 key + value를 URL 뒤에 순서대로 연결
4. `HMAC-SHA256(signingKey, data)` 결과를 Base64 인코딩

## Python 수동 구현

```python
import hmac, hashlib, base64
from flask import Flask, request

app = Flask(__name__)
SIGNING_KEY = "your_signing_key"

@app.route("/webhook", methods=["POST"])
def webhook():
    signature = request.headers.get("X-Signature", "")
    params = request.form.to_dict()
    url = request.url

    # 파라미터를 키 사전순으로 정렬 후 URL 뒤에 연결
    sorted_params = sorted(params.items())
    data = url + "".join(f"{k}{v}" for k, v in sorted_params)

    # HMAC-SHA256 서명 생성
    expected = base64.b64encode(
        hmac.new(SIGNING_KEY.encode(), data.encode(), hashlib.sha256).digest()
    ).decode()

    if not hmac.compare_digest(signature, expected):
        return "Unauthorized", 401

    # 서명 검증 성공
    call_id = request.form["CallId"]
    ...
```

## SDK를 사용한 검증

한국AI통신 Python SDK의 `webhooks.verify()` 메서드를 사용하면 서명 검증을 간결하게 처리할 수 있습니다.

```python
from clawops import ClawOps, WebhookVerificationError

client = ClawOps()

@app.route("/webhook", methods=["POST"])
def webhook():
    try:
        client.webhooks.verify(
            url="https://my-app.com/webhook",
            params=request.form.to_dict(),
            signature=request.headers["X-Signature"],
            signing_key="your_signing_key",
        )
    except WebhookVerificationError:
        return "Unauthorized", 401

    call_id = request.form["CallId"]
    ...
```


Signing Key는 대시보드 > 개발자 > 웹훅 의 **Webhook Signing Secret** 에서 확인할 수 있습니다. API Key와는 별도의 값입니다.

## Signing Key 재발급

계정 소유자는 같은 화면의 **재발급** 버튼으로 새 Signing Key 를 받을 수 있습니다.

재발급하면 기존 키는 즉시 폐기되고, 그 뒤의 모든 요청은 새 키로 서명됩니다. 두 키가 함께 쓰이는 기간은 없습니다. 재발급 시점에 진행 중인 통화의 요청은 통화가 끝날 때까지 기존 키로 서명될 수 있습니다. 콜 플로우나 인증 전화로 진행 중인 통화는 재발급 직후 끊깁니다.

서비스 영향 없이 바꾸려면 수신 서버가 기존 키와 새 키 중 하나로 검증되면 통과하도록 준비한 뒤 재발급하고, 진행 중이던 통화가 끝난 뒤 기존 키를 지우세요.

## 재전송(replay) 방어

서명은 **요청이 우리에게서 왔다**는 것만 보장합니다. 서명까지 그대로 복사한 요청을 나중에 다시 보내는
재전송 공격은 서명 검증만으로는 막을 수 없습니다.

Status Callback · Message Webhook · 계정 레벨 이벤트 웹훅(`callflow.ended` 등)에는 발생 시각인
`Timestamp`(ISO 8601)가 들어 있고 **이 값도 서명 대상에 포함**되므로 위조할 수 없습니다.
서명 검증을 통과한 뒤 시각이 너무 오래됐으면 거부하세요.

```python
from datetime import datetime, timezone

MAX_AGE_SEC = 300  # 5분

ts = datetime.fromisoformat(request.form["Timestamp"].replace("Z", "+00:00"))
age = (datetime.now(timezone.utc) - ts).total_seconds()
if age > MAX_AGE_SEC:
    return "Stale webhook", 400
```


재시도로 도착한 요청은 **처음 발생 시각**을 유지합니다. 허용 범위를 너무 좁게 잡으면 정상 재시도가
거부될 수 있으니 5분 이상을 권장합니다. 중복 수신 자체는 `CallId` 를 idempotency key 로 처리하세요.

[수신 전화(VoiceML)](/docs/build/voiceml) 요청에는 `Timestamp` 가 **없습니다** — 첫 요청과
`action` 콜백 모두 해당합니다. 위 코드를 그대로 옮기면 값을 찾지 못해 실패하므로, VoiceML
경로에서는 이 방식 대신 `action` URL 에 직접 넣어 둔 일회성 토큰이나 `CallId` 기준의 상태
관리로 재전송을 걸러내세요.

