Private Docs

Body Signature — แผน Implement รอบ POC

แผนลงมือจริงรอบแรก (Backend 2 repo) — blocker ที่เจอในโค้ด, wire contract ที่ freeze แล้ว, ไฟล์ที่จะแตะ, ลำดับงาน และสิ่งที่ยังต้องตัดสิน

อัปเดต: 2026-07-31

Body Signature — แผน Implement รอบ POC

คู่กับ ภาพระบบหลัง Implement — เล่มนั้นคือ “จะเป็นแบบไหน” เล่มนี้คือ “จะทำยังไง รอบนี้ทำแค่ไหน”

สถานะ ณ 31/07/2026: ยังไม่มีโค้ดถูกแก้แม้แต่บรรทัดเดียว — เอกสารนี้คือแผนที่ผ่านการตรวจโค้ดจริง 5 repo, ผ่าน security review, ผ่านการท้าทายจาก skeptic และ ผ่านการพิสูจน์ด้วยเบราว์เซอร์จริง + .NET จริงแล้ว (ข้อ 8)

1. รอบนี้ได้อะไร — และยังไม่ได้อะไร

ได้ (Backend 2 repo)

  • SupApp_util_lib มีความสามารถตรวจ ลายเซ็นแบบกุญแจคู่ต่อผู้ใช้ เพิ่มเข้ามา โดยของเดิม (HMAC ที่ ~13 service ใช้อยู่) ไม่ถูกแตะแม้แต่ byte เดียว
  • Codex มี endpoint รับลงทะเบียน public key แบบ TOFU (คนแรกชนะ) และมี endpoint นำร่องที่ บังคับลายเซ็นจริง — ไม่มีลายเซ็น/ลายเซ็นผิด/เวลาเก่า = 401
  • ครอบ GET ด้วย ตามมติ — GET ที่ไม่มีลายเซ็นถูกปฏิเสธจริง ไม่ถูกกฎ “ข้าม GET” ของเดิมเล็ดลอด
  • มีชุดทดสอบที่พิสูจน์ทั้งเคสผ่านและเคสที่ต้องพัง รวม edge ของการประกอบข้อความ (query ภาษาไทย, ช่องว่าง, +, BOM, ลายเซ็นผิดรูปแบบ)

ยังไม่ได้ (เลื่อนไปรอบหน้าโดยตั้งใจ)

  • ฝั่ง Angular ทั้งหมด — interceptor ใน @exim/auth-sdk + การต่อเข้า shell + การทดสอบผ่าน browser จริง
  • หลักฐานว่า 2 ฝั่งเซ็น/ตรวจตรงกันจริงปิดช่องนี้ไปแล้ว 31/07 ด้วย spike ก่อนเริ่มเขียนโค้ด (ดูข้อ 8)
  • Sentinel / APIM / CSP / field encryption / Sonar-coverage — อยู่นอกรอบนี้ทั้งหมด

2. 4 เรื่องที่เจอในโค้ดจริง และเกือบทำให้แผนเดิมพัง

ทั้ง 4 ข้อไม่ได้มาจากการเดา — ตรวจจากไฟล์จริงทั้งหมด

#เจออะไรถ้าไม่รู้ก่อนจะเกิดอะไรปิดยังไง
1SecurityFilterExtensions.cs:85 สร้าง HmacSignatureService ทันทีที่ register และ HmacSignatureService.cs:24-25 โยน exception เมื่อไม่มี secret · Codex ไม่มี SecretKey ทั้งใน appsettings และ IaCCodex boot ไม่ขึ้นเลย ทันทีที่เปิด signature ตามแผนเดิมแยกเป็นคลาสใหม่ที่ไม่พึ่ง HMAC เลย (ดูข้อ 4)
2ตัวกรองลายเซ็นไม่เคยถูกต่อเข้า pipeline — ApiExtensions.cs:87-88 มีแค่ ApiKey + Idempotencyติด attribute ไปก็ไม่มีผลอะไร เข้าใจผิดว่าปลอดภัยแล้วทั้งที่ไม่ตรวจอะไรเลยใช้ [ServiceFilter] ระบุตัวกรองตรงๆ ที่ controller นำร่อง
3ตัวกรองเดิมเช็ค “ข้าม GET” ก่อนอ่าน header (SignatureValidationFilter.cs:50)มติ “ครอบ GET” ถูกเลี่ยงได้ด้วยการไม่ส่ง header เลย → 200 · และชุดทดสอบยังผ่านแบบหลอกๆคลาสใหม่ไม่มีกลไก “ข้าม GET” อยู่เลย ปิดโดยโครงสร้าง
4X-Key-Id ไม่อยู่ใน Cors:AllowedHeaders (appsettings.{Development,SIT,Uat}.json:31-37 และ IaC :51) และ CORS ใช้ allowlist ไม่ใช่ allow-anypreflight ตก → browser ไม่เคยส่ง signed request สำเร็จสักตัว และการเติม header นี้จะลาก IaC ทั้ง 3 env เข้ามาด้วยตัด X-Key-Id ออกจากรอบนี้ — เหลือ 2 header ที่อยู่ใน allowlist อยู่แล้ว

