HMAC 생성

메시지와 키에서 HMAC-SHA-1, SHA-256, SHA-384, SHA-512를 계산하고, 주어진 서명이 어느 것으로 만들어졌는지 판별합니다.

메시지
HMAC-SHA-1
HMAC-SHA-256
HMAC-SHA-384
HMAC-SHA-512

서명을 계산하려면 키를 입력하세요.

이 도구가 하는 일

HMAC은 메시지와 공유 시크릿을 짧은 서명으로 바꿉니다. 같은 시크릿을 가진 사람은 누구나 그것을 다시 계산해 메시지가 바뀌지 않고 도착했는지, 시크릿을 아는 누군가에게서 왔는지 볼 수 있습니다. 이 도구는 흔한 네 변형을 한꺼번에 계산하고 — 받은 서명을 주면 — 어느 것이 그것을 냈는지 알려 줍니다.

그 두 번째 방향이 대개 사람이 찾아오는 이유입니다. Webhook이 검증에 실패하는데, 물음은 사실 "이 서명이 유효한가"가 아니라 "그럴듯한 여러 가지 중 내가 보낸 이와 다르게 하는 것이 무엇인가"입니다. 붙여넣은 서명 앞에 붙은 sha256= 형태의 접두사는 인식되어 제거되므로, 헤더 값을 도착한 그대로 붙여넣어도 됩니다.

어긋나는 곳은 키

HMAC은 바이트를 바이트로 서명합니다. 메시지는 대개 뻔합니다 — 맨 요청 본문 — 하지만 키는 거의 결코 그렇지 않습니다. 시크릿이 문자열로 도착하고, 문자열은 어떻게 읽을지 정하기 전에는 바이트가 아니기 때문입니다:

a3f2   텍스트로   4바이트   61 33 66 32
a3f2   16진수로   2바이트   a3 f2

두 읽기 모두 정당하고, 둘 다 흠 없이 잘 된 서명을 내며, 두 서명에는 공통점이 없습니다. 아무것도 경고하지 않습니다: 오류도, 길이 불평도 없고, 그저 보낸 이가 계산한 것과 맞지 않는 값이 있을 뿐입니다. 그래서 여기서 키 인코딩은 추측이 아니라 보이는 선택입니다 — 서명이 맞지 않을 때 먼저 바꿔 볼 것이 이것입니다.

대략의 기준: whsec_ 같은 접두사나 대소가 섞인 글자와 숫자의 연속을 지닌 시크릿은 보통 텍스트로 의도된 것입니다. 정확히 32자나 64자로 0-9와 a-f만 쓰는 문자열은 대개 16진수입니다. =로 끝나는 것은 거의 확실히 Base64입니다. 다만 모양은 증명이 아니므로, 모양이 아니라 보낸 이의 문서를 확인하세요.

메시지는 정확한 바이트여야 한다

어긋남의 나머지 절반은 메시지입니다. HMAC은 바이트 위에 정의되므로, 바이트를 바꾸는 것은 무엇이든 서명을 완전히 바꿉니다 — 부분 점수도, 아까운 차이도 없습니다.

  • JSON 재직렬화. 본문을 파싱해 다시 문자열화하면 키가 재배열되거나, 간격이 바뀌거나, 숫자가 정규화될 수 있습니다. 받은 맨 본문에 서명하고 검증하세요, 결코 그것의 왕복한 복사본이 아니라.
  • 끝의 줄바꿈. 본문을 파일에 저장할 때 더하는 도구가 있습니다. 그것은 바이트이고, 모든 것을 바꿉니다.
  • 문자 인코딩. 비ASCII 텍스트를 담은 본문은 양쪽이 쓰는 같은 인코딩 — 실제로는 UTF-8 — 으로 읽혀야 합니다.
  • 압축이나 미들웨어. 핸들러 앞의 무언가가 본문을 풀거나 다시 쓴다면, 그 후가 아니라 그 전에 검증하세요.

