Private Docs

IMPL PLAN 02 — Data in Transit

BodySignature, field encryption, parameter encode — ตอบคำถามหลัก "Frontend เอา key มาจากไหน"

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

IMPL PLAN 02 — การรับส่งข้อมูล (BodySignature · Field Encryption · Parameter Encode)

วันที่: 07/07/2026 · สถานะ: ⏳ รอ verify · Priority: 🟠 Phase 1-2 อ้างอิง: DISCUSSION หมวด 2 · โค้ดจริง: Backend_Package/src/Infrastructure/Security/HmacSignatureService.cs, AesEncryptionService.cs, Middleware/SignatureValidationMiddleware.cs, Filters/SignatureValidationFilter.cs

สรุป

ของมีครบใน Backend_Package แล้ว (HMAC-SHA256 + AES-256 + middleware/filter) — งานคือ เปิดใช้ให้ถูก scope และตอบโจทย์สำคัญที่สุดของหมวดนี้: “Frontend จะเอา key มาจากไหน” ซึ่งมีคำตอบต่างกันตามชนิด key


§1 หลักคิดเรื่อง Key บน Client — ตอบคำถามหลักก่อน

คำถามทีม: “BodySignature — Frontend จะเอา key จากไหนมาเข้ารหัส การเก็บไว้กับตัวก็เสี่ยง”

คำตอบตรงๆ: ถูกต้องแล้วที่สงสัย — secret แบบ static เก็บบน browser/mobile ไม่ได้เลย (bundle JS อ่านได้, app binary แกะได้) ทางออกมี 3 ทางตามระดับความต้องการ:

ทางหลักการSecret อยู่ที่ client?ป้องกันอะไรEffort
(1) ไม่ sign จาก browser — ใช้ TLS + HttpOnly cookie + CSRF + APIM subscription keyintegrity ขา client→APIM ให้ TLS ทำ (ครอบอยู่แล้ว) · HMAC ใช้เฉพาะ service-to-service / external partner ที่สองฝั่งเก็บ key ใน KV ได้❌ ไม่มีเลยtampering ระหว่างทาง (TLS) + CSRF + app identityต่ำ — แนะนำเป็น Phase แรก
(2) Session-bound ephemeral key — Sentinel แจก signing key ต่อ sessionหลัง login Sentinel สุ่ม key ผูกกับ session (เก็บ Redis) ส่งให้ SPA ทาง TLS · SPA เก็บ ใน memory เท่านั้น (ห้าม localStorage) · key ตายพร้อม session✅ แต่อายุสั้น + per-sessioncookie theft (ขโมย cookie อย่างเดียวยิง API ไม่ได้เพราะไม่มี key) + replay (timestamp+nonce)กลาง — Phase ถัดไปถ้าต้องการ
(3) Asymmetric (public key) — client เข้ารหัสด้วย public key ของ serverใช้กับ field encryption ขาส่ง (ไม่ใช่ signature) — public key แจกได้อิสระ ไม่ใช่ความลับ · server ถือ private key ใน KV ฝั่งเดียว✅ แต่เป็น public = ปลอดภัยโดยนิยามfield อ่อนไหว (เลขบัตร, PIN) ไม่โผล่ plaintext แม้ TLS ถูก inspect/terminate กลางทางกลาง

⚠️ พูดตรง (จำกัดความคาดหวัง): ทางเลือก (2) ป้องกัน “ขโมย cookie แล้วยิงจากเครื่องอื่น” ได้ แต่ไม่ป้องกัน XSS ที่ยึด browser ได้ทั้งตัว (attacker อ่าน key ใน memory ได้เหมือนกัน) — เกราะหลักกัน XSS คือ CSP (มีแล้วใน AdvancedSecurityHeadersMiddleware) + Angular sanitization

ข้อเสนอ: Phase 1 ทำ (1) — เปิด HMAC เฉพาะ service-to-service/external · Field encryption FE→BE ที่จำเป็นใช้ (3) · ส่วน (2) เก็บเป็น decision D4 ว่าคุ้ม effort ไหม


§2 BodySignature — Phase 1: Service-to-Service / External (แนะนำเริ่มที่นี่)