3. ข้อความที่เอาไปเซ็น (freeze แล้ว ห้ามตีความเอง)

v1\n{ts}\n{METHOD}\n{path}\n{query}\n{base64(sha256(body))}

ทุกช่องคั่นด้วยขึ้นบรรทัดใหม่ ไม่ใช่จุด — เพราะจุดเป็นอักขระที่อยู่ใน path และ body ได้ ทำให้ /a + .x กับ /a. + x กลายเป็นข้อความเดียวกัน (security review จับได้)

ส่วนกติกา
tsUnix seconds · ต่างจากเวลาเซิร์ฟเวอร์เกิน 300 วิ = ปฏิเสธ
METHODตัวพิมพ์ใหญ่
pathเริ่มที่ /api/codex-service/... · ตัด / ท้าย ยกเว้น root · มี % หรือ .. = ปฏิเสธ
queryแตกออก → ถอดรหัส → เรียงตาม (ชื่อ, ค่า) → เข้ารหัสใหม่ด้วยชุดอักขระ A-Za-z0-9-._~ เท่านั้น (ช่องว่าง = %20, บวกจริง = %2B) → ต่อด้วย & · ไม่มี query = ค่าว่าง · ห้ามยุบคีย์ซ้ำ
bodyhash จาก bytes ดิบ อ่านจาก stream เอง — ห้ามใช้ body ที่ middleware แคชไว้ (มันตัด BOM ทิ้ง = ลายเซ็นเพี้ยนเป็นครั้งคราวแบบหาสาเหตุยากมาก) · GET = hash ของค่าว่าง
ลายเซ็นECDSA P-256 + SHA-256 · รูปแบบ raw r‖s 64 bytes เป๊ะ (ไม่ใช่ DER) · ส่งเป็น base64 ปกติ
การหา keyจาก oid ใน JWT ที่ผ่านการตรวจแล้ว เท่านั้น · หาไม่เจอ = 401 และ ห้ามถอยไปใช้ HMAC
การลงทะเบียนเขียน Redis แบบ atomic (เขียนได้เฉพาะเมื่อยังว่าง) · ส่ง JWK เดิมซ้ำ = สำเร็จเงียบๆ (กันกดรีเฟรชแล้วเด้ง) · ส่ง JWK ใหม่ทับ = 409 + log เตือน

4. จะเพิ่มอะไรที่ไหน

Backend_Package — เพิ่มของใหม่ล้วน 4 ไฟล์ ไม่แก้ตัวเก่าเลย

ไฟล์ทำอะไร
Abstractions/Security/ISignatureKeyResolver.csช่องเสียบ “หา public key ของ user นี้จากไหน” (Codex เป็นคนตอบ)
Infrastructure/Security/EcdsaClientSignatureVerifier.csประกอบข้อความตามข้อ 3 + ตรวจลายเซ็น
Filters/ClientSignatureValidationFilter.csคลาสใหม่ทั้งตัว — ไม่มีกลไกข้าม GET, ไม่พึ่ง HMAC
Extension/ClientSignatureExtensions.csตัวลงทะเบียนเข้า DI

การแยกเป็นคลาสใหม่ปิด blocker ข้อ 1 และ 3 พร้อมกัน และทำให้พิสูจน์ “ของเดิมไม่ถูกแตะ” ได้ด้วยคำสั่งเดียว: git diff ของไฟล์เดิม = ว่างเปล่า

Backend_CodexService — endpoint ใหม่บน controller ใหม่