많은 제공자는 본문만 서명하지도 않습니다. Stripe는 타임스탬프와 본문을 점으로 이어 서명하고, AWS는 메서드, 경로, 헤더, 페이로드의 해시로 만든 정규 요청에 서명합니다. 맨 본문에 대해 검증이 실패하면 서명된 문자열은 아마 맨 본문이 아닙니다 — 그것은 보낸 이 쪽에 문서화되어 있고, 더 디버깅하기 전에 읽을 만합니다.

SHA-1이 해시 도구가 아니라 여기서 제공되는 이유

해시 도구는 SHA-1을 깨진 것으로 표시합니다. 이것은 HMAC-SHA-1을 경고 없이 싣고, 그것은 실수가 아니라 의도입니다.

SHA-1은 서명과 인증서에 쓸 수 없습니다. 충돌을 구성할 수 있기 때문입니다: 서로 다른 두 문서가 다이제스트를 공유하도록 만들 수 있습니다. HMAC은 그 성질에 기대지 않습니다. 그 안전성은 시크릿 키에 있고, 구성 — 메시지를 두 번, 두 번 다 키를 섞어 해시하는 것 — 은 바탕 해시가 충돌에 약해도 버팁니다. HMAC-SHA-1은 지금도 건전하고 OAuth 1.0a와 오래된 AWS 서명이 아직 쓰는 것이라, 그것을 계산하기를 거부하는 도구는 더 안전해지지 않으면서 그저 덜 쓸모 있어질 뿐입니다.

새로운 것에는 무엇이든 SHA-256이 무난한 기본값입니다. 긴 다이제스트가 여기서 유의미하게 더 강하지는 않으므로 — 안전성의 천장은 다이제스트 길이가 아니라 키입니다 — SHA-384와 SHA-512는 상호 운용해야 할 무언가가 그것을 요구할 때만 고를 만합니다.

서명을 안전하게 비교하기

이 페이지는 보통의 문자열 비교로 비교하고, 여기서는 그것으로 괜찮습니다: 당신이 직접 키를 가지고 있어 새어 나갈 것이 없습니다. 들어오는 요청을 검증하는 서버에서는 괜찮지 않습니다. 보통의 비교는 첫 다른 바이트에서 멈추므로, 걸리는 시간이 추측의 얼마가 맞았는지 드러내고, 많은 요청을 보낼 수 있는 공격자가 유효한 서명을 한 바이트씩 복원할 수 있습니다.

플랫폼이 제공하는 상수 시간 비교 — Node의 crypto.timingSafeEqual, Python의 hmac.compare_digest, PHP의 hash_equals — 를 16진수 문자열이 아니라 맨 바이트에 대해 쓰세요. 그와 함께, 요청에 이름 붙은 알고리즘을 믿는 대신 직접 계산한 서명에 대해 비교하고, 오래된 유효한 서명이 재생되지 않도록 타임스탬프가 지금에서 먼 메시지를 거부하세요.

HMAC은 해시도 서명도 아니다

여기서 세 가지가 혼동되고, 그 차이는 나중에 무엇을 주장할 수 있는지에 중요합니다:

  • 해시는 메시지를 받아 다이제스트를 냅니다. 누구나 계산할 수 있으므로 메시지가 우연히 바뀌지 않았음을 증명합니다 — 특정한 누군가에게서 왔음이 아니라.
  • HMAC은 메시지와 공유 시크릿을 받습니다. 양쪽 당사자가 같은 키를 가지므로 보낸 이가 시크릿을 알았음을 증명합니다. 어느 쪽이 보냈는지는 증명할 수 없습니다. 둘 다 낼 수 있었기 때문입니다.
  • 서명은 보낸 이만 가진 개인 키와 누구나 검증할 수 있는 공개 키를 씁니다. 그것이 부인 방지를 줍니다: 보낸 이가 나중에 그것을 부정할 수 없습니다.

그래서 HMAC은 이미 시크릿을 공유하는 두 시스템 사이 — Webhook, 내부 API, 세션 토큰 — 에서 옳은 도구이고, 제삼자에게 누가 무엇을 보냈는지 증명해야 할 때 그른 것입니다. 또 인증은 하지만 감추지는 않음에 유의하세요: 메시지는 평문으로 오가고, HMAC은 기밀성에 대해 아무것도 말하지 않습니다.

