ข้ามไปยังเนื้อหา
กลับไปหน้าบทความ
ความปลอดภัยเครือข่าย

TLS 1.3 Handshake ทีละข้อความ อะไรยังอ่านได้เมื่อไม่ถอดรหัส

ไล่ดู TLS 1.3 Handshake ทีละข้อความ ตั้งแต่ ClientHello ถึง Finished ทำไม Firewall ยังเห็น SNI แต่ไม่เห็น Certificate และความเสี่ยงของ 0-RTT

ปานกลาง11 นาทีในการอ่าน

ในบทความนี้

ใครที่ดูแลไฟร์วอลล์หรือ proxy คงเจอ TLS อยู่ในแทบทุก ticket ไม่ว่าจะเป็น decryption rule ที่ทำให้แอปพัง URL category ที่ match จากชื่อเซิร์ฟเวอร์ หรือ "certificate error" ที่เกิดเฉพาะตอนวิ่งผ่าน proxy ปัญหาส่วนใหญ่จะเข้าใจง่ายขึ้นมาก ถ้าเรารู้ว่าแต่ละข้อความใน handshake มีอะไรอยู่ และข้อความไหนวิ่งแบบอ่านได้ (in the clear) ครับ

บทความนี้จะไล่ดู TLS 1.3 handshake แบบที่เห็นใน packet capture ทีละข้อความ โดยอ้างอิงสองแหล่งหลัก คือตัวมาตรฐาน RFC 8446 และ The Illustrated TLS 1.3 Connection ซึ่งอธิบายการเชื่อมต่อจริงทีละไบต์ ถ้าอยากทำตามไปด้วย แนะนำให้เปิดหน้านั้นไว้อีกแท็บครับ

ทำไม TLS 1.3 ถึงต่างจาก TLS 1.2 มาก

RFC 8446 เผยแพร่ในเดือนสิงหาคม 2561 หัวข้อ 1.2 "Major Differences from TLS 1.2" ควรอ่านทั้งหัวข้อ แต่การเปลี่ยนแปลงที่สำคัญที่สุดเมื่อมองจากสายสัญญาณมีดังนี้

  • ใช้ round trip เดียวแทนสองครั้ง ใน TLS 1.2 การเชื่อมต่อใหม่ต้องรอสอง round trip กว่า client จะส่งข้อมูลที่เข้ารหัสได้ แต่ TLS 1.3 ใช้แค่หนึ่ง เพราะ client ส่ง key share มาตั้งแต่ข้อความแรก (Cloudflare, "A Detailed Look at RFC 8446")
  • ตัด cryptography รุ่นเก่าออก เหลือเฉพาะ AEAD cipher ส่วน cipher suite แบบ static RSA และ Diffie-Hellman ถูกตัดออก ทำให้ key exchange แบบ public key ทุกแบบมี forward secrecy นอกจากนี้ยังตัด compression และ custom DHE group ออกด้วย (RFC 8446 หัวข้อ 1.2)
  • ทุกอย่างหลัง ServerHello ถูกเข้ารหัส รวมถึง certificate ของเซิร์ฟเวอร์ด้วย (RFC 8446 หัวข้อ 1.2)
  • มีโหมด 0-RTT ให้ client ที่เคยเชื่อมต่อแล้วส่งข้อมูลได้ตั้งแต่ flight แรก แลกกับคุณสมบัติด้านความปลอดภัยบางอย่าง ซึ่งจะกลับมาพูดถึงในช่วงหลัง

ข้อที่สามสำคัญที่สุดสำหรับอุปกรณ์เครือข่าย ขอให้จำไว้ระหว่างอ่านต่อครับ

เตรียม lab: ดูด้วยตัวเอง

ไม่ต้องใช้ฮาร์ดแวร์พิเศษครับ แค่จับ traffic ของเบราว์เซอร์ตัวเองด้วย Wireshark แล้วกรองด้วย tls ฟิลด์ที่ใช้ในบทความนี้มีอยู่ใน Wireshark TLS display filter reference ทั้งหมด ได้แก่ tls.handshake.type, tls.handshake.extensions_server_name, tls.handshake.extensions.supported_version, tls.handshake.extensions_key_share_group และ tls.record.opaque_type