ไฟล์ทำอะไร
Codex04.Domain/Ports/Security/IClientKeyStore.csสัญญาของที่เก็บ key (ตามกฎ interface อยู่ Domain)
Codex02.Infrastructure/Security/RedisClientKeyStore.csเก็บ/อ่าน public key ใน Redis + เป็นตัวตอบให้ Package · key ตั้ง TTL เสมอ
Codex03.Application/Features/ClientKeys/...Command + Validator + Handler (3 ไฟล์ ตามโครงเดิมของ repo)
Codex01.API/Controllers/v1/ClientKeysController.csPOST client-keys (ลงทะเบียน) · GET client-keys/ping และ POST client-keys/echo (บังคับลายเซ็น)
DependencyInjection.cs + Codex04.Domain.csproj + appsettings.jsonต่อสาย + อัปเวอร์ชัน lib + ค่าคอนฟิกที่ไฟล์ base

:::details ทำไม endpoint นำร่องเป็นของใหม่ ไม่ไปแขวนกับ endpoint เดิม endpoint เดิมของ Codex (Mdm*, AppManagement, CmsConfig) ถูกเรียกจาก Admin Portal ซึ่งยังไม่เซ็นอะไรเลย ถ้าไปบังคับลายเซ็นตรงนั้น = ระบบพังทันทีบน dev · endpoint นำร่องที่ไม่มีใครเรียกอยู่ ทำให้ “เปิดใช้จริง” ได้โดยไม่มีผลกระทบ และยังพิสูจน์ทุกเคสได้ครบ

ผลข้างเคียงที่ต้องรู้: appsettings.{ENV}.json จะไม่ถูกแตะเลย (คอนฟิกลงที่ไฟล์ base) ซึ่งแปลว่า IaC ไม่ต้องแก้ และด่าน config-parity ใน CI ไม่ถูกกระตุ้น :::

5. ลำดับงาน

  1. ประตูด่านแรก — spike WebCryptoผ่านแล้ว 31/07 (ข้อ 8) — และได้ลายเซ็นจริงจากเบราว์เซอร์มาใช้เป็นชุดทดสอบตายตัวของฝั่ง .NET ด้วย
  2. เขียนชุดทดสอบที่ยึดพฤติกรรมปัจจุบันของ HMAC ไว้ก่อน แล้วรันให้เขียว — เป็นตาข่ายจับว่าของเดิมไม่พัง
  3. Backend_Package เขียนของใหม่ 4 ไฟล์ → รันเทสต์ทั้ง repo
  4. dotnet packตรวจว่าไฟล์ในแพ็กเกจมีของใหม่จริง → publish ขึ้น feed ด้วย feed token (เคยเจอเคส pack ติด build เก่ามาแล้ว และ feed ดันทับเวอร์ชันเดิมไม่ได้ ผิดแล้วต้องขึ้นเลขใหม่)
  5. Backend_CodexService เขียน + อัปเวอร์ชัน lib → build + รันเทสต์เดิมทั้งหมดให้ยังเขียว
  6. ยิง HTTP จริงใส่ Codex ที่รันอยู่ 7 เคส (ลงทะเบียน / ping ลายเซ็นถูก / echo ลายเซ็นถูก / ไม่มี header / body เพี้ยน / ไม่เคยลงทะเบียน / ลงทะเบียนซ้ำด้วย key ใหม่)
  7. review อิสระ + ตรวจว่าไฟล์ต้องห้ามไม่ถูกแตะ

6. เกณฑ์ว่าสำเร็จ

:::details 14 เกณฑ์ + ชุดทดสอบเต็ม (กางดูเมื่อจะตรวจรับ) เคสที่ต้องผ่าน: ลายเซ็นถูก → 200 ทั้ง GET และ POST · ข้อความที่เซ็นตรงสเปกแบบ byte-for-byte

เคสที่ต้องถูกปฏิเสธ: ไม่มี header → 401 “Missing Signature” · เวลาเก่ากว่า 300 วิ → 401 “Expired Request” (แม้ลายเซ็นถูก) · GET ไม่มีลายเซ็น → 401 · body หรือ query เพี้ยน 1 byte → 401 “Invalid Signature” · ไม่เคยลงทะเบียน key → 401 และพิสูจน์ด้วย spy ว่าไม่มีการเรียก HMAC · ไม่มีตัวหา key ใน DI → 401 ไม่ใช่ crash