Stripe Webhook 서명 검증하기

Stripe는 여기서 붙여넣기로 끝나지 않는 유일한 조리법이고, 대개는 찾아오기 전에 이미 시도해 본 쪽입니다. Stripe-Signature 헤더는 서명이 아니라 쉼표로 나뉜 요소들의 목록이고, Stripe가 서명한 문자열은 요청 본문만이 아닙니다. 헤더를 여러 줄로 끊는 것은 읽기 쉽게 하려는 Stripe 자신의 문서 방식이고, 진짜 헤더는 한 줄로 도착합니다.

Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
  • 메시지: t=의 값, 그다음 점, 그다음 도착한 그대로의 맨 요청 본문. 위 헤더라면 그 문자열은 1492774577. 로 시작하고 본문이 바로 이어집니다. Stripe는 이것을 서명된 페이로드라 부르며, Stripe 서명이 본문만과는 결코 맞지 않는 이유가 여기에 다 있습니다.
  • 키: 그 엔드포인트 하나의 서명 시크릿을 whsec_ 접두사까지 통째로. API 키도 아니고 다른 엔드포인트의 시크릿도 아닙니다 — 같은 URL을 두 번 등록하면 두 개가 됩니다.
  • 키 인코딩: 텍스트. 서명 시크릿은 Stripe가 고른 문자열이고, 무작위로 보여도 16진수도 Base64도 아닙니다. 어느 하나로 읽으면 다른 바이트가 되고 아무것과도 맞지 않는 서명이 나옵니다.
  • 대조할 서명: 헤더 전체가 아니라 v1 요소만. v1=은 그대로 붙여넣어도 됩니다. 그 표지는 sha256=과 똑같이 제거되기 때문입니다 — 하지만 헤더 전체는 안 됩니다. 앞머리의 t=가 표지로 읽히고 그 뒤에 오는 것은 서명이 전혀 아니기 때문입니다.

다른 곳을 파기 전에 이 헤더에 대해 알아 둘 것이 세 가지 더 있습니다. v1이 아닌 스킴은 모두 무시하세요: 테스트 이벤트에 붙는 v0 요소는 의도적으로 진짜 서명이 아니고, 나머지를 무시하는 것이 바로 더 약한 스킴이 강요되는 것을 막습니다. 엔드포인트의 시크릿을 교체하는 동안에는 둘 다 최대 하루 유효하고, 헤더는 시크릿마다 v1 요소를 하나씩 실으며 그중 맞는 것은 하나뿐입니다. 그리고 배달 시도마다 새로 서명되므로 재시도는 첫 시도의 서명을 싣지 않습니다 — 서명되는 문자열 안의 타임스탬프는 오래된 서명을 거부하는 일을 몸짓이 아닌 실제 방어로 만드는 것이기도 합니다. 서명을 깨지 않고 그것을 바꿀 수는 없기 때문입니다.

GitHub Webhook 서명 검증하기

GitHub은 이 도구가 그 둘레에 지어진 붙여넣기이고, 여기서 처음부터 끝까지 확인할 수 있는 유일한 조리법입니다. GitHub이 계산해 둔 예를 공개하기 때문입니다. 서명은 X-Hub-Signature-256 헤더에 표지 sha256=과 그에 이어지는 16진수 다이제스트로 도착하고, 그 표지는 여기서 인식되어 제거되므로 헤더 값은 도착한 그대로 들어갑니다.

X-Hub-Signature-256:
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
  • 메시지: 맨 요청 본문을 한 바이트씩 그대로. GitHub이 공개한 예는 본문 Hello, World! 에 서명하고 그 뒤에는 아무것도 없습니다 — 끝의 줄바꿈도 없습니다.
  • 키: Webhook 설정에 적어 넣은 시크릿을 적은 그대로. 공개된 예는 It's a Secret to Everybody를 씁니다.
  • 키 인코딩: 텍스트. GitHub 시크릿은 당신이 고른 문자열이므로 결코 16진수가 아닙니다 — 16진수 숫자만으로 된 시크릿이어도 적어 넣은 문자로 읽힙니다.
  • 대조할 서명: sha256=까지 포함한 헤더 값 전체. 16진수는 대소문자를 가리지 않고 비교되므로 로그가 어느 쪽으로 찍든 상관없습니다.

