วิธีใช้งาน
- เลือก “อัลกอริทึม” โดยค่าเริ่มต้นคือ HMAC-SHA256 ซึ่งเป็นอัลกอริทึมที่เว็บฮุกและ API ส่วนใหญ่ใช้
- ป้อน “คีย์ลับ” และตั้ง “รูปแบบคีย์” เป็น “ข้อความ” (อ่านเป็น UTF-8) “เลขฐานสิบหก” หรือ “Base64” ให้ใช้เลขฐานสิบหกหรือ Base64 เมื่อคีย์เป็นไบต์ดิบ ไม่ใช่สตริงที่มีลักษณะคล้ายรหัสผ่าน
- พิมพ์หรือวาง “ข้อความ” ที่ต้องการ ตั้งรูปแบบอินพุตเป็น “เลขฐานสิบหก” สำหรับไบต์ดิบ หรือคลิก “เปิดไฟล์” ค่า HMAC จะอัปเดตทันทีขณะที่คุณพิมพ์
- เลือก “รูปแบบเอาต์พุต” เป็น “hex (ตัวพิมพ์เล็ก)” “HEX (ตัวพิมพ์ใหญ่)” หรือ “Base64” แล้วคัดลอกผลลัพธ์ หรือวางลายเซ็นที่ได้รับลงในช่อง “เปรียบเทียบกับ” เพื่อดูว่า “ตรงกัน” หรือ “ไม่ตรงกัน”
HMAC ทำงานอย่างไร
HMAC (hash-based message authentication code, RFC 2104) คือรหัสยืนยันความถูกต้องของข้อความที่ใช้ฟังก์ชันแฮชสำหรับงานเข้ารหัสลับร่วมกับคีย์ลับ มีเพียงผู้ที่รู้คีย์เท่านั้นที่สร้างค่าที่ถูกต้องสำหรับข้อความหนึ่ง ๆ ได้ HMAC ที่ตรงกันจึงพิสูจน์สองสิ่งพร้อมกัน คือข้อความไม่ถูกแก้ไข และข้อความมาจากผู้ที่ถือคีย์นั้น
โครงสร้างของ HMAC คือ H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ message)) หากคีย์ยาวกว่าขนาดบล็อกของฟังก์ชันแฮช (64 ไบต์สำหรับ MD5, SHA-1 และ SHA-256 และ 128 ไบต์สำหรับ SHA-384 และ SHA-512) คีย์จะถูกแฮชก่อน จากนั้นเติมศูนย์จนครบขนาดบล็อก แล้วนำไป XOR กับ ipad (ไบต์ 0x36) เพื่อคำนวณแฮชชั้นในจากข้อความ และ XOR กับ opad (ไบต์ 0x5c) เพื่อคำนวณแฮชชั้นนอกจากผลลัพธ์ของชั้นใน วิธีแบบง่าย ๆ อย่าง SHA-256(key ‖ message) ไม่มีแฮชชั้นนอก ผู้ที่เห็นแท็กเพียงค่าเดียวจึงต่อข้อมูลเพิ่มท้ายข้อความและคำนวณแท็กใหม่ที่ถูกต้องได้โดยไม่ต้องรู้คีย์ (การโจมตีแบบขยายความยาว หรือ length extension)
เอาต์พุตมีความยาวเท่ากับฟังก์ชันแฮชที่ใช้ เช่น 32 ไบต์หรือเลขฐานสิบหก 64 อักขระสำหรับ HMAC-SHA256 RFC 2104 แนะนำให้ใช้คีย์ที่ยาวอย่างน้อยเท่านั้น และตัวสร้าง Base64 แบบสุ่มก็สร้างคีย์ที่เหมาะสมให้ได้
ลายเซ็น HMAC ใช้ที่ใดบ้าง
- เว็บฮุกของ GitHub ส่วนหัว
X-Hub-Signature-256คือsha256=ตามด้วย HMAC-SHA256 ในรูปเลขฐานสิบหกของเนื้อหาคำขอดิบ โดยใช้คีย์ลับของเว็บฮุกเป็นคีย์ คุณวางค่าส่วนหัวทั้งหมดลงในช่อง “เปรียบเทียบกับ” ได้เลย เพราะตัวนำหน้าsha256=จะถูกละเว้น - เว็บฮุกของ Stripe ส่วนหัว
Stripe-Signatureมีเวลาประทับtและลายเซ็นv1ซึ่งคือ HMAC-SHA256 ในรูปเลขฐานสิบหกของเวลาประทับ จุด และเนื้อหาดิบที่ต่อกัน โดยใช้คีย์ลับของปลายทาง (whsec_...) เป็นคีย์แบบข้อความ - เว็บฮุกของ Shopify
X-Shopify-Hmac-Sha256เก็บ HMAC-SHA256 ของเนื้อหาดิบในรูป Base64 โดยใช้คีย์ลับของไคลเอนต์ (client secret) ของแอปเป็นคีย์ ดังนั้นให้เลือกเอาต์พุตเป็น Base64 หรือวางค่าส่วนหัวลงในช่อง “เปรียบเทียบกับ” - AWS Signature Version 4 คีย์สำหรับลงนามได้มาจากการคำนวณ HMAC-SHA256 ต่อกันเป็นทอด ๆ เริ่มจากวันที่ โดยใช้
AWS4ต่อด้วยคีย์การเข้าถึงลับ (secret access key) เป็นคีย์ จากนั้นจึงเป็นภูมิภาค บริการ และaws4_requestซึ่งแต่ละขั้นใช้ผลลัพธ์ของขั้นก่อนหน้าเป็นคีย์ ให้ตั้ง “รูปแบบคีย์” เป็น “เลขฐานสิบหก” เพื่อนำเอาต์พุตของขั้นหนึ่งไปใช้ในขั้นถัดไป - JSON Web Token HS256, HS384 และ HS512 คือ HMAC-SHA256, -384 และ -512 ที่คำนวณจากส่วนหัวที่เข้ารหัสแล้ว จุด และเพย์โหลดที่เข้ารหัสแล้ว ส่วนที่สามของโทเค็นคือ HMAC นั้นในรูป Base64 แบบปลอดภัยสำหรับ URL ซึ่งช่อง “เปรียบเทียบกับ” รับได้ตามที่เป็นอยู่ ส่วนตัวถอดรหัส JWTใช้ตรวจสอบโทเค็นทั้งชุดได้
ให้คำนวณ HMAC จากเนื้อหาดิบตรงตามที่ได้รับมาเสมอ การแยกวิเคราะห์ JSON แล้วแปลงกลับเป็นสตริงใหม่จะเปลี่ยนการเว้นวรรคหรือลำดับของคีย์ และแม้แต่อักขระขึ้นบรรทัดใหม่ที่เกินมาเพียงตัวเดียวก็ทำให้ได้ HMAC ที่ต่างไปโดยสิ้นเชิง
HMAC ในโค้ด
# Python
import hmac, hashlib
sig = hmac.new(b'key', b'message', hashlib.sha256).hexdigest()
hmac.compare_digest(sig, received) # เปรียบเทียบแบบใช้เวลาคงที่
// Node.js
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', 'key').update('message').digest('hex');
// timingSafeEqual จะโยนข้อผิดพลาดหากความยาวไม่เท่ากัน จึงควรตรวจสอบความยาวก่อน
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(received));
// PHP: ข้อความมาก่อนคีย์
$sig = hash_hmac('sha256', 'message', 'key');
hash_equals($sig, $received);
# OpenSSL: คีย์แบบข้อความ ตามด้วยคีย์เดียวกันในรูปเลขฐานสิบหก
printf '%s' 'message' | openssl dgst -sha256 -hmac 'key'
printf '%s' 'message' | openssl dgst -sha256 -mac HMAC -macopt hexkey:6b6579
# ค่าทดสอบ: คีย์ "key" ข้อความ "The quick brown fox jumps over the lazy dog"
# HMAC-SHA256 = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
เมื่อตรวจสอบลายเซ็นในโค้ดของคุณเอง ให้ใช้การเปรียบเทียบแบบใช้เวลาคงที่ (constant-time) อย่างในตัวอย่างข้างต้น การเปรียบเทียบด้วย == ธรรมดาจะหยุดทันทีที่พบอักขระตัวแรกที่ไม่ตรงกัน และความต่างของเวลานั้นอาจทำให้ผู้โจมตีหาลายเซ็นที่ถูกต้องได้ทีละไบต์ สำหรับรูปแบบที่มีเวลาประทับอย่างของ Stripe ให้ปฏิเสธคำขอที่มีเวลาประทับเก่ากว่าสองสามนาทีด้วย เพื่อป้องกันการส่งคำขอซ้ำ (replay)
คำถามที่พบบ่อย
HMAC-MD5 หรือ HMAC-SHA1 ยังปลอดภัยอยู่หรือไม่
การโจมตีแบบชนกัน (collision) ทำให้ MD5 และ SHA-1 ไม่ปลอดภัยเมื่อใช้เป็นแฮชธรรมดา แต่ไม่ได้ให้วิธีที่ใช้ได้จริงในการปลอมแปลง HMAC เพราะความปลอดภัยของ HMAC ขึ้นอยู่กับคุณสมบัติอื่นของฟังก์ชันแฮชและคีย์ลับ (RFC 6151 อภิปรายเรื่อง HMAC-MD5 ไว้) ระบบที่กำหนดให้ใช้อัลกอริทึมเหล่านี้ เช่น รหัส TOTP ที่อิง HMAC-SHA1 จึงยังใช้ต่อไปได้ แต่สำหรับการออกแบบใหม่ ให้ใช้ HMAC-SHA256
ทำไม HMAC ของฉันจึงไม่ตรงกับลายเซ็นของเว็บฮุก
สาเหตุที่พบบ่อยคือการแฮช JSON ที่แยกวิเคราะห์แล้วแปลงกลับเป็นสตริงใหม่แทนที่จะใช้เนื้อหาดิบ มีการขึ้นบรรทัดใหม่ต่อท้ายข้อความ ใช้คีย์ลับหรือรูปแบบคีย์ผิด (คีย์ลับที่เป็น Base64 หรือเลขฐานสิบหกแต่ป้อนเป็น “ข้อความ” หรือกลับกัน) และใช้อัลกอริทึมผิด ส่วนเรื่องเลขฐานสิบหกกับ Base64 ไม่ใช่ปัญหา เพราะช่อง “เปรียบเทียบกับ” รับได้ทั้งเลขฐานสิบหกตัวพิมพ์เล็กหรือใหญ่ Base64 มาตรฐาน และ Base64 แบบปลอดภัยสำหรับ URL
HMAC ต่างจากแฮชธรรมดาอย่างไร
HMAC เหมือนกับลายเซ็นดิจิทัลหรือไม่
ไม่เหมือน ทั้งสองฝ่ายของ HMAC ใช้คีย์ลับเดียวกัน ใครก็ตามที่ตรวจสอบแท็กได้จึงสร้างแท็กได้เช่นกัน ส่วนลายเซ็นอย่าง RSA หรือ ECDSA (RS256 หรือ ES256 ใน JWT) สร้างด้วยคีย์ส่วนตัวและตรวจสอบด้วยคีย์สาธารณะ ผู้ที่ตรวจสอบลายเซ็นจึงปลอมแปลงลายเซ็นไม่ได้
คีย์ลับของฉันถูกส่งไปยังเซิร์ฟเวอร์หรือไม่
ไม่ HMAC-SHA1 และ HMAC ตระกูล SHA-2 คำนวณด้วย Web Crypto API ของเบราว์เซอร์ ส่วน HMAC-MD5 คำนวณด้วย JavaScript ในหน้านี้ คีย์และข้อความจะไม่ออกจากอุปกรณ์ของคุณ ถึงอย่างนั้นก็ควรจัดการคีย์ลับของระบบจริง (production) อย่างระมัดระวัง และเปลี่ยนคีย์ใหม่หากคีย์อาจถูกเปิดเผย