웹훅 Content-Digest 검수HTTP Message Signatures 본문 검증RFC 9421 Content-Digest웹훅 서명 본문 불일치

웹훅 본문 무결성 검수, 서명은 맞는데 본문이 다를 때

웹훅의 HTTP 메시지 서명과 Content-Digest를 대조해 본문 무결성을 확인하는 기준입니다. 서명 범위와 수신 바이트가 달라지는 가상 표본 7개를 검수합니다.

이 글을 쓴 알파카랩스카카오·네이버·쿠팡 출신, 재하청 0%, 대기업·공공기관 등 18개사+ 레퍼런스

발행 ·알파카랩스

RFC 9421로 웹훅 본문까지 보호하려면 Content-Digest를 서명 범위에 넣고 수신 바이트의 해시와 대조합니다. 2026년 11월 10일 기준입니다.

주문 상태를 받는 웹훅에서 packed가 shipped로 바뀌었는데 서명 검사가 통과했다고 해 보겠습니다. RFC 9421 방식에서 서명 대상이 해시 필드이고 그 필드가 그대로라면 서명만으로는 본문 변경을 발견할 수 없습니다. 서명에 포함된 해시와 수신 콘텐츠의 바이트를 따로 대조해야 합니다.

서명은 통과했는데 본문이 왜 달라질 수 있나요?#

RFC 9421의 HTTP 메시지 서명은 요청에 포함된 구성 요소 중 서명 대상으로 정한 값을 확인합니다. 서명 베이스에 content-digest를 포함하면 그 필드 값이 서명으로 보호됩니다. 실제 HTTP 콘텐츠를 해시한 값과 일치하는지도 수신기가 확인합니다. RFC 9530은 Content-Digest의 알고리즘과 결과 바이트를 구조화된 딕셔너리에 담는 방법을 정의합니다. RFC 9421 본문 검증 · RFC 9530 Content-Digest

이 글은 송·수신자가 서명 대상과 해시 알고리즘을 미리 합의한 웹훅을 가정합니다. 수신기는 신뢰하는 발신 키로 서명을 검사하고, 같은 서명이 Content-Digest 필드 전체를 포함했는지 확인한 뒤, 허용된 SHA-256 값과 수신 콘텐츠에서 계산한 값을 대조합니다.

어떤 요청을 통과·거부 표본에 넣나요?#

아래 표는 POST 웹훅의 대상 URI와 콘텐츠 타입을 고정한 가상 표본입니다. 현업 인수표에는 서명 결과와 서명 범위, 수신 콘텐츠 일치 여부, 업무 처리 결과를 별도 열로 둡니다.

가상 요청서명 검사본문 검수 판정
서명·필드·본문 일치통과pass-content
서명과 해시 필드는 그대로, 본문만 변경통과reject-content
서명은 그대로, 본문과 해시 필드를 교체실패reject-signature
해시 필드를 포함하지 않는 유효한 서명통과reject-coverage
서명된 SHA-256 값이 32바이트보다 짧음통과reject-digest
이 프로필이 받지 않는 SHA-512 값만 있음통과reject-digest
JSON 값은 같고 공백만 변경통과reject-content
가상 요청 7개에서 Ed25519 서명과 SHA-256 해시를 실행했습니다. 제한된 예시 서명 베이스와 합성 키를 사용했습니다.

함수는 신뢰하는 발신 키로 검사한 서명 결과와 그 서명이 해시 필드 전체를 포함했는지 확인한 값, 파서가 추출한 SHA-256 바이트와 수신 콘텐츠 바이트를 입력받습니다. 콘텐츠 인코딩 없는 요청을 가정합니다. 키 관리, 서명 입력 파싱, 대상 URI·메서드·콘텐츠 타입의 예상값과 서명 시각 검사는 앞 단계에서 수행합니다. 이 함수의 통과는 본문 검수 통과 상태입니다.

import { createHash, timingSafeEqual } from "node:crypto";

export function checkContent({ signatureValid, coversDigest, digest, content }) {
  if (!signatureValid) return "reject-signature";
  if (!coversDigest) return "reject-coverage";
  if (!Buffer.isBuffer(digest) || digest.length !== 32) {
    return "reject-digest";
  }
  const actual = createHash("sha256").update(content).digest();
  return timingSafeEqual(actual, digest) ? "pass-content" : "reject-content";
}