이 세 값을 함께 넣는 것이 있는 중에 가장 빠른 온전성 확인입니다: 붙여넣으면 이 페이지가 HMAC-SHA-256의 16진수로 맞았다고 알리고, 그것은 실제 배달에 어느 하나를 시도하기 전에 계산기와 당신의 조리법 읽기가 둘 다 옳다는 것을 알려 줍니다. GitHub은 더 오래된 헤더 X-Hub-Signature도 보냅니다. 같은 본문에 대한 HMAC-SHA-1이고 옛 호환을 위해서만 남아 있는 것입니다. 이 페이지는 네 다이제스트를 한꺼번에 계산하므로 둘 중 어느 헤더에서 온 서명이든 어느 쪽인지 말하지 않아도 식별됩니다. 구할 수 없는 하나는 GitHub이 보낸 바이트가 아닌 무엇으로 도착한 본문입니다 — 페이로드는 ASCII 밖의 문자를 실을 수 있고, 양쪽이 그것을 UTF-8로 읽어야 합니다.

Shopify Webhook 서명 검증하기

Shopify는 다이제스트를 16진수가 아니라 Base64로 적고, 여기서 뜻이 있는 차이는 그것뿐입니다: 같은 32바이트가 등호 하나로 끝나는 44자가 됩니다. 그 문자는 표지가 아니라 채움이므로 앞에서 아무것도 잘리지 않습니다.

X-Shopify-Hmac-SHA256:
dXEH6g6yUJ/CESIczphLijdXC211hsIsRvQ3nIsEPhc=
  • 메시지: 배달된 그대로의 맨 요청 본문을 한 바이트씩. Shopify 자신의 경고는 본문을 파싱하는 미들웨어에 대한 것입니다 — 먼저 검증하고 나중에 파싱하세요. 본문을 이미 객체로 바꾼 파서는 바이트를 버렸습니다.
  • 키: 그 Webhook이 속한 앱의 클라이언트 시크릿. 그 액세스 토큰도 아니고, 같은 화면 옆의 API 키도 아닙니다.
  • 키 인코딩: 텍스트. 다른 둘과 같이 시크릿은 바이트의 인코딩이 아니라 문자열입니다.
  • 대조할 서명: 헤더 값 전체. 그대로 붙여넣으면 되고, 모든 다이제스트에 대해 16진수와 Base64가 둘 다 시도되므로 이 페이지가 돌려주는 인코딩 자체가 보낸 이가 어느 쪽으로 적었는지에 대한 답이 됩니다.

위 칸의 값은 모양을 보이기 위해 거기 있습니다: GitHub 예와 같은 32바이트를 16진수가 아니라 Base64로 적은 것입니다. 나란히 볼 만합니다. 두 헤더 차이의 내용은 그것뿐 — 하나의 다이제스트, 두 가지 적는 법 — 이고, 이 페이지가 무언가 맞았다고만 말하는 대신 알고리즘과 나란히 인코딩을 밝히는 이유도 거기 있습니다.

자주 묻는 질문

