เริ่มจาก TLS
Transport Layer Security (TLS) เป็นโปรโตคอล (นิยามข้อตกลงในการสื่อสาร เพื่อวัตประสงค์บางอย่างที่ตกลงกัน) ที่ออกแบบมาสำหรับการสื่อสารผ่านอินเทอร์เน็ต เพื่อป้องกันการดักฟัง การดัดแปลง และการปลอมแปลงข้อความ ดังนั้นเราจะเป็น TLS จึงถูกนำไปประยุกต์ใช้ได้กับหลาย ๆ กรณี เพื่อปรับปรุงการสื่อสารที่อาจจะมีความเสี่ยงกับการถูกดักฟัง ดัดแปลง และปลอมแปลงข้อความ
หนึ่งในโปรโตคอลที่นำเอา TLS ไปทำงานร่วมกัน และพวกเราคุ้นเคยกันดี ก็คือ HTTP ที่ถูกนำเสนอในเดือน พฤษภาคม 2543 ในหัวข้อ HTTP Over TLS มีรายละเอียดการทำเอา TLS ทำงานร่วมกัน HTTP เพื่อให้การสื่อสารด้วย HTTP มีความปลอดภัยมากขึ้น ในรายละเอียดยังมีการเอ่ยถึงการนิยายม URI ที่จากเดิมที่เป็น http ให้ใช้เป็น https ในกรณีที่ใช้ HTTP Over TLS
Mutual TLS authentication
ถ้าไปหาคำแปลของคำว่า mutual จะเจอว่าหนึ่งในคำแปลก็คือ "ทั้งสองฝ่าย" ... แล้ว authentication มันไปเกี่ยวอะไรกับ TLS ... ถ้าจะเจาะจงไปที่ HTTPs กันไปเลย เวลาที่เราใช้ browser (client) เพื่อร้องขอไปยัง server ด้วย URL ที่เป็น https ... ตอนที่กำลังเริ่มเชื่อมต่อกัน ที่ฝั่ง server จะมีการส่ง server certificate มาให้ จากนั่นที่ฝั่ง browser จะตรวจสอบ signature ใน server certificate ว่าที่ฝั่ง browser (client) รู้จักไหม (ขอไม่ลงรายละเอียดนะครับ ไม่งั้นยาวแน่ ๆ) ถ้าตรวจสอบแล้วว่าไม่รู้จัก browser เองการจะไม่ยอมทำงานต่อ และแสดงความข้อความที่ไม่ยอมเชื่อมต่อกับ server ออกมาเพื่อให้ผู้ใช้ทราบ
ในทางกลับกัน ฝั่ง server ต้องการตรวจสอบฝั่ง browser บ้างล่ะ? ... ถ้าดูรายละเอียดในโปรโตคอล TLS จะเจอว่าที่ฝั่ง server สามารถร้องขอ certificate จากฝั่ง client ได้เหมือนกัน โดยการส่ง "CertificateRequest" กลับมาด้วย ถ้าฝั่ง client ไม่ส่ง client certficate มาให้ หรือ ส่งมาแล้วฝั่ง server ตรวจสอบแล้วไม่รู้จัก ฝั่ง server ก็จะปฎิเสธการเชื่อมต่อ อาจจะส่ง HTTP Code ตามที่กำหนดไว้กลับมาก็ได้
Client Server Key ^ ClientHello Exch | + key_share* | + signature_algorithms* | + psk_key_exchange_modes* v + pre_shared_key* --------> ServerHello ^ Key + key_share* | Exch + pre_shared_key* v {EncryptedExtensions} ^ Server {CertificateRequest*} v Params {Certificate*} ^ {CertificateVerify*} | Auth {Finished} v <-------- [Application Data*] ^ {Certificate*} Auth | {CertificateVerify*} v {Finished} --------> [Application Data] <-------> [Application Data] + Indicates noteworthy extensions sent in the previously noted message. * Indicates optional or situation-dependent messages/extensions that are not always sent. {} Indicates messages protected using keys derived from a [sender]_handshake_traffic_secret. [] Indicates messages protected using keys derived from [sender]_application_traffic_secret_N. Message Flow for Full TLS Handshake
Reference: The Transport Layer Security (TLS) Protocol Version 1.3
ให้ Server ส่ง CertificateRequest มาที่ Client
ในบทความนี้เราจะใช้ NGINX เป็นตัวอย่างในการทดสอบ และเรียนรู้เรื่อง mTLS ... โดยปกติ ที่เราเห็นกับอยู่โดยทั่วไป เราก็มักจะเห็นว่า NGINX จะทำให้หน้าที่รองรับการร้องข้อ HTTP over TLS และจะไม่มีการส่ง "CertificateRequest" กลับมาที่ฝั่ง Client
หากเราต้องการให้ NGINX ถามหา Client Certificate และตรวจสอบ ต้องเพิ่ม configuration ในส่วนที่เกี่ยวข้องกับ TLS ดังนี้
- ssl_verify_client เป็นการกำหนดให้ NGINX ส่ง CertificateRequest และตรวจสอบในเงื่อนไขที่กำหนด โดยปกติหาไม่มีการกำหนดค่า ssl_verify_client จะมีค่าเป็น off ดังนั้น เราจะไม่เห็นการร้องขอ certificate ไปที่ client และหากเราต้องการให้มีการส่ง CertificateRequest ไป ก็ให้กำหนดค่านี้เป็น on ... นอกจากตัวเลือกที่เป็น on และ off แล้วยังไม่อีก 2 ทางเลือก คือ optional ก็เปิดให้เป็นทางเลือกว่าฝั่ง client จะส่ง หรือไม่ส่ง Client Certficate มาก็ได้ แต่จะไปเลือกตรวจสอบที่ตัวแปร ssl_client_verify แทน กับอีกตัวเลือกก็คือ optional_no_ca ก็เปิดให้เป็นทางเลือกว่าฝั่ง client จะส่ง หรือไม่ส่ง Client Certficate และมีตรวจสอบว่า Trusted CA เป็นคน sigend มาหรือไม่ ซึ่งทางฝั่ง server สามารถนำ certificate ไปใช้งานได้ผ่านตัวแปร ssl_client_cert
- ssl_client_certificate เป็นการกำหนดไฟล์ที่เก็บ Trusted CA Certificate เพื่อเอาไว้ตรวจสอบ Client Certificate
ตัวอย่าง NGINX Configuration ที่กำหนดให้ server ร้องขอ Client Certificate
server { listen 443 ssl ; listen [::]:443 ssl ; ssl_certificate /etc/nginx/conf.d/server1.crt; ssl_certificate_key /etc/nginx/conf.d/server1.key; ssl_dhparam /etc/nginx/conf.d/dhparam; error_log /tmp/log debug; ssl_client_certificate /etc/nginx/conf.d/ca.crt; # เลือกกำหนดอย่างได้อย่างหนึ่ง ssl_verify_client {off,on,optional,optional_no_ca}; location / { location / { default_type text/plain; if ($ssl_client_verify != SUCCESS) { return 403 "Unauthorized Access\n"; } return 200 'Welcome to my service'; } }
ข้อมูลเบื้องต้นของระบบที่ใช้ในการทดสอบดังนี้
- NGINX ในรูปแบบของ container โดยใช้ Office NGINX Image จาก Docker Hub ในวันที่ทดสอบเป็น NGINX v1.25.3
- hostname ชื่อว่า server.d8k.dev
- เลือกใช้ ssl_verify_client เป็น optional
- ตรวจสอบผลการ verify ตัว Client Certificate Verification ผ่านตัวแปร $ssl_client_verify
- curl 8.4.0 (x86_64-apple-darwin23.0) บน Macbook Air M2
- ca.key และ ca.crt เป็น Key Pair ที่ทำหน้าที่เป็น Certificate Authority
- server1.key และ server1.crt เป็น Key Pair สำหรับฝั่ง server ที่ถูกกำหนดให้มีคุณสมบัติ TLS Web Server Authentication
- user1.key และ user1.crt เป็น Key Pair สำหรับฝั่ง client ที่ถูกกำหนดให้มีคุณสมบัติ TLS Web Client Authentication
- stranger.key และ stranger.crt เป็น Key Pair คู่เดียวที่ไม่ได้ถูก signed โดย Certificate Authority เพื่อใช้ในการทดสอบฝั่ง client
วิธีการสร้าง key pair
openssl genrsa -out ca.key 4096 openssl req -new -key ca.key -subj "/CN=My CA" -out ca.csr openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt -extfile myca.cnf -extensions v3_ca openssl genrsa -out server1.key 4096 openssl req -new -key server1.key -subj "/CN=server1" -out server1.csr echo subjectAltName = DNS:localhost,DNS:server.d8k.dev,IP:192.168.254.108,IP:127.0.0.1 >> server1.cnf echo extendedKeyUsage = serverAuth >> server1.cnf openssl x509 -req -in server1.csr -CAkey ca.key -CA ca.crt -out server1.crt -CAcreateserial -extfile server1.cnf openssl genrsa -out user1.key 4096 openssl req -new -key user1.key -subj "/CN=user1" -out user1.csr echo extendedKeyUsage = clientAuth >> user1.cnf openssl x509 -req -in user1.csr -CAkey ca.key -CA ca.crt -out user1.crt -CAcreateserial -extfile user1.cnf openssl genrsa -out stranger.key 4096 openssl req -new -key stranger.key -subj "/CN=stranger" -out stranger.csr openssl x509 -req -in stranger.csr -signkey stranger.key -out stranger.crt openssl dhparam -out dhparam 2048
ที่ฝั่ง client ใช้คำสั่ง curl ในการทดสอบ
curl เป็นเครื่องมือที่ทำหน้าที่เป็น client เพื่อร้องขอไปยัง server ปลายทางที่รองร้บ protocol ที่หลากหลาย ในกรณีของเรา เราจะใช้ curl ร้องขอไปยัง server ปลายที่ ด้วย HTTP over TLS หรือ https แล้วให้ส่ง Client Certification ไปด้วย
คำสั่ง curl ที่เรียกใช้ option ในการทดสอบดังนี้
- --cacert <file> เพื่อบอกกับ curl ว่าให้ใช้ certificate ตัวนี้ในการตรวจสอบ Service Certificate ดังนั้นในกรณีที่ฝั่ง server เป็น Self Signed Certificate อย่างในกรณีที่จะทดสอบ เพื่อให้ curl ตรวจสอบ certificate ได้ จึงต้องกำหนดค่า cacert
- --cert <certificate[:password]> เพื่อกำหนดไฟล์ Client Certficate เมื่อมีการร้องขอจาก server
- --key <key> เพื่อกำหนดไฟล์ Private Key ที่คู่กับ Client Certificate
หลังจากที่สั่งให้ NGINX เริ่มทำงาน โดยกำหนด ให้ ssl_verify_client optional; แล้ว และ กำหนด ไฟล์ CA Certficate ที่ siged ให้กับ user1.crt ให้กับ ssl_client_certificate แล้ว มาลองดูผลการทำงานของ NGINX ที่ทดสอบด้วย curl ดังนี้
- ทดสอบโดยเรียกคำสั่ง curl https://server.d8k.dev
- ทดสอบโดยเรียกคำสั่ง curl --cacert ca.crt https://server.d8k.dev เพื่อกำหนดให้ curl ใช้ไฟล์ ca.crt ตรวจสอบ Server Certificate
- ทดสอบโดยเรียกคำสั่ง curt https://server.d8k.dev พร้อม option --cacert, --key และ --cert โดยใช้ key pair ที่ถูก signed โดย CA
- ทดสอบโดยเรียกคำสั่ง curl https://server.d8k.dev พร้อม option --cacert, --key และ --cert โดยใช้ key pair ที่ไม่ได้ถูก signed โดย CA
> curl https://server.d8k.dev curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the web page mentioned above.
> curl --cacert ./ca.crt https://server.d8k.dev Unauthorized Access
> curl --cacert ./ca.crt --key user1.key --cert user1.crt https://server.d8k.dev Welcome to my service
> curl --cacert ./ca.crt --key stranger.key --cert stranger.crt https://server.d8k.dev <html> <head><title>400 The SSL certificate error</title></head> <body> <center><h1>400 Bad Request</h1></center> <center>The SSL certificate error</center> <hr><center>nginx/1.25.3</center> </body> </html>
Reference:
เก็บตก นิดหน่อย
สำหรับท่านที่สนใจ ศึกษาเรื่อง TLS แบบจริงจัง อยากเห็นว่าในแต่ละขั้นนตอนในการเชื่อมต่อ TLS ใน HTTPS เป็นอย่างไร แนะนำคำสั่งนี้เลยครับ
> echo -e "GET /\n" | openssl s_client -key user1.key -cert user1.crt -connect server.d8k.dev:443 -CAfile ca.crt -state -ign_eof Connecting to 192.168.254.108 CONNECTED(00000006) SSL_connect:before SSL initialization SSL_connect:SSLv3/TLS write client hello SSL_connect:SSLv3/TLS write client hello SSL_connect:SSLv3/TLS read server hello SSL_connect:TLSv1.3 read encrypted extensions SSL_connect:SSLv3/TLS read server certificate request depth=1 CN=My CA verify return:1 depth=0 CN=server.d8k.dev verify return:1 SSL_connect:SSLv3/TLS read server certificate SSL_connect:TLSv1.3 read server certificate verify SSL_connect:SSLv3/TLS read finished SSL_connect:SSLv3/TLS write change cipher spec SSL_connect:SSLv3/TLS write client certificate SSL_connect:SSLv3/TLS write certificate verify SSL_connect:SSLv3/TLS write finished --- Certificate chain 0 s:CN=server.d8k.dev i:CN=My CA a:PKEY: rsaEncryption, 4096 (bit); sigalg: RSA-SHA256 v:NotBefore: Dec 29 19:58:31 2023 GMT; NotAfter: Jan 28 19:58:31 2024 GMT --- Server certificate -----BEGIN CERTIFICATE----- MIIFQjCCAyqgAwIBAgIUBmayoWOT0D+a4dczHLVqGBJI04IwDQYJKoZIhvcNAQEL BQAwEDEOMAwGA1UEAwwFTXkgQ0EwHhcNMjMxMjI5MTk1ODMxWhcNMjQwMTI4MTk1 [... truncated output ...] VtRYR6ndfVMDbWR1aC4DeQAIgUBO0Z7QVZDQ3fd5ienbqoxVgtTkyuHcurzseqE4 Boq7zjrOWaXrvsq3ZUWu9AvIXTyKqxte0B9V2Cxcp+nkKEJC93oRr84vjCqv/oLi zT++Zm0E -----END CERTIFICATE----- subject=CN=server.d8k.dev issuer=CN=My CA --- Acceptable client certificate CA names CN=My CA Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224 Shared Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512 Peer signing digest: SHA256 Peer signature type: RSA-PSS Server Temp Key: X25519, 253 bits --- SSL handshake has read 2259 bytes and written 3553 bytes Verification: OK --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Server public key is 4096 bit This TLS version forbids renegotiation. Compression: NONE Expansion: NONE No ALPN negotiated Early data was not sent Verify return code: 0 (ok) --- SSL_connect:SSL negotiation finished successfully SSL_connect:SSL negotiation finished successfully --- Post-Handshake New Session Ticket arrived: SSL-Session: Protocol : TLSv1.3 Cipher : TLS_AES_256_GCM_SHA384 Session-ID: ABFFBB7B7AA8ECFB39D8941239643B1101E6A456DEA12F8A3BF7AA94E9F430F0 Session-ID-ctx: Resumption PSK: C55FBA0997BBEBECC8A2A9237F2A4F562DB882C18B7BC816B807F349D425B6F943F7A16947ABFFC021435153217B752A PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 300 (seconds) TLS session ticket: 0000 - ce 41 16 79 dc c4 df 7b-dc 1e 09 e9 ef fe e6 37 .A.y...{.......7 0010 - 4e f3 5e 23 c1 21 3a 10-1e c1 40 92 d7 75 01 3c N.^#.!:...@..u.< [... truncated output ...] 05e0 - a9 a2 75 32 74 d4 57 b8-af 3c a2 fd 24 c6 35 b2 ..u2t.W..<..$.5. 05f0 - 97 6e 9a 63 8b 14 1c dd-dc 35 ab 9d bf 62 ee 8c .n.c.....5...b.. Start Time: 1704005301 Timeout : 7200 (sec) Verify return code: 0 (ok) Extended master secret: no Max Early Data: 0 --- SSL_connect:SSLv3/TLS read server session ticket read R BLOCK SSL_connect:SSL negotiation finished successfully SSL_connect:SSL negotiation finished successfully --- Post-Handshake New Session Ticket arrived: SSL-Session: Protocol : TLSv1.3 Cipher : TLS_AES_256_GCM_SHA384 Session-ID: 70FB69D829601BA65B2C7C200E28612F6D64210904DF26DD3F7D0D5831BC1E6F Session-ID-ctx: Resumption PSK: 37FC3E74B70A1B9500E7CCE70D9B1A6DDC3B7F11ECA6A3D32BDCB90C2F43856794C33305653DF50A3FA87AB1946C7586 PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 300 (seconds) TLS session ticket: 0000 - ce 41 16 79 dc c4 df 7b-dc 1e 09 e9 ef fe e6 37 .A.y...{.......7 0010 - a3 c2 ac 6f 2b 16 76 f1-6d 6f 38 b4 86 93 68 85 ...o+.v.mo8...h. [... truncated output ...] 05e0 - 6b 01 9f 77 f0 a4 d1 f0-88 75 ac 58 e7 ed 8a fb k..w.....u.X.... 05f0 - be 56 0f 3a 2e f9 f4 a8-58 c2 0d ff 1a 16 29 68 .V.:....X.....)h Start Time: 1704005301 Timeout : 7200 (sec) Verify return code: 0 (ok) Extended master secret: no Max Early Data: 0 --- SSL_connect:SSLv3/TLS read server session ticket read R BLOCK Welcome to my service SSL3 alert read:warning:close notify closed SSL3 alert write:warning:close notify