JSON을 객체로 읽고 다시 문자열로 만들면 공백이나 직렬화 형식이 달라질 수 있습니다. HTTP 전송 프레이밍이 제거된 콘텐츠 바이트를 사용하고, Content-Encoding이 있는 요청은 해시 계산 시점의 인코딩된 콘텐츠를 확인합니다. 프록시가 압축을 풀거나 변환하는 환경은 검증 지점에서 어떤 바이트를 읽는지 따로 시험합니다. 프록시나 본문 파서가 바뀌면 같은 표본을 다시 돌립니다. RFC 9530 인코딩 차이

모든 웹훅에 Content-Digest가 필요한가요?#

공급자마다 수신 검증 계약이 다릅니다. 예를 들어 GitHub 웹훅은 수신 본문에 대한 별도 HMAC 서명을 검증하도록 안내합니다. 그 경우 GitHub의 요청 형식과 헤더로 시험합니다. Content-Digest를 요구할지는 이 글의 예시처럼 RFC 9421 기반 프로필을 구성할 때 정합니다. GitHub 웹훅 검증

유효한 요청이 재전송되면 서명과 본문이 둘 다 일치할 수 있습니다. 업무 상태 반영에는 재생 제한, 중복 이벤트와 현재 상태 전이 규칙을 별도로 검수합니다. 인증을 통과한 이벤트의 적용 순서는 SCM 웹훅 이벤트 순서 검수에서 다룹니다.

어떤 문서에서 기준을 확인했나요?#

  • IETF RFC 9421 §7.2.8은 서명과 콘텐츠 해시를 대조하는 두 검증을 설명합니다. 2024년 2월 공개.
  • IETF RFC 9530 §2는 Content-Digest의 입력과 필드 형식을 정의합니다. 2024년 2월 공개.
  • IETF 자동화 트래픽 서명 초안은 RFC 9421을 사용해 자동화 트래픽을 식별하는 프로토콜을 제안합니다. 2026년 10월 29일 버전이며, 이 글의 웹훅 본문 판정은 위 두 RFC를 근거로 삼았습니다.
  • 알파카랩스 업무 자동화·API 연동은 API 접근, 실패·중복과 운영 인수 조건을 협의하는 서비스를 안내합니다.

공개 문서는 2026년 11월 10일 확인했습니다. 거부 라벨과 허용 알고리즘은 가상 프로필의 정책입니다.

자주 묻는 질문#

서명이 통과하면 웹훅 본문도 확인된 건가요?

서명 범위에 포함된 값이 확인됩니다. Content-Digest로 본문까지 검수하려면 그 필드가 서명 범위에 있는지 확인하고 수신 콘텐츠 바이트에서 해시를 다시 계산해 대조합니다.

받은 JSON을 파싱한 뒤 다시 문자열로 바꿔도 되나요?

수신 콘텐츠의 바이트를 검증에 사용합니다. JSON을 다시 직렬화하면 공백이나 필드 표현이 달라져 같은 데이터도 해시가 달라질 수 있습니다.

Content-Encoding이 있으면 어떤 바이트를 쓰나요?

Content-Digest는 실제로 전달된 HTTP 콘텐츠에 적용합니다. 압축이 적용된 요청은 전송 프레이밍이 제거된 인코딩된 콘텐츠를 확인하고, 프록시와 파서가 바이트를 바꾸는 지점을 검수합니다.

Content-Digest 값만 같으면 발신자를 확인한 건가요?

해시 일치는 받은 콘텐츠와 필드 값의 관계를 확인합니다. 발신자를 검증하려면 신뢰하는 키와 합의한 서명 정책을 적용합니다.

서명과 본문이 모두 통과하면 중복 반영도 막나요?

유효한 서명과 본문도 다시 도착할 수 있습니다. 업무 반영에는 이벤트 식별자와 멱등 처리, 유효 시간과 재생 제한을 별도로 검수합니다.

웹훅 본문의 인수 범위를 정할 때#

서명 웹훅 본문의 인수 기준을 정리합니다

첫 범위 협의에서 비식별 요청과 기대 거부 결과를 바탕으로 서명 범위·본문 대조의 인수 기준을 정리합니다. 소요 시간은 표본과 프록시 검증 범위를 확인한 뒤 협의합니다.

웹훅 본문 인수표 초안 협의