Omise (ปัจจุบันคือ Opn Payments) หนึ่งในผู้ให้บริการชำระเงินที่ใช้กันแพร่หลายในไทยและเอเชียตะวันออกเฉียงใต้ เซ็นชื่อการแจ้งเตือน webhook ทุกครั้งด้วย HMAC-SHA256: ร้านค้าจะได้รับคีย์ลับตอนตั้งค่าการเชื่อมต่อ และการแจ้งเตือนที่เข้ามาแต่ละครั้งจะมาพร้อมลายเซ็นที่ต้องคำนวณใหม่ในฝั่งเซิร์ฟเวอร์แล้วเทียบกับค่าที่ได้รับ หากลายเซ็นไม่ตรงกัน แสดงว่าการแจ้งเตือนนั้นไม่ได้มาจากผู้ให้บริการชำระเงินจริง ๆ และไม่ควรถูกประมวลผลเป็นการยืนยันการชำระเงินจริง
เหตุใดจึงต้องใช้คีย์ลับ ไม่ใช่แค่แฮชธรรมดา
แฮชธรรมดาอย่าง SHA-256 พิสูจน์ได้เพียงว่าข้อมูลไม่เสียหายระหว่างการส่ง แต่ไม่ได้พิสูจน์ว่าใครเป็นผู้ส่ง หากเซิร์ฟเวอร์เพียงแค่แฮชเนื้อหาของ webhook แล้วเทียบกับแฮชที่แนบมาในคำขอ ผู้โจมตีก็สามารถปลอมแปลงเพย์โหลดของตัวเองพร้อมแนบแฮชที่ตรงกันได้ HMAC แก้ปัญหานี้โดยผสมคีย์ลับที่เฉพาะผู้ส่งและผู้รับเท่านั้นที่รู้เข้าไปด้วย — หากไม่มีคีย์นี้ การสร้างลายเซ็นที่ถูกต้องแทบเป็นไปไม่ได้ แม้ผู้โจมตีจะรู้แน่ชัดว่าใช้ฟังก์ชันแฮชตัวใด
เหตุใดสิ่งนี้ไม่ใช่แค่ "แฮชของคีย์บวกข้อความ"
การต่อคีย์และข้อความเข้าด้วยกันแบบง่าย ๆ ก่อนการแฮช (เช่น hash(คีย์ + ข้อความ)) มีช่องโหว่ต่อการโจมตีแบบ length extension ในฟังก์ชันแฮชบางตัว — บางครั้งผู้โจมตีสามารถต่อข้อมูลเพิ่มและคำนวณแฮชใหม่ที่ถูกต้องได้โดยไม่ต้องรู้คีย์ทั้งหมด HMAC ใช้โครงสร้างเฉพาะที่มีการแฮชสองชั้นและการเติมข้อมูลภายใน/ภายนอก ซึ่งปิดช่องโหว่ประเภทนี้ได้อย่างสมบูรณ์
ที่อื่นที่ HMAC ปรากฏ
นอกจากการเซ็นชื่อ webhook แล้ว HMAC ยังใช้ยืนยันตัวตนคำขอ API และเป็นแกนหลักของอัลกอริทึม TOTP ซึ่งเป็นอัลกอริทึมเดียวกับที่สร้างรหัสหกหลักในแอปยืนยันตัวตน
ทำไมต้องใช้สิ่งนี้
- ตรวจสอบความแท้จริงของ webhook จากผู้ให้บริการชำระเงินหรือบริการภายนอกอื่น ๆ
- เซ็นชื่อคำขอ API โดยไม่ต้องส่งคีย์ลับเองผ่านเครือข่ายเลย
- เข้าใจกลไกภายในของรหัส TOTP และโทเค็นเซสชัน
การเปรียบเทียบแบบเวลาคงที่
การเปรียบเทียบลายเซ็นที่ได้รับกับค่าที่คาดไว้ด้วยตัวดำเนินการ === ธรรมดานั้นไม่ปลอดภัย เพราะตัวดำเนินการนี้มักจะหยุดทำงานทันทีที่พบไบต์แรกที่ไม่ตรงกัน และเวลาที่ใช้ในการเปรียบเทียบจะเผยให้ผู้โจมตีรู้ว่าเขาเดาไบต์แรก ๆ ถูกกี่ไบต์ ตัวจัดการ webhook ที่สร้างมาอย่างถูกต้องจะใช้ฟังก์ชันเปรียบเทียบแบบเวลาคงที่เสมอ เช่น hash_equals ใน PHP หรือ crypto.timingSafeEqual ใน Node.js ซึ่งจะตรวจสอบทุกไบต์เสมอไม่ว่าความไม่ตรงกันแรกจะอยู่ตรงไหน