Golang 웹훅 서명 검증: HMAC과 hmac.Equal
Go에서 Sume 웹훅 검증하기: io.ReadAll로 본문을 한 번 읽고, 타임스탬프와 원본 바이트에 HMAC-SHA256을 계산한 뒤, 각 sume-v1 항목을 hmac.Equal로 비교하세요.

Go에서 Sume 웹훅 서명을 검증하려면 io.ReadAll(http.MaxBytesReader(...))로 본문을 한 번 읽고, hmac.New(sha256.New, secret)로 <timestamp>.<raw_body>에 대한 HMAC-SHA256을 계산한 뒤, X-Sume-Webhook-Signature 헤더를 쉼표로 나눈 각 sume-v1= 항목을 hex 디코딩해 hmac.Equal로 MAC과 비교하세요. 재전송 허용 시간을 벗어난 타임스탬프는 거부하고, JSON은 검증한 바이트에서만 언마샬하세요.
Go 관련 내용은 표준 라이브러리 문서의 crypto/hmac, net/http, io, encoding/hex에서, Sume 관련 내용은 Run 웹훅 (영문), 웹훅 (영문), 웹훅 검증에서 가져왔습니다. 모두 2026-09-27에 확인했습니다. Sume에는 Go SDK가 없습니다. @sume-com/sdk는 TypeScript용이며, 문서에 다른 언어로 수신기를 만드는 경우를 위한 서명 방식이 자세히 나와 있습니다. Express 본문 한도를 포함한 Node 버전은 Express 웹훅 서명 검증에 있습니다.
검증의 각 단계에는 어떤 Go 호출을 쓰나요?
모두 표준 라이브러리에 있습니다.
| 단계 | Sume 규칙 | Go |
|---|---|---|
| 원본 본문 | JSON을 파싱하기 전에 원본 바이트를 검증 | io.ReadAll(http.MaxBytesReader(w, r.Body, n)) |
| 헤더 | x-sume-webhook-timestamp와 x-sume-webhook-signature | 대소문자를 구분하지 않고, 헤더가 없으면 ""를 반환하는 r.Header.Get(...) |
| MAC | <timestamp>.<raw_body>에 대한 HMAC-SHA256 | hmac.New(sha256.New, secret) 다음에 Write와 Sum(nil) |
| 서명 | sume-v1=<hex>. 시크릿 교체 중에는 유효한 시크릿마다 항목 하나씩, 쉼표로 구분 | 쉼표 기준으로 strings.Split, 이어서 sume-v1= 뒷부분에 hex.DecodeString |
| 비교 | 상수 시간, 일치하는 항목이 하나라도 있으면 수락 | 모든 항목에 hmac.Equal(got, expected) |
| 재전송 허용 시간 | 벗어난 타임스탬프는 거부. 오 분이 무난한 기본값 | 파싱한 타임스탬프를 time.Now().Unix()와 비교 |
crypto/hmac으로 검증기는 어떻게 작성하나요?
Go의 crypto/hmac 문서는 타이밍 부채널을 피하려면 수신 측에서 hmac.Equal로 MAC을 비교하라고 안내합니다. Equal은 타이밍 정보를 노출하지 않고 두 MAC을 비교합니다. 이 함수는 hex 문자열이 아니라 원시 MAC 바이트를 비교합니다. hex.DecodeString은 hex 문자로만 된 짝수 길이 입력을 기대하며, 루프는 디코딩에 실패한 항목을 건너뜁니다. 시크릿을 교체한 뒤 24시간 동안 Sume는 유효한 시크릿마다 항목을 하나씩 최신순으로 보내므로, 함수는 모든 항목을 확인합니다. Sume의 TypeScript 검증기처럼 빈 시크릿을 거부하므로, 설정되지 않은 환경 변수가 누구나 계산할 수 있는 빈 HMAC 키가 되지 않습니다. 필요한 패키지는 crypto/hmac, crypto/sha256, encoding/hex, strconv, strings, time입니다.
func verifySume(body []byte, ts, header string, secret []byte) bool {
t, err := strconv.ParseInt(ts, 10, 64)
now := time.Now().Unix()
if len(secret) == 0 || err != nil || t < now-300 || t > now+300 { // five-minute window
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
mac.Write(body) // the raw bytes, never re-encoded JSON
expected := mac.Sum(nil)
ok := false
for _, entry := range strings.Split(header, ",") { // two entries during a rotation
sig, found := strings.CutPrefix(strings.TrimSpace(entry), "sume-v1=")
got, err := hex.DecodeString(sig)
if found && err == nil && hmac.Equal(got, expected) {
ok = true // keep checking the other entries
}
}
return ok
}net/http 핸들러에서 원본 본문은 어떻게 읽나요?
서버 요청에서 r.Body는 항상 nil이 아니고 서버가 닫아 주므로, 핸들러는 읽기만 하면 됩니다. io.ReadAll로 바이트 슬라이스에 한 번 읽으세요. 이를 http.MaxBytesReader로 감싸세요. 이 함수는 들어오는 요청 본문을 제한하기 위한 것으로, 한도를 넘으면 *MaxBytesError를 반환합니다. 상한은 Sume가 인라인으로 담는 가장 큰 실행 영수증 크기인 1 MiB보다 크게 잡아, 실행 전달이 여기에 걸리지 않게 하세요. ServeMux 패턴은 메서드까지 매칭할 수 있으므로 http.HandleFunc("POST /hooks/sume", sumeWebhook)로 등록하세요. 핸들러는 encoding/json, io, net/http, os도 import합니다.
func sumeWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2<<20)) // 2 MiB cap
if err != nil { // for example a *http.MaxBytesError past the cap
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
secret := []byte(os.Getenv("SUME_COM_WEBHOOK_SIGNING_SECRET"))
if !verifySume(body, r.Header.Get("X-Sume-Webhook-Timestamp"),
r.Header.Get("X-Sume-Webhook-Signature"), secret) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
var event struct {
Event string `json:"event"`
RequestID string `json:"request_id"` // dedupe key for run webhooks
JobID string `json:"job_id"` // dedupe key for job webhooks
}
if err := json.Unmarshal(body, &event); err != nil { // the verified bytes
http.Error(w, "bad json", http.StatusBadRequest)
return
}
recordOnce(event.Event, event.RequestID, event.JobID, body) // your store or queue
w.WriteHeader(http.StatusNoContent)
}어떤 실수가 Go 검증기를 망가뜨리나요?
망가졌거나 안전하지 않은 검증기는 대부분 다음 중 하나가 원인입니다.
- 검증하기 전에
json.NewDecoder로r.Body를 디코딩하는 경우.io.ReadAll로 얻은 슬라이스를 검증한 뒤, 그 슬라이스를 그대로json.Unmarshal하세요. - 다시 마샬링한 JSON을 해시하는 경우. 키 순서와 공백도 Sume가 서명한 대상의 일부이므로, 파싱했다가 다시 직렬화한 객체는 검증되지 않습니다.
- hex 문자열을
==로, 또는 바이트를bytes.Equal로 비교하는 경우. Go 문서가 MAC에 권하는 대로hmac.Equal을 쓰세요. - 본문 상한이 1 MiB보다 작은 경우. 그러면 큰 영수증은 읽기에 실패하고, 핸들러는 2xx가 아닌 응답을 보내며, Sume는 시도 횟수를 모두 소진할 때까지 재시도합니다.
- 헤더나 시크릿 문제. 헤더 전체를 비교하면 시크릿 교체 기간의 모든 전달에서 실패하며, 시크릿이 맞지 않는지는
x-sume-webhook-secret-fingerprint헤더로 드러납니다. 두 가지 모두 웹훅 전달 디버깅에서 다룹니다.
검증한 뒤 핸들러는 무엇을 해야 하나요?
이벤트를 기록하고, Sume의 10초 시도 시간 안에 204로 응답한 뒤, 느린 작업은 큐에 넘기세요. 핸들러의 구조체에는 실행과 Job의 중복 제거 키인 request_id와 job_id가 이미 들어 있습니다. 나머지 전달 규칙은 Sume 영상 실행용 서명된 웹훅에서 다루며, PHP 버전은 같은 검증을 hash_equals로 수행합니다.
출처
관련 글
연동 카테고리의 다른 글
- Google ADK MCP 도구: McpToolset으로 Sume 서버 연결
McpToolset, API 키 헤더, tool_filter로 Google ADK 에이전트에 Sume 호스팅 MCP 서버를 추가하고, 유료 도구는 실행 전에 확인을 받게 하세요.
- Google Sheets 영상 자동화: Apps Script와 Sume
Apps Script 요청 한 번으로 시트 행 최대 100개를 Sume 영상 실행으로 바꾸고, 시간 기반 트리거로 큐를 폴링해 URL을 다시 시트에 쓰세요.
- Gradio 영상 생성 앱: Sume API 키는 Space 시크릿에
Sume API로 Gradio 영상 생성 앱을 만드세요. 키는 Hugging Face Space 시크릿에 두고, 제너레이터가 Job을 폴링하고, gr.Video가 영상을 재생합니다.
- Inngest 이벤트 대기: Sume 영상 실행이 끝나면 재개
step.run에서 Sume 실행을 시작하고, transform으로 웹훅을 Inngest 이벤트로 바꾼 뒤, 실행 id를 기준으로 2시간 타임아웃을 둔 step.waitForEvent로 기다리세요.
작성자 Sume