ขอบที่ต้องทดสอบ: query ภาษาไทย · ช่องว่าง (ต้องเป็น %20) · เครื่องหมายบวกจริง (ต้องเป็น %2B) · คีย์ซ้ำ ?a=1&a=2 (ห้ามยุบ) · ไม่มี query · body มี BOM (ดักว่าอ่าน bytes ดิบจริง ไม่ได้ใช้ตัวที่แคชไว้) · body ภาษาไทย/emoji · body มีจุดและมีขึ้นบรรทัดใหม่ · GET body ว่าง · ลายเซ็น 63/65 bytes → ปฏิเสธ · ลายเซ็นแบบ DER → ปฏิเสธ · เวลาที่ขอบพอดี 300 วิ → ผ่าน, 301 → ไม่ผ่าน

การลงทะเบียน: ครั้งแรก → สำเร็จ · JWK เดิมซ้ำ → สำเร็จเงียบ · JWK ใหม่ทับ → 409 + log เตือน + ของเดิมไม่เปลี่ยน · JWK ที่ไม่ใช่ EC/P-256, มี private component, x/y ไม่ใช่ 32 bytes → ปฏิเสธ ไม่เขียนลง Redis

ของเดิมต้องไม่พัง: เทสต์เดิมของ Package ผ่านโดยไม่ถูกแก้ · git diff ของ 4 ไฟล์ต้องห้าม = ว่าง · เทสต์เดิมของ Codex ผ่านหลังอัปเวอร์ชัน lib · endpoint เดิมของ Codex ไม่ต้องการลายเซ็น

สิ่งที่ต้องยืนยันว่าไม่เกิด: ไม่มีการถอยไปใช้ HMAC เมื่อหา key ไม่เจอ · ไม่มี log ที่มี body ดิบหรือค่าลายเซ็น · Redis key ประกอบจาก oid เท่านั้น ห้ามจากค่าที่ client ส่งมา · ไม่มีการเปิด middleware ตัวเก่า · ไม่มี attribute ไปโผล่บน controller เดิม :::

7. สิ่งที่รอบนี้ยังไม่กัน (พูดให้ชัด อย่าขายเกิน)

เรื่องสถานะ
XSSไม่กัน — สคริปต์ในหน้าเดียวกันสั่งเซ็นได้ · คุณค่าจริงของ key แบบ export ไม่ได้ คือ “ขโมยออกไปใช้ที่เครื่องอื่นไม่ได้” ไม่ใช่ “สคริปต์ในหน้าใช้ไม่ได้”
ยิงซ้ำภายใน 300 วิยังทำได้ (ยังไม่มีตัวกันซ้ำ) · รับได้เพราะ endpoint นำร่องไม่มีผลข้างเคียง แต่ ต้องเพิ่มตัวกันซ้ำก่อนเอาไปใช้กับ endpoint จริง
ขโมย cookie แล้วชิงลงทะเบียนก่อนเจ้าของช่องแคบๆ ที่เหลือของ TOFU — ปิดสนิทได้ตอนย้ายจุดผูก key ไป Sentinel
ผู้ใช้ล้าง browser / ใช้หลายเครื่องจะติด 409 · รอบนี้แก้ด้วยการตั้งอายุ key ไว้เสมอ (หมดอายุแล้วลงทะเบียนใหม่ได้) — ยังไม่มีการจัดการหลายอุปกรณ์
HMAC ยังเปิดอยู่ในระบบใครถือ shared secret ยังใช้ทางเดิมได้ · ไม่กระทบ endpoint นำร่อง (มันไม่รับ HMAC เลย) แต่ต้องรู้ไว้ตอนขยายผล

8. พิสูจน์แล้วก่อนเริ่มเขียนโค้ด (31/07/2026)

สมมติฐานที่ทั้งแผนตั้งอยู่บนมัน ถูกทดสอบด้วยของจริงทั้งสองฝั่งแล้ว — ก่อนเขียนโค้ดสักบรรทัด

ฝั่งเบราว์เซอร์ (Edge 150 บนเครื่อง Owner) — ผ่านทั้ง 3 ด่าน

ด่านผล
สร้างกุญแจ extractable:false แล้วยังดึง public key ออกมาได้✅ ได้ JWK ครบ (EC / P-256 / x,y ยาว 32 bytes)
ลายเซ็นเป็น raw r‖s 64 bytes (ไม่ใช่ DER)✅ 64 bytes
เก็บกุญแจข้ามการรีเฟรชหน้าได้✅ IndexedDB เก็บ CryptoKey ได้