서명이 맞지 않습니다. 먼저 무엇을 확인하나요?
키 인코딩, 다음으로 메시지 바이트입니다. 16진수처럼 보이는 시크릿은 흔히 텍스트가 아니라 16진수로 의도되고, 둘은 어느 쪽도 오류 없이 전혀 다른 서명을 냅니다. 그다음, 재직렬화한 복사본이 아니라 받은 그대로의 맨 본문에 서명하는지 확인하세요.
해시 도구가 깨졌다고 하는 SHA-1이 여기서 제공되는 이유는 무엇인가요?
HMAC이 SHA-1이 잃은 성질인 충돌 저항에 기대지 않기 때문입니다. 그 안전성은 키에서 옵니다. HMAC-SHA-1은 지금도 건전하고 OAuth 1.0a와 오래된 AWS 서명에서 아직 쓰이지만, 새로운 것에는 SHA-256이 옳은 기본값입니다.
긴 다이제스트가 더 안전한가요?
유의미하게는 아닙니다. HMAC의 강도는 다이제스트 길이가 아니라 시크릿으로 한정되므로, SHA-512가 SHA-256보다 네 배 낫지는 않습니다. 상대가 기대하는 것을 고르세요.
HMAC과 디지털 서명의 차이는 무엇인가요?
HMAC은 양쪽이 아는 하나의 시크릿을 쓰므로 보낸 이가 시크릿을 알았음은 증명하지만 어느 쪽이 보냈는지는 아닙니다. 디지털 서명은 보낸 이만 가진 개인 키를 쓰므로 제삼자가 검증할 수 있고 보낸 이가 부정할 수 없습니다. 그 마지막 성질이 필요하면 HMAC은 그른 도구입니다.
서명을 ===로 비교해야 하나요?
서버에서는 안 됩니다. 보통의 비교는 바이트가 다르면 곧 돌아오고, 그 타이밍이 추측한 서명의 얼마가 맞았는지 새게 합니다. 플랫폼의 상수 시간 비교를 맨 바이트에 쓰세요. 이 페이지에서는 이미 키를 가지고 있어 문제되지 않습니다.
제 키가 어딘가로 전송되나요?
아니요. 모든 것이 Web Crypto API로 브라우저 안에서 계산됩니다. 메시지도 키도 기기 밖으로 나가지 않습니다.
Stripe Webhook 서명은 어떻게 검증하나요?
헤더의 t= 요소에 있는 타임스탬프, 점, 맨 요청 본문 세 가지를 한 문자열로 이어 붙이고, 엔드포인트의 서명 시크릿을 텍스트로 읽어 그것에 서명한 뒤, 결과를 헤더의 v1 요소와 비교하세요. 위 절이 그것을 항목별로 짚어 갑니다. 거의 모두가 걸려 넘어지는 곳은 첫 단계입니다: 본문만은 Stripe가 서명한 것이 아닙니다.
GitHub Webhook 서명은 어떻게 검증하나요?
맨 요청 본문에 Webhook의 시크릿을 텍스트로 읽어 SHA-256으로 서명하고, 16진수 다이제스트를 X-Hub-Signature-256 헤더와 비교하세요. 여기에 붙여넣을 때 표지 sha256=은 그대로 두어도 됩니다. GitHub은 계산해 둔 예 — 본문 Hello, World! 와 시크릿 It's a Secret to Everybody — 를 공개하고 있고, 실제 배달에 시도하기 전에 조리법 읽기를 확인하는 가장 빠른 길입니다.
Shopify Webhook 서명은 어떻게 검증하나요?
맨 요청 본문에 그 Webhook이 속한 앱의 클라이언트 시크릿을 텍스트로 읽어 SHA-256으로 서명하고, Base64 다이제스트를 X-Shopify-Hmac-SHA256 헤더와 비교하세요. 헤더 값 전체를 그대로 붙여넣으면 됩니다 — 끝의 등호는 Base64 채움입니다. 맞지 않을 때 흔한 원인은 핸들러가 보기 전에 본문을 파싱해 버린 미들웨어입니다.
다른 제공자의 Webhook 서명은 어떻게 검증하나요?
네 가지 물음이 결판을 내고, 보낸 이의 문서에 넷 다 있습니다: 어느 헤더가 서명을 싣는지, 실제로 서명되는 문자열이 무엇인지, 시크릿을 어떻게 읽어야 하는지, 다이제스트가 16진수인지 Base64인지. 그것을 위 칸에 채우세요. 그래도 맞지 않으면 답은 거의 언제나 두 번째입니다 — 아주 많은 보낸 이가 본문만이 아니라 무언가를 이어 붙인 본문에 서명합니다.

관련 도구