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 key | integrity ขา 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-session | cookie 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 |
|---|---|---|
| 1 | Backend_UserService/src/UserService01.API/Program.cs (~L55-65) | uncomment AddSignatureAuth(...) block ที่มีอยู่ + uncomment options.Filters.Add<SignatureValidationFilter>() (L120) |
| 2 | appsettings ทุก env + IaC config parity | เพิ่ม section SignatureValidation: { SecretKey: "", SignatureHeaderName: "X-Signature", TimestampHeaderName: "X-Timestamp", TimestampToleranceSeconds: 300 } — SecretKey ว่าง ให้ KV inject |
| 3 | KV ทุก env | {prefix}-SignatureValidation--SecretKey = random 32+ bytes Base64 (openssl rand -base64 32) |
| 4 | Endpoint ที่เรียกโดย service อื่น (internal write) | ติด [RequireSignature] — เริ่มจากคู่ที่มี InternalHttpClient เรียกหากันอยู่แล้ว |
| 5 | ฝั่ง caller | เปลี่ยน HttpClient ธรรมดา → SignedHttpClient (มีใน Package) |
| 6 | Backend_ThirdPartyService/.../MiddlewarePipelineExtensions.cs:80-81 | uncomment 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 |
|---|---|---|
| Sentinel | SessionController/session model + AuthController.cs (GET /sentinel/user L183) | เพิ่ม field signingKey ใน session record (Redis) — generate ตอนสร้าง session, คืนใน user info response |
| Sentinel | InternalController.cs POST resolve-session (L29) | แนบ signingKey (หรือ hash อ้างอิง) ใน response ให้ APIM ส่งต่อ backend เป็น header ภายใน |
| Frontend | apps/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) |
| Backend | SignatureValidationFilter | ขยายให้รองรับ per-session key lookup (ตอนนี้รับ key เดียวจาก options) — เพิ่ม ISignatureKeyResolver abstraction ใน Package |
| APIM | policy | ส่งผ่าน 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) — ใช้ได้เลย งานคือ:
- ทำ PII inventory ก่อน (IMPL_PLAN_08) → ได้ list field ที่ต้องติด
[Encrypt] - ติด attribute บน Command/Query properties ที่เป็น PII ระดับสูง (เลขบัตร, เลขบัญชี)
- ตรวจว่า
EncryptionBehaviorDefaultตั้งเป็นOptInทุก service (UserService ตั้งแล้ว —Program.csL152)
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 gate | Consent 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/BOLA | resource id ที่เดาได้ (sequential int) → GUID + object-level authorization ทุก endpoint (WHERE ownerId = currentUser) — สำคัญกว่าตัว encode | audit ต่อ 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 แล้ว
⚠️ ข้อควรระวัง
- เปิด
SignatureValidationFilterก่อนแจก key ให้ caller ครบ = caller เดิมพังทันที — ลำดับ: แจก key → caller ใช้SignedHttpClient→ ค่อยติด[RequireSignature] SignatureValidationMiddlewareแบบทั้ง pipeline (ThirdParty) ต้องตั้งExcludedPathsครบ (/health,/swagger, OIDC callback) มิฉะนั้น probe fail- AES-CBC ไม่มี authentication tag (integrity) — ถ้า upgrade รอบหน้าพิจารณา AES-GCM ใน Package (breaking change — วางแผน migration
enc:→enc2:) - Web Crypto ใช้ได้เฉพาะ secure context (HTTPS) — dev localhost OK, แต่ห้าม serve ผ่าน http IP
🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)
- D4 = external write เท่านั้น — HMAC เปิดเฉพาะ service-to-service + external partner (ทาง (1) ของ §1) · ทุก write ภายใน = เพิ่ม cost ทุก request โดยที่ TLS+JWT ครอบอยู่แล้ว
- ข้ามทาง (2) session-bound key — ป้องกัน cookie-theft แต่ไม่กัน XSS · เงินเท่ากันเอาไปลง CSP + แก้ F1/F5 ของ IMPL_PLAN_10 §9 คุ้มกว่า — ทำเฉพาะเมื่อผู้ตรวจบังคับเป็นลายลักษณ์อักษร
- ลำดับที่ผมเลือก: เปิด HMAC S2S ที่ ThirdParty (ของถูก comment ปิดอยู่ — เปิดกลับง่ายสุด) → คู่ service ที่มี InternalHttpClient อยู่แล้ว → RSA field encryption หลัง PII inventory — อย่าเริ่มที่ RSA ก่อนเพราะยังไม่รู้ว่า field ไหนต้อง encrypt
- แยก key ต่อความสัมพันธ์ตั้งแต่วันแรก — อย่าเริ่มด้วย key เดียวทั้ง platform แล้วค่อยแยกทีหลัง (การแยกทีหลัง = rotate ทั้งระบบอีกรอบ)
- 2.3 Parameter Encode: เลือก (ก)+(ค) เป็นงานจริง ส่วน (ข) เป็น audit เดียวจบ — (ก) ตั้งเป็นกฎ + CI gate · (ค) opaque ID + BOLA check สำคัญสุดในสามเพราะเป็น OWASP API #1 · (ข) EF ครอบอยู่แล้ว เหลือ grep จุด bypass ครั้งเดียว
- 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 |