กลไกที่มีอยู่แล้ว (verified จากโค้ด)

  • HmacSignatureService.CreateSignature(payload, timestamp) = Base64(HMAC-SHA256(key, "{timestamp}.{payload}")) — constant-time compare, timestamp tolerance 300s
  • Headers: X-Signature + X-Timestamp (ปรับได้ผ่าน SignatureValidationOptions)
  • ฝั่งรับ: SignatureValidationMiddleware (ทั้ง pipeline) หรือ SignatureValidationFilter ([RequireSignature]/[SkipSignature] รายจุด — แนะนำแบบ filter ควบคุม scope ง่ายกว่า)
  • ฝั่งส่ง: SignedHttpClient มีแล้วใน Package

Sequence Diagram — Service-to-Service Signature

sequenceDiagram
    autonumber
    participant A as Service A (caller)
    participant KV as Key Vault
    participant B as Service B (endpoint ติด [RequireSignature])

    Note over A,KV: ตอน startup
    A->>KV: โหลด SignatureValidation:SecretKey (ผ่าน AddAzureKeyVaultWithServicePrefix)
    B->>KV: โหลด key เดียวกัน (shared secret ต่อ "คู่/กลุ่ม" service)

    Note over A,B: ตอนเรียก API
    A->>A: timestamp = UnixNow<br/>signature = HMAC(key, "{ts}.{jsonBody}")
    A->>B: POST /api/x + X-Signature + X-Timestamp (ผ่าน SignedHttpClient)
    B->>B: ValidateTimestamp(ts, 300s) — กัน replay เก่า
    B->>B: recompute HMAC + FixedTimeEquals
    alt signature ถูก + timestamp ใน tolerance
        B-->>A: 200 OK
    else ผิด/หมดเวลา
        B-->>A: 401 — log SignatureValid=false (มีใน HttpContext.Items แล้ว)
    end

ไฟล์ที่ต้องแก้

#ไฟล์Action
1Backend_UserService/src/UserService01.API/Program.cs (~L55-65)uncomment AddSignatureAuth(...) block ที่มีอยู่ + uncomment options.Filters.Add<SignatureValidationFilter>() (L120)
2appsettings ทุก env + IaC config parityเพิ่ม section SignatureValidation: { SecretKey: "", SignatureHeaderName: "X-Signature", TimestampHeaderName: "X-Timestamp", TimestampToleranceSeconds: 300 } — SecretKey ว่าง ให้ KV inject
3KV ทุก env{prefix}-SignatureValidation--SecretKey = random 32+ bytes Base64 (openssl rand -base64 32)
4Endpoint ที่เรียกโดย service อื่น (internal write)ติด [RequireSignature] — เริ่มจากคู่ที่มี InternalHttpClient เรียกหากันอยู่แล้ว
5ฝั่ง callerเปลี่ยน HttpClient ธรรมดา → SignedHttpClient (มีใน Package)
6Backend_ThirdPartyService/.../MiddlewarePipelineExtensions.cs:80-81uncomment app.UseSignatureValidation() + ตั้ง ExcludedPaths (/health, /swagger) — ตัวนี้เป็น middleware แบบทั้ง pipeline เพราะรับจาก external partner

การจัด Key (สำคัญ)

  • แยก key ต่อ “ความสัมพันธ์” ไม่ใช่ key เดียวทั้ง platform — อย่างน้อย: key ภายใน (service↔service) แยกจาก key ต่อ external partner แต่ละราย · รั่ว 1 key ไม่ล้มทั้งระบบ
  • เก็บใน KV เท่านั้น + rotation ดู IMPL_PLAN_06 (dual-key window ตอน rotate: ฝั่งรับ validate ได้ 2 key ชั่วคราว)

§3 BodySignature — Phase 2 (Optional): Browser Client ด้วย Session-Bound Key

ทำเมื่อที่ประชุมตัดสินว่าต้องการเกราะกัน cookie-theft เพิ่มจาก TLS — ถ้ายังไม่จำเป็น ข้ามได้ ไม่บล็อกหมวดอื่น

Sequence Diagram — แจก key + ใช้ key