ถ้าอยากเห็นส่วนที่เข้ารหัส ให้ใช้ key log file ตั้งค่า environment variable SSLKEYLOGFILE ก่อนเปิด Firefox หรือ Chrome แล้วชี้ preference "(Pre)-Master-Secret log filename" ของ Wireshark ไปที่ไฟล์นั้น Wireshark wiki ระบุว่าวิธีเก่าที่โหลด RSA private key ของเซิร์ฟเวอร์ ใช้ไม่ได้ กับ TLS 1.3 และกับ cipher suite แบบ (EC)DHE ทุกตัว (Wireshark wiki: TLS) นี่คือ forward secrecy ที่ทำงานตามที่ออกแบบไว้ และเป็นเหตุผลที่ไฟร์วอลล์ถอดรหัส TLS 1.3 ไม่ได้ด้วยการให้ private key ของเซิร์ฟเวอร์ไปเฉยๆ แต่ต้องเข้าไปอยู่ตรงกลางของการเชื่อมต่อ ซึ่งจะเห็นในช่วงท้ายครับ

ชนิดของ handshake message ที่เราจะใช้กรองนิยามไว้ใน RFC 8446 หัวข้อ 4 ได้แก่ client_hello(1), server_hello(2), new_session_ticket(4), encrypted_extensions(8), certificate(11), certificate_verify(15) และ finished(20)

Full handshake แบบ 1-RTT มีเพียง Hello สองข้อความที่อ่านได้บนสาย ทุกอย่างในกรอบเส้นประถูกเข้ารหัส

ข้อความที่ 1: ClientHello (อ่านได้)

Client เป็นฝ่ายเริ่มก่อน และนี่คือข้อความที่มีข้อมูลมากที่สุดสำหรับคนที่เฝ้าดูเครือข่าย สิ่งที่ควรสังเกตใน capture มีดังนี้

ฟิลด์ version ตั้งใจให้ดูเหมือนเวอร์ชันเก่า legacy_version ใน ClientHello มีค่า 0x0303 ซึ่งเป็นค่าของ TLS 1.2 ส่วนเวอร์ชันที่เสนอจริงอยู่ใน extension supported_versions ซึ่งต้องมี 0x0304 สำหรับ TLS 1.3 และเมื่อมี supported_versions เซิร์ฟเวอร์ต้องไม่ใช้ legacy_version ในการตกลงเวอร์ชัน (RFC 8446 หัวข้อ 4.2.1) ใน record header อาจเห็นค่า 0x0301 ด้วยซ้ำ ซึ่งอนุญาตสำหรับ ClientHello แรก (หัวข้อ 5.1) เหตุผลคือปัญหา ossification ระหว่างการทำมาตรฐาน middlebox บางตัวพังเมื่อเจอการเปลี่ยนแปลงเล็กๆ บนสาย เช่นหมายเลขเวอร์ชันใหม่ TLS 1.3 จึงถูกออกแบบให้ดูเหมือน session resumption ของ TLS 1.2 (Cloudflare)

key_share คือ key exchange ครึ่งของฝั่ง client Client เดาว่าเซิร์ฟเวอร์จะรับ group ไหน (ตัวอย่างใน Illustrated TLS ใช้ X25519) แล้วส่ง public value ของ group นั้นมาเลย การเดานี้เองที่ประหยัดไปหนึ่ง round trip ถ้าเซิร์ฟเวอร์ไม่รองรับ share ที่เสนอมาเลย จะตอบด้วย HelloRetryRequest ที่ระบุ group ที่รับได้ แล้ว client ก็ส่งใหม่ (RFC 8446 หัวข้อ 4.1.4 และ 4.2.8) ใน capture นั้น HelloRetryRequest จะหน้าตาเหมือน ServerHello แต่ฟิลด์ Random มีค่าพิเศษที่กำหนดตายตัว

server_name (SNI) คือชื่อโฮสต์แบบข้อความธรรมดา เป็นหนึ่งใน extension ที่แอปพลิเคชันที่เป็นไปตาม TLS ต้อง implement เว้นแต่ application profile จะกำหนดไว้เป็นอย่างอื่น (หัวข้อ 9.2)

