การส่งต่อ Raw Body และลายเซ็น x-line-signature
ภาพรวม
หัวข้อนี้เป็นจุดที่มักเข้าใจผิด: webhook-go ไม่ได้ verify ลายเซ็น x-line-signature ด้วยตัวเอง
จากการตรวจสอบโค้ดทั้ง internal/ และ cmd/ ไม่พบการทำ HMAC-SHA256 เทียบกับ channel secret
ที่ใดเลยในโปรเจกต์นี้ สิ่งที่พบคือ internal/util/hash.go (ฟังก์ชัน HMACShort
ที่ใช้สร้าง short hash ทั่วไป) และ internal/util/base64.go (ฟังก์ชัน fileSignature
ที่อ่าน magic bytes ของไฟล์) ซึ่งไม่เกี่ยวข้องกับการตรวจลายเซ็นของ LINE
สิ่งที่ service นี้ทำแทนคือ รักษา bytes ดิบของ request body ไว้โดยไม่แตะต้อง เพื่อให้ปลายทาง ไม่ว่าจะเป็น endpoint ของลูกค้าที่รับ forward หรือ worker-go ที่ consume คิว สามารถ verify ลายเซ็นได้เอง จึงเป็นการ ย้ายความรับผิดชอบ ไม่ใช่การละเลย
เหตุผลทางเทคนิคคือ x-line-signature เป็น HMAC-SHA256 ของ body ดิบทั้งก้อน
หากนำมา json.Unmarshal แล้ว json.Marshal กลับ Go จะเรียง key ของ map ตามลำดับตัวอักษร
ทำให้ bytes เปลี่ยนและลายเซ็นใช้ไม่ได้ทันที แม้เนื้อหา JSON จะเหมือนเดิมทุกประการ
Business Flow
processLineอ่าน body ด้วยio.ReadAll(c.Request.Body)ได้ผลลัพธ์เป็นraw []byte- สำเนา bytes เก็บไว้ใน
BodyRawชนิดjson.RawMessageโดยเป็นการ copy จริง ไม่ใช่การอ้างถึง buffer ที่ gin จะนำกลับไปใช้ซ้ำ - parse
rawเป็นmap[string]interface{}เก็บไว้ในฟิลด์Bodyซึ่งใช้เฉพาะ logic routing ภายในเท่านั้น (อ่านevents[].type,source.userId,message.text,postback.data) และไม่เคยถูกนำไป serialize ต่อ - เมื่อ publish ลงคิว ตัว
LineWebhookPayload.MarshalJSONถูก override ให้ปล่อยBodyRawออกไปตรง ๆ ในฟิลด์bodyและจะ fallback ไป marshal จาก mapBodyก็ต่อเมื่อBodyRawว่าง คือกรณีที่ body ไม่ใช่ JSON object - เมื่อ forward ต่อผ่าน HTTP ระบบจะส่ง
BodyRawเป็น request body พร้อม copy headerx-line-signatureไปด้วย ทำให้ปลายทางเทียบลายเซ็นกับ body ที่ได้รับได้พอดี - header ทุกตัวถูกแปลง key เป็นตัวพิมพ์เล็กตั้งแต่ชั้น handler เพราะ Go จะ canonicalize
เป็น
X-Line-Signatureขณะที่ระบบปลายทางซึ่ง port มาจาก Express จะ lookup ด้วย คีย์x-line-signature
ผลกระทบด้านความปลอดภัยที่ควรบันทึกไว้
- endpoint
POST /api/line/:idเปิดสาธารณะและไม่มี auth หากผู้อื่นทราบwebhookIdก็สามารถยิง payload ปลอมเข้ามาได้ ด่านป้องกันที่มีอยู่จริงคือ rate limit ต่อ IP และการที่webhookIdเดาได้ยาก - การ verify จริงต้องเกิดที่ worker-go ในฐานะ consumer ของคิว
line_webhookหาก worker ไม่ verify ก็เท่ากับทั้ง pipeline ไม่มีการ verify - payload ที่ publish ลงคิวแนบ
headersครบทุกตัวรวมถึงลายเซ็น จึงมีข้อมูลเพียงพอ ให้ปลายทาง verify ได้
ไฟล์และฟังก์ชันหลัก
| ไฟล์ | จุดที่เกี่ยวข้อง |
|---|---|
internal/line/handler.go | Handler.processLine ทำ io.ReadAll แล้ว copy เป็น bodyRaw และ toLowerHeader |
internal/line/service.go | LineWebhookPayload.MarshalJSON, webhookPayload.MarshalJSON และ marshalWebhookBody ซึ่งเป็นตัวกลางที่เลือกใช้ raw ก่อน map |
internal/line/service.go | Service.forwardWebhook ส่ง raw พร้อมเซ็ต header ลายเซ็นกลับเข้าไปใน request |
internal/line/rawbody_test.go | test ที่ยืนยันว่า body ที่ออกไปเป็น bytes เดิมทุกประการ |
จุดเชื่อมต่อกับ Service อื่น
- worker-go — เป็นผู้รับผิดชอบการ verify จริง เนื่องจากได้รับทั้ง
headersและ body ดิบ - endpoint ของลูกค้า (
forwardWebhookUrl) — verify ได้เองเพราะได้รับทั้ง raw body และ header ลายเซ็น (ดู ส่งต่อ Webhook ไปยังระบบลูกค้า) - ข้อควรระวังเวลาแก้โค้ด: ห้ามแก้ไข map
Bodyแล้ว re-marshal ในเส้นทาง publish หรือ forward เพราะจะทำลาย parity นี้ทันที โดยในโค้ดมี comment เตือนไว้ทั้ง 3 จุด - จุดที่ parity ยังไม่สมบูรณ์: ใน
mbox_handoffบาง branch ได้แก่exitและmbox_teamส่งwebhookPayloadโดยไม่ได้เซ็ตBodyRawทำให้ body ที่ออกไปถูก marshal ใหม่จาก map และมีการเรียง key ใหม่ ซึ่งต่างจาก branchhandoffและdepartment_pickerที่ส่ง raw (ดู ส่งต่อให้เจ้าหน้าที่)