sequenceDiagram
    autonumber
    participant SPA as Angular Shell (memory เท่านั้น)
    participant APIM as APIM (session façade)
    participant SG as Sentinel Gateway
    participant R as Redis (session store)
    participant BE as Backend Service

    Note over SPA,R: ① Login แล้วแจก ephemeral key
    SPA->>SG: GET /sentinel/login → OIDC flow (มีอยู่แล้ว)
    SG->>R: สร้าง session + สุ่ม signingKey (32B) เก็บใน session record
    SG-->>SPA: Set-Cookie (HttpOnly) + คืน signingKey ใน response body ของ /sentinel/user
    Note over SPA: เก็บ key ใน closure/service memory<br/>❌ ห้าม localStorage/sessionStorage

    Note over SPA,BE: ② เรียก API พร้อม signature
    SPA->>SPA: sig = HMAC-SHA256(signingKey, "{ts}.{body}") via Web Crypto API
    SPA->>APIM: POST /api/... + Cookie + X-Signature + X-Timestamp
    APIM->>SG: resolve-session (cookie → JWT + sessionId) — flow เดิม
    APIM->>BE: forward + JWT + X-Signature + X-Timestamp + X-Session-Id
    BE->>R: ดึง signingKey ของ session (หรือรับจาก resolve-session ผ่าน APIM)
    BE->>BE: validate HMAC + timestamp
    BE-->>SPA: 200 / 401

    Note over SPA,R: ③ Refresh หน้า → key หาย → ขอใหม่
    SPA->>SG: GET /sentinel/user (มี cookie)
    SG-->>SPA: คืน signingKey เดิมของ session

งานที่ต้องทำ (สรุประดับไฟล์)

ฝั่งไฟล์/จุดAction
SentinelSessionController/session model + AuthController.cs (GET /sentinel/user L183)เพิ่ม field signingKey ใน session record (Redis) — generate ตอนสร้าง session, คืนใน user info response
SentinelInternalController.cs POST resolve-session (L29)แนบ signingKey (หรือ hash อ้างอิง) ใน response ให้ APIM ส่งต่อ backend เป็น header ภายใน
Frontendapps/shell/src/app/interceptors/ — สร้าง body-signature.interceptor.ts ใหม่ (วางคู่ gateway-credentials.interceptor.ts ที่มีอยู่)intercept POST/PUT/PATCH → Web Crypto crypto.subtle.sign("HMAC", key, ...) → ใส่ 2 headers · key มาจาก auth state service (memory)
BackendSignatureValidationFilterขยายให้รองรับ per-session key lookup (ตอนนี้รับ key เดียวจาก options) — เพิ่ม ISignatureKeyResolver abstraction ใน Package
APIMpolicyส่งผ่าน X-Signature/X-Timestamp + สั่ง reject ถ้า endpoint บังคับ signature แต่ header หาย (fail-fast ที่ขอบ)

ข้อจำกัดที่ต้องแจ้งทีม: multi-tab ใช้ session เดียวกัน = key เดียวกัน (OK) · SSR/prerender ไม่มี Web Crypto (shell เป็น SPA — ไม่กระทบ) · mobile WebView ต้องเทสแยก


§4 Field Encryption

4.1 ฝั่ง Backend↔Backend / at-rest — ใช้ของเดิม

[Encrypt] attribute + MediatR EncryptionBehavior + AesEncryptionService (AES-256-CBC, key 32B Base64 จาก KV, ciphertext มี enc: prefix) — ใช้ได้เลย งานคือ:

  1. ทำ PII inventory ก่อน (IMPL_PLAN_08) → ได้ list field ที่ต้องติด [Encrypt]
  2. ติด attribute บน Command/Query properties ที่เป็น PII ระดับสูง (เลขบัตร, เลขบัญชี)
  3. ตรวจว่า EncryptionBehaviorDefault ตั้งเป็น OptIn ทุก service (UserService ตั้งแล้ว — Program.cs L152)

4.2 ฝั่ง Frontend→Backend (field อ่อนไหวขาส่ง) — ใช้ Asymmetric แก้ปัญหา key