cipher_suites สั้นลงมาก TLS 1.3 มีเพียงห้า suite ตั้งแต่ TLS_AES_128_GCM_SHA256 (0x1301) ถึง TLS_AES_128_CCM_8_SHA256 (0x1305) ตอนนี้ suite ระบุแค่ AEAD cipher กับ hash ส่วน key exchange และ signature แยกไปตกลงกันใน extension ส่วน TLS_AES_128_GCM_SHA256 เป็นตัวที่ต้อง implement (RFC 8446 หัวข้อ 9.1 และภาคผนวก B.4)

Extension อื่นที่จะเห็นในข้อความนี้ได้แก่ supported_groups, signature_algorithms และ application_layer_protocol_negotiation (ALPN เช่น h2) ตาราง extension ใน RFC 8446 ระบุว่า server_name และ ALPN อยู่ได้ทั้งใน ClientHello และ EncryptedExtensions (หัวข้อ 4.2) แปลว่าสิ่งที่ client เสนอมองเห็นได้ แต่คำตอบของเซิร์ฟเวอร์มองไม่เห็นครับ

ข้อความที่ 2: ServerHello (สิ่งสุดท้ายที่อ่านได้)

เซิร์ฟเวอร์เลือก cipher suite แล้วส่ง key_share ของตัวเองใน group เดียวกันกลับมา พร้อม supported_versions ที่ตั้งเป็น 0x0304 มีรายละเอียดสองข้อที่ควรรู้ครับ

  • การป้องกัน downgrade อยู่ในฟิลด์ Random เซิร์ฟเวอร์ TLS 1.3 ที่ตกลงใช้ TLS 1.2 หรือเก่ากว่า จะใส่ marker ตายตัวในแปดไบต์สุดท้ายของ ServerHello.random (ตัวอักษร ASCII DOWNGRD ตามด้วยอีกหนึ่งไบต์) และ client ที่รองรับ TLS 1.3 เมื่อเห็น marker นี้จะยกเลิกการเชื่อมต่อ (RFC 8446 หัวข้อ 4.1.3)
  • ตอนนี้ทั้งสองฝั่งมี shared secret แล้ว เมื่อมี key share ครบทั้งสองฝั่ง client และเซิร์ฟเวอร์ต่างคำนวณ handshake traffic key ได้เอง ตั้งแต่จุดนี้ไป ส่วนที่เหลือของ handshake ผู้สังเกตการณ์จะอ่านไม่ได้อีก

หลัง ServerHello อาจเห็น record ChangeCipherSpec ด้วย ใน TLS 1.3 มันไม่มีหน้าที่อะไร เป็น record หลอกจาก "middlebox compatibility mode" ที่ส่งไว้ให้การแลกเปลี่ยนดูเหมือน TLS 1.2 resumption ในสายตาอุปกรณ์รุ่นเก่า (RFC 8446 ภาคผนวก D.4 และใน capture ของ Illustrated TLS จะเห็นหนึ่ง record ในแต่ละทิศทาง)

ข้อความที่ 3 ถึง 6: flight ของเซิร์ฟเวอร์ที่เข้ารหัสแล้ว

ถ้าไม่มี key ทาง Wireshark จะแสดง record ถัดๆ ไปเป็น "Application Data" ซึ่งเป็นไปตามการออกแบบ เพราะ record ที่เข้ารหัสแล้วใน TLS 1.3 ทุกตัวมี outer content type เป็น 23 (application_data) และเวอร์ชัน 0x0303 ส่วนชนิดจริงซ่อนอยู่ข้างในการเข้ารหัส (RFC 8446 หัวข้อ 5.2) เมื่อโหลด key log แล้วจะเห็นว่าเป็นข้อความเหล่านี้

  1. EncryptedExtensions คำตอบของเซิร์ฟเวอร์ที่สมัยก่อนส่งแบบอ่านได้ เช่น ALPN protocol ที่เลือก
  2. Certificate certificate chain ของเซิร์ฟเวอร์ TLS เวอร์ชันก่อนหน้าส่งส่วนนี้แบบข้อความธรรมดา (RFC 8744) แต่ใน TLS 1.3 ถูกปกป้องด้วย handshake key (RFC 8446 Figure 1)
  3. CertificateVerify ลายเซ็นด้วย private key ของ certificate ครอบ handshake ทั้งหมดจนถึงจุดนั้น เพื่อพิสูจน์ว่าเซิร์ฟเวอร์ถือ key นั้นจริง และผูก certificate เข้ากับ handshake ครั้งนี้โดยเฉพาะ (หัวข้อ 4.4.3)
  4. Finished ค่า MAC ครอบ transcript ทั้งหมด ถ้ามีใครแก้ไขข้อความก่อนหน้าก็จะตรวจพบ (หัวข้อ 4.4.4)