ฝั่ง .NET — เอาลายเซ็นที่เบราว์เซอร์เพิ่งสร้าง มาให้ .NET ตรวจด้วยกติกาเดียวกับที่จะ implement จริง ผ่านทั้ง 9 ข้อ

PASS  body hash .NET == browser        PASS  same bytes rejected when read as DER
PASS  JWK x is 32 bytes                PASS  tampered body rejected
PASS  JWK y is 32 bytes                PASS  different method rejected
PASS  signature is 64 bytes (P1363)    PASS  different path rejected
PASS  VERIFY browser signature with IeeeP1363
ALL CHECKS PASSED - browser and .NET agree on the wire contract

ทำไมข้อนี้สำคัญกว่าที่เห็น: bug ที่ฆ่างานประเภทนี้บ่อยที่สุดคือ “สองฝั่งพูดคนละภาษาโดยไม่รู้ตัว” — ฝั่ง .NET เขียนเทสต์เอง ตรวจลายเซ็นที่ตัวเองสร้าง จะเขียว 100% แม้รูปแบบจะเข้ากับเบราว์เซอร์ไม่ได้เลย แล้วไปแตกตอนต่อ FE ซึ่งตอนนั้นแพงกว่ามาก · ตอนนี้เรารู้แน่แล้วว่าเข้ากันได้ และรู้ด้วยว่าถ้าใครเผลอเปลี่ยนรูปแบบเป็น DER มันจะไม่ผ่าน (ข้อ 6 ในผลทดสอบ)

ทั้ง 3 การตัดสินที่ค้างอยู่ ปิดครบแล้ว

เรื่องมติ
สมมติฐาน WebCrypto✅ พิสูจน์แล้วตามตารางข้างบน
เวลา✅ Owner อนุมัติขยายงบเวลา
การเผยแพร่แพ็กเกจpublish ผ่าน feed จริงทั้งสองฝั่ง (NuGet และ npm) ด้วย feed token — ไม่ใช้ local feed · ตามด้วยผลที่ต้องระวัง: เลขเวอร์ชันที่ push แล้วดันทับไม่ได้ ดังนั้นก่อน push ต้องตรวจว่าไฟล์ในแพ็กเกจมีของใหม่จริง (dotnet pack เคยแพ็ค build เก่ามาแล้ว)

9. รอบถัดไป (FE)

@exim/auth-sdk เพิ่มตัวจัดการกุญแจ + interceptor → shell ต่อสาย → publish 1.6.0 ขึ้น feed → ทดสอบผ่าน browser จริง (login จริง ไม่ seed ข้อมูล)

งานที่ต้องระวังไว้ล่วงหน้า — เจอไว้แล้วตอนสำรวจ:

  • shell ประกาศคอนฟิกซ้ำ 2 ที่ (app.config.ts บรรทัด 102-108 และ 119) ตัวหลังชนะ — ถ้าใส่ค่าผิดที่ interceptor จะเงียบสนิทโดยที่ build ยังเขียว จับได้เฉพาะตอนเปิดเบราว์เซอร์ดู Network tab
  • jest ทดสอบ WebCrypto/IndexedDB ไม่ได้ (jsdom ไม่มีให้) → ต้องแยก “ตัวประกอบข้อความที่จะเซ็น” ออกมาเป็นฟังก์ชันบริสุทธิ์ตั้งแต่แรก จะได้ทดสอบเทียบกับชุดตายตัวเดียวกับฝั่ง .NET ได้
  • spec ของ interceptor เดิมในไลบรารี (csrf.interceptor.spec.ts) มี mock ที่รูปทรงไม่ตรงกับที่โค้ดอ่านจริง — ห้าม copy เป็นแม่แบบ

10. สถานะล่าสุด

โค้ดที่แก้ไปแล้ว0 บรรทัด ทั้ง 5 repo
ผ่านแล้วสำรวจโค้ดจริง 5 repo · security review · skeptic challenge · test charter 14 เกณฑ์ · พิสูจน์ cross-stack ด้วยของจริง 9/9
ค้างที่รอ Owner สั่งเริ่มเขียนโค้ด
ถัดไปทันทีเมื่อได้ไฟเขียวBackend_Package 4 ไฟล์ใหม่ (ไม่แตะของเดิม) → เทสต์ → pack+publish → Backend_CodexService endpoint ใหม่ → เทสต์ → ยิง HTTP จริง 7 เคส → review