นี่คือคำตอบของ “FE จะเอา key จากไหน” สำหรับ encryption: ไม่ต้องมี secret เลย — ใช้ RSA-OAEP public key ของ server (public key ไม่ใช่ความลับ ฝังใน bundle ได้)

sequenceDiagram
    autonumber
    participant SPA as Angular (Web Crypto API)
    participant BE as Backend Service
    participant KV as Key Vault (ถือ private key)

    Note over SPA,BE: ครั้งแรก (หรือ cache ตาม kid)
    SPA->>BE: GET /api/v1/crypto/public-key
    BE-->>SPA: { kid, publicKeyJwk (RSA-OAEP-256) }

    Note over SPA: ก่อน submit form ที่มี field อ่อนไหว
    SPA->>SPA: encryptedField = RSA-OAEP(publicKey, plaintext)<br/>(field เล็ก เช่น citizenId — ไม่ใช่ทั้ง body)
    SPA->>BE: POST { citizenId: "rsa:<kid>:<base64>", ...field อื่น plaintext }
    BE->>KV: unwrap ด้วย private key (KV Keys API — private key ไม่ออกจาก KV)
    BE->>BE: process plaintext ใน memory เท่านั้น
    BE-->>SPA: 200

งาน: Package เพิ่ม RsaFieldDecryptionService (ใช้ KV Keys Decrypt operation — private key ไม่เคยออกจาก vault) + [EncryptedFromClient] model binder/behavior · Frontend เพิ่ม crypto.service.ts (Web Crypto RSA-OAEP) ใช้เฉพาะ form ที่ inventory ชี้ว่าอ่อนไหวจริง (KYC, บัตรประชาชน)

อย่าทำ: เข้ารหัสทั้ง body ทุก request ด้วย RSA (ช้า + ไม่จำเป็น — TLS ครอบอยู่แล้ว) · ใช้ AES key ฝังใน frontend (= ปัญหา key เดิมวนกลับมา)

4.3 ฝั่ง Backend→Frontend (response) — อย่าเข้ารหัส ใช้ minimization แทน

EnableResponseEncryption ใน middleware มีอยู่ แต่สำหรับ browser = ต้องแจก symmetric key ให้ browser ถอด → security theater (ใครถอดได้ = attacker ถอดได้) → ทางถูก: ไม่ส่ง field ที่ไม่ใช้ (data minimization) + mask ที่ server (1234-XXXX-XXXX) — ดู IMPL_PLAN_08


§5 Parameter Encode — 3 ความหมาย 3 งาน

ความหมายงานไฟล์/จุด
(ก) ห้าม sensitive ใน URL/queryตั้งกฎ: PII/secret ห้ามอยู่ใน query string (ไป body/header เสมอ) + audit GET endpoints ที่รับ ?identifier=, ?email= (เจอจริงที่ Consent GET /user?identifier=) + CI grep gateConsent ConsentController → เปลี่ยนเป็น POST หรือใช้ opaque id · เพิ่ม rule ใน regression-gates.yml (IMPL_PLAN_01 A9)
(ข) Encode กัน injectionฝั่ง SQL ปลอดภัยแล้ว (EF parameterized) — audit เฉพาะจุด bypass: raw SQL (FromSqlRaw ที่ concat!), Angular innerHTML/bypassSecurityTrust*grep -rn "FromSqlRaw|ExecuteSqlRaw" ทุก backend · grep -rn "bypassSecurityTrust|\[innerHTML\]" ใน Frontend_*
(ค) Opaque ID กัน IDOR/BOLAresource id ที่เดาได้ (sequential int) → GUID + object-level authorization ทุก endpoint (WHERE ownerId = currentUser) — สำคัญกว่าตัว encodeaudit ต่อ controller ตอนทำ FallbackPolicy rollout (IMPL_PLAN_05) — เช็ค handler ว่า filter ด้วย user id จาก claims เสมอ

Verify

  • Service-to-service: ยิง endpoint [RequireSignature] โดยไม่มี header → 401 · ด้วย signature ถูก → 200 · timestamp เก่ากว่า 300s → 401
  • Unit test: HmacSignatureService — payload เพี้ยน 1 byte → invalid
  • FE field encryption: intercept network เห็น rsa:<kid>:... ไม่เห็น plaintext citizenId
  • grep FromSqlRaw = ทุก hit ใช้ parameter placeholder
  • Consent GET /user?identifier= ไม่รับ PII ทาง query แล้ว