เซิร์ฟเวอร์ส่ง application data ต่อจาก Finished ได้ทันที ฝั่ง client ตอบด้วย Finished ของตัวเองที่เข้ารหัสแล้ว (พร้อม Certificate และ CertificateVerify ถ้าเซิร์ฟเวอร์ขอ client certificate) เท่านี้ handshake ก็จบ หลังจากนั้นเซิร์ฟเวอร์มักส่ง NewSessionTicket หนึ่งข้อความหรือมากกว่า ซึ่งเข้ารหัสเช่นกัน ไว้ให้ client ใช้ resume ภายหลัง อายุของ ticket ต้องไม่เกินเจ็ดวัน (หัวข้อ 4.6.1)

ไฟร์วอลล์เห็นอะไรและไม่เห็นอะไรเมื่อไม่ได้ถอดรหัส

ส่วนนี้คือจุดที่การไล่ดูทีละข้อความมีประโยชน์กับงานเครือข่ายจริงๆ ครับ

เมื่อไม่ได้ถอดรหัส middlebox อ่านได้ว่าใครคุยกับที่ไหน (IP, SNI, ALPN ที่เสนอ, เวอร์ชัน) แต่อ่าน certificate และ payload ไม่ได้

ยังมองเห็น: IP address และ port รวมถึงทุกอย่างใน ClientHello ได้แก่ SNI, ALPN protocol ที่เสนอ, เวอร์ชันที่รองรับ, cipher suite และ key share group ส่วน suite ที่ ServerHello เลือกก็เห็นได้ เช่นเดียวกับขนาดของ record และจังหวะเวลา

มองไม่เห็นแล้ว: certificate ของเซิร์ฟเวอร์ และ extension ที่เซิร์ฟเวอร์ตอบกลับ RFC 8744 อธิบายผลลัพธ์นี้ไว้ดี เมื่อ TLS 1.3 เข้ารหัส certificate แล้ว การซ่อน SNI จึงมีประโยชน์มากขึ้น เพราะใน TLS เวอร์ชันก่อนหน้า ชื่อเดียวกันนี้ก็มองเห็นได้อยู่แล้วใน certificate ที่ส่งแบบข้อความธรรมดา เอกสารยังระบุอุปกรณ์ที่พึ่งพา SNI ด้วย ซึ่งรวมถึงไฟร์วอลล์องค์กรที่ใช้บล็อกเว็บไซต์ และอุปกรณ์ที่ใช้ตัดสินว่า traffic ไหนได้รับการยกเว้นจากการตรวจ

ผลในทางปฏิบัติมีสองข้อครับ

  • Rule ที่ต้องอ่าน certificate ของเซิร์ฟเวอร์ (เพื่อตรวจผู้ออก วันหมดอายุ หรือ subject) ใช้กับ TLS 1.3 ที่ไม่ได้ถอดรหัสไม่ได้ Palo Alto Networks เขียนไว้ตรงๆ ว่าสำหรับ traffic TLS 1.3 ที่ไม่ได้ถอดรหัส ไฟร์วอลล์มองไม่เห็น certificate จึงบล็อก session ที่ certificate หมดอายุหรือมาจากผู้ออกที่ไม่น่าเชื่อถือไม่ได้ (PAN-OS docs: TLS 1.3 Decryption)
  • SNI อาจไม่ได้อ่านได้ตลอดไป RFC 9849, TLS Encrypted Client Hello ซึ่งเผยแพร่เป็น Proposed Standard ในเดือนมีนาคม 2569 เข้ารหัส ClientHello ตัวจริงไว้ใน ClientHello ตัวนอก SNI ตัวนอกโดยปกติจะเป็น public name ของ client-facing server ของผู้ให้บริการ ผู้สังเกตการณ์ยังรู้ได้ว่าเรากำลังเชื่อมต่อกับผู้ให้บริการรายไหน และ ECH ไม่ได้ซ่อน IP ปลายทางหรือการ query DNS แบบข้อความธรรมดา แต่ policy ที่อิงชื่อโฮสต์จาก SNI แบบตรงตัวจะเห็นข้อมูลน้อยลงเมื่อ ECH ถูกใช้แพร่หลายขึ้น

