서명 검증
모든 웹훅 전송에는 X-ParaSta-Signature 헤더가 포함됩니다.
X-ParaSta-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt는 ParaSta 가 요청에 서명한 시점의 Unix 타임스탬프(초)입니다.v1는 엔드포인트 서명 시크릿으로 키잉된<t>.<raw body>의 HMAC-SHA256 hex 다이제스트입니다.
검증 알고리즘
섹션 제목: “검증 알고리즘”- 헤더에서
t와v1를 파싱합니다. t가 5분 이상 과거이거나 5분 이상 미래라면 거부합니다.expected = HMAC_SHA256(secret, t + '.' + raw_body)(hex) 를 계산합니다.expected와v1를 상수 시간 비교(constant-time)로 비교합니다.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map(s => s.split('='))); const t = parts.t, v1 = parts.v1; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}
// Express:app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const ok = verify(req.body.toString('utf8'), req.headers['x-parasta-signature'], process.env.WEBHOOK_SECRET); if (!ok) return res.status(400).send('bad signature'); const event = JSON.parse(req.body.toString('utf8')); // ...handle event res.json({ received: true });});import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split('=', 1) for p in header.split(',')) t, v1 = parts['t'], parts['v1'] if abs(int(time.time()) - int(t)) > 300: return False expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1)
# Flask:@app.post('/webhook')def webhook(): raw = request.get_data() if not verify(raw, request.headers['X-ParaSta-Signature'], os.environ['WEBHOOK_SECRET']): abort(400) event = json.loads(raw) # ...handle event return {'received': True}# (검증은 서버 사이드에서 수행; cURL 예시는 수동 테스트용)# 커맨드라인에서 재계산:body='{"id":"evt_test"}'ts=1730000000signature=$(printf "%s.%s" "$ts" "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')echo "t=$ts,v1=$signature"흔한 실수
섹션 제목: “흔한 실수”- 검증 전에 본문을 파싱 — JSON 파싱은 공백을 변경해 HMAC 이 깨집니다. 항상 본문의 원본 바이트 로 검증하세요.
- 단일 글로벌 시크릿을 여러 엔드포인트에 재사용 — 각 엔드포인트는 자체 시크릿을 가집니다.
- 상수 시간 비교 대신 문자열 동등 비교 사용 — 타이밍 공격에 취약합니다.