⚠️ ข้อควรระวัง

  1. เปิด SignatureValidationFilter ก่อนแจก key ให้ caller ครบ = caller เดิมพังทันที — ลำดับ: แจก key → caller ใช้ SignedHttpClient → ค่อยติด [RequireSignature]
  2. SignatureValidationMiddleware แบบทั้ง pipeline (ThirdParty) ต้องตั้ง ExcludedPaths ครบ (/health, /swagger, OIDC callback) มิฉะนั้น probe fail
  3. AES-CBC ไม่มี authentication tag (integrity) — ถ้า upgrade รอบหน้าพิจารณา AES-GCM ใน Package (breaking change — วางแผน migration enc:enc2:)
  4. Web Crypto ใช้ได้เฉพาะ secure context (HTTPS) — dev localhost OK, แต่ห้าม serve ผ่าน http IP

🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)

  1. D4 = external write เท่านั้น — HMAC เปิดเฉพาะ service-to-service + external partner (ทาง (1) ของ §1) · ทุก write ภายใน = เพิ่ม cost ทุก request โดยที่ TLS+JWT ครอบอยู่แล้ว
  2. ข้ามทาง (2) session-bound key — ป้องกัน cookie-theft แต่ไม่กัน XSS · เงินเท่ากันเอาไปลง CSP + แก้ F1/F5 ของ IMPL_PLAN_10 §9 คุ้มกว่า — ทำเฉพาะเมื่อผู้ตรวจบังคับเป็นลายลักษณ์อักษร
  3. ลำดับที่ผมเลือก: เปิด HMAC S2S ที่ ThirdParty (ของถูก comment ปิดอยู่ — เปิดกลับง่ายสุด) → คู่ service ที่มี InternalHttpClient อยู่แล้ว → RSA field encryption หลัง PII inventory — อย่าเริ่มที่ RSA ก่อนเพราะยังไม่รู้ว่า field ไหนต้อง encrypt
  4. แยก key ต่อความสัมพันธ์ตั้งแต่วันแรก — อย่าเริ่มด้วย key เดียวทั้ง platform แล้วค่อยแยกทีหลัง (การแยกทีหลัง = rotate ทั้งระบบอีกรอบ)
  5. 2.3 Parameter Encode: เลือก (ก)+(ค) เป็นงานจริง ส่วน (ข) เป็น audit เดียวจบ — (ก) ตั้งเป็นกฎ + CI gate · (ค) opaque ID + BOLA check สำคัญสุดในสามเพราะเป็น OWASP API #1 · (ข) EF ครอบอยู่แล้ว เหลือ grep จุด bypass ครั้งเดียว
  6. AES-CBC → GCM: อย่าทำเป็นงานแยก — ทำพร้อม key versioning ของ IMPL_PLAN_06 งาน 6.3 รอบเดียว (enc:enc2: ต้องออกแบบครั้งเดียวให้จบ)

📚 อ้างอิงมาตรฐาน (สำหรับใช้ในที่ประชุม):

หลักการแหล่งอ้างอิง
HMAC — สเปคต้นฉบับ (กลไกที่ HmacSignatureService ใช้)RFC 2104 — HMAC: Keyed-Hashing for Message Authentication
เหตุผลที่ static secret บน client ไม่ปลอดภัย + crypto ผิดวิธีOWASP A02
— Cryptographic Failures
การเลือก algorithm/mode (CBC vs GCM, RSA-OAEP)OWASP Cryptographic Storage Cheat Sheet
Web Crypto API (กลไก crypto ฝั่ง browser — ของ native ไม่ใช่ lib เสริม)MDN — Web Crypto API
Key management lifecycle (แยก key ต่อความสัมพันธ์, rotation)NIST SP 800-57 Part 1 Rev.5 — Key Management
Opaque ID / object-level authorization (2.3 ข้อ ค)OWASP API1
— Broken Object Level Authorization