0-RTT resumption และปัญหา replay

เมื่อ client มี ticket จากการเชื่อมต่อครั้งก่อน ก็ส่ง application data ได้ตั้งแต่ flight แรก โดยต้องใส่ทั้ง extension pre_shared_key และ early_data (RFC 8446 หัวข้อ 4.2.10) early data นี้ถูกเข้ารหัส แต่ RFC 8446 เขียนถึงข้อแลกเปลี่ยนไว้ชัดเจน (หัวข้อ 2.3)

  • ไม่มี forward secrecy เพราะถูกปกป้องด้วย key ที่ได้มาจาก PSK เท่านั้น
  • ไม่มีการรับประกันว่าจะไม่ถูก replay ข้ามการเชื่อมต่อ ข้อมูลแบบ 1-RTT ปกติได้รับการปกป้องจากค่า Random ใหม่ของเซิร์ฟเวอร์ ซึ่ง 0-RTT data พึ่งพาไม่ได้
ผู้ดักฟังส่ง flight แบบ 0-RTT ที่ดักได้ซ้ำ เซิร์ฟเวอร์ที่จดจำ ClientHello ที่เคยรับไว้จะจับตัวซ้ำได้และปฏิเสธ early data

ภาคผนวก E.5 อธิบายว่าทำไมเรื่องนี้สำคัญ request ที่ถูก replay อาจทำให้ action ที่มีผลข้างเคียงเกิดซ้ำ เช่นการซื้อสินค้าหรือการโอนเงิน เซิร์ฟเวอร์ป้องกันตัวเองได้ด้วย single-use ticket หรือการบันทึก ClientHello ภายในช่วงเวลาหนึ่ง และ RFC 8446 เสริมว่า client ต้องส่งเฉพาะ early data ที่ replay ได้อย่างปลอดภัย (หัวข้อ 8) สำหรับ HTTP มี RFC 8470 ที่เพิ่ม status code 425 (Too Early) และ header Early-Data เพื่อให้เซิร์ฟเวอร์หรือ gateway ขอให้ client ส่งใหม่โดยไม่ใช้ early data

เอกสารแนวปฏิบัติต่างระมัดระวังเรื่องนี้ RFC 9325 ระบุว่าแอปพลิเคชันต้องหลีกเลี่ยง 0-RTT เว้นแต่ protocol ของตนมีข้อกำหนดเรื่องนี้ไว้ชัดเจน ส่วน NIST SP 800-52 Rev. 2 (หัวข้อ 3.6) ระบุว่าโดยทั่วไปเซิร์ฟเวอร์ไม่ควรรับ 0-RTT data และถ้ารับก็ควรใช้ single-use ticket (NIST SP 800-52r2)

เวอร์ชันเก่า และแนวปฏิบัติปัจจุบัน

เอกสาร สาระสำคัญ
RFC 8996 (พ.ศ. 2564) TLS 1.0 และ TLS 1.1 MUST NOT ถูกใช้ เหตุผลรวมถึงความถูกต้องของ handshake ที่ขึ้นกับ SHA-1 และ cipher suite รุ่นเก่าที่ไม่มีตัวเลือก AEAD สมัยใหม่
RFC 9325 (BCP 195, พ.ศ. 2565) ห้ามตกลงใช้ SSL 2.0/3.0 หรือ TLS 1.0/1.1 ต้อง (MUST) รองรับ TLS 1.2 ควร (SHOULD) รองรับ TLS 1.3 และเมื่อรองรับ 1.3 แล้วต้อง (MUST) เลือก 1.3 ก่อน
NIST SP 800-52 Rev. 2 (พ.ศ. 2562) เซิร์ฟเวอร์ TLS ของรัฐบาลสหรัฐฯ ต้องรองรับ TLS 1.2 และต้องรองรับ TLS 1.3 ภายในวันที่ 1 มกราคม 2567 เซิร์ฟเวอร์ที่ให้บริการเฉพาะภาครัฐต้องไม่ใช้ TLS 1.0, SSL 3.0 หรือ SSL 2.0 รายการ TLS 1.3 suite ในเอกสาร (หัวข้อ 3.3.1.2) มีสี่จากห้าตัว โดยไม่มี ChaCha20-Poly1305

NIST SP 800-52 เขียนขึ้นสำหรับระบบของรัฐบาลกลางสหรัฐฯ แต่ตารางในเอกสารใช้เป็น checklist ได้ดีเวลาเขียน decryption profile หรือ SSL/TLS service profile ของเราเองครับ

บันทึกใช้งานจริงสำหรับ decryption policy และการ troubleshoot

การถอดรหัส TLS 1.3 ไฟร์วอลล์ต้องทำตัวเป็นเซิร์ฟเวอร์ต่อ client และเป็น client ต่อเซิร์ฟเวอร์ตัวจริง RFC 8446 กำหนดให้ middlebox ที่ terminate TLS ต้องทำงานเป็น TLS server ที่ถูกต้องตามมาตรฐานต่อ client และเป็น TLS client ที่ถูกต้องตามมาตรฐานต่อเซิร์ฟเวอร์ ซึ่งรวมถึงการตรวจสอบ certificate ของเซิร์ฟเวอร์ตัวจริงด้วย (หัวข้อ 9.3) ต่อไปนี้คือสิ่งที่ผมใช้เตือนตัวเอง โดยยก PAN-OS เป็นตัวอย่างเพราะเอกสารเขียนไว้ชัดครับ

  • ช่วงเวอร์ชัน Palo Alto Networks แนะนำ decryption profile ที่ตั้ง minimum เป็น TLSv1.2 และ maximum เป็น "Max" เพื่อให้รับเวอร์ชันใหม่ได้อัตโนมัติ (TLS 1.3 Decryption)
  • แอปที่ pin certificate จะพัง แอปมือถือจำนวนมาก pin certificate ไว้ หน้าเดียวกันแนะนำให้จำกัด maximum version ไว้ที่ TLSv1.2 สำหรับ rule ที่ครอบคลุมแอปเหล่านั้น หรือไม่ถอดรหัส traffic นั้นเลย
  • ใช้ exclusion สำหรับปัญหาทางเทคนิค certificate pinning, mutual (client-certificate) authentication, certificate chain ที่ไม่ครบ และ cipher ที่ไม่รองรับ คือเหตุผลที่ระบุไว้สำหรับ SSL decryption exclusion list ส่วน traffic ที่เลือกไม่ถอดรหัสด้วยเหตุผลทางธุรกิจ ข้อกำกับ ความเป็นส่วนตัว หรือเหตุผลอื่น ให้ใช้ policy-based exclusion แทน (Decryption Exclusions)
  • QUIC เลี่ยง TLS policy ของเรา QUIC วิ่งบน UDP และไฟร์วอลล์ถอดรหัสไม่ได้ วิธีที่ใช้กันทั่วไปคือบล็อก QUIC application และ UDP 80/443 เพื่อให้เบราว์เซอร์ถอยกลับไปใช้ TLS ซึ่งไฟร์วอลล์ถอดรหัสได้ (Define Traffic to Decrypt)
  • อ่าน capture ตามลำดับ เมื่อเว็บไซต์เข้าไม่ได้เฉพาะตอนผ่านไฟร์วอลล์ ให้ไล่ตาม handshake ClientHello ออกไปพร้อม SNI ที่คาดไว้ไหม มี ServerHello กลับมา หรือได้ alert แทน ถ้า session ถูกถอดรหัส client ได้ certificate ของใคร ของไฟร์วอลล์หรือของเซิร์ฟเวอร์ คำตอบแต่ละข้อชี้ไปคนละชั้น คือ routing และ policy, การตั้งค่า TLS ของเซิร์ฟเวอร์ หรือ decryption profile และ trust chain ของมัน

เมื่ออ่าน handshake ทีละข้อความได้สักสองสามครั้ง การแยกว่า "ปัญหา TLS" ไหนฝั่งเครือข่ายแก้ได้และไหนแก้ไม่ได้ก็จะง่ายขึ้นมากครับ

แหล่งอ้างอิง

อ่านต่อ