14.4.5. การตรวจสอบเซิร์ฟเวอร์สาธารณะ (กล้องทำหน้าที่เป็น client)

ทุกสิ่งที่กล่าวถึงในหน้าก่อนหน้าเกี่ยวกับ client "ที่มี root อยู่แล้ว" เป็นความจริงสำหรับเบราว์เซอร์ โทรศัพท์ และ PC -- แต่ ไม่ เป็นความจริงสำหรับกล้อง ssl ของ MicroPython ไม่มี trust store ในตัว: กล้องที่เพิ่งแฟลชใหม่จะไม่เชื่อถือ CA ใดเลย และค่าเริ่มต้น (ssl.CERT_NONE) จะไม่ตรวจสอบสิ่งใดและเปิดช่องโหว่ man-in-the-middle ไว้อย่างสมบูรณ์ ดังนั้นเมื่อกล้องเป็น client ที่เชื่อมต่อออกไปยัง TLS server สาธารณะ (HTTPS API, MQTT broker, ...) และคุณต้องการให้ตรวจสอบ server นั้นจริงๆ คุณต้องจัดหา trust anchor เองด้วยตัวเอง

กลไกเหมือนกับตัวอย่าง self-signed client ใน ใบรับรอง Self-signed ทั้งหมด โดยมีความแตกต่างเพียงอย่างเดียวคือไฟล์ที่คุณโหลดเป็น CA certificate จริงแทนที่จะเป็น certificate ของ peer เอง:

  1. รับ CA certificate ที่เป็น anchor สำหรับ chain ของ server "Anchor" หมายถึง certificate ที่อยู่บน (หรือใกล้) ยอดของ chain ของ server ที่คุณเลือกเป็นจุดเริ่มต้นของความเชื่อถือ TLS server จะส่ง leaf และมักจะส่ง intermediate(s) ด้วย แต่จะไม่ส่ง root คุณต้องขอ trust anchor นั้นด้วยตัวเองและ โดยไม่ขึ้นกับ server -- การเชื่อถือสิ่งที่ server ส่งมาให้โดยตรงจะทำลายจุดประสงค์ทั้งหมดของการตรวจสอบ

    ขั้นแรก ให้ค้นหาว่า CA ใดที่ออก certificate ของ server จริงๆ ตัวอย่างเช่น สำหรับ openmv.io

    openssl s_client -connect openmv.io:443 -showcerts < /dev/null
    

    บล็อก Certificate chain แสดงรายการ certificate แต่ละรายการพร้อม subject (s:) และ issuer (i:); OpenSSL เวอร์ชันใหม่กว่าจะพิมพ์บรรทัด a: (ประเภท key) และ v: (ความถูกต้อง) ที่คุณสามารถเพิกเฉยได้ที่นี่:

    Certificate chain
     0 s:CN=openmv.io
       i:C=US, O=Let's Encrypt, CN=E8
     1 s:C=US, O=Let's Encrypt, CN=E8
       i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
    

    Entry 0 คือ leaf (openmv.io) ออกโดย intermediate E8 Entry 1 คือ intermediate นั้น ออกโดย root ISRG Root X1 issuer (i:) ของ entry บนสุดชี้ไปที่ root -- ที่นี่คือ ISRG Root X1 (intermediate คือ E8 แทนที่จะเป็น R10 / R11 ที่คุณอาจเคยเห็นที่อื่น เนื่องจาก openmv.io ใช้ ECDSA certificate; Let's Encrypt จะเซ็น ECDSA leaf ด้วย intermediate ชุด E และ RSA leaf ด้วยชุด R ทั้งคู่ chain ไปที่ ISRG Root X1)

    OpenSSL ยังพิมพ์บรรทัด depth= และอาจรายงาน root ด้วย Verification: OK สิ่งนี้เกิดขึ้นเพราะ PC ของคุณ เชื่อถือ ISRG Root X1 อยู่แล้ว -- server ไม่ได้ ส่งมันมา (server ไม่เคยส่ง root ของตน) และกล้องซึ่งไม่มี trust store ก็จะไม่มีมันเช่นกัน นั่นคือเหตุผลที่คุณต้องจัดหามันเอง

    ดาวน์โหลด root นั้น จากหน้า root ที่ CA เผยแพร่เอง Let's Encrypt รวบรวม root ทั้งหมดไว้ใน หน้า certificate ของ Let's Encrypt; ไฟล์โดยตรงสำหรับ ISRG Root X1 คือ isrgrootx1.pem (พวกเขายังเสนอเวอร์ชันที่เข้ารหัสล่วงหน้าเป็น isrgrootx1.der) CA อื่นๆ เผยแพร่ root ของตนบนหน้า "root certificates" / "repository" ที่คล้ายกัน ชุด public ที่เป็นแม่แบบคือ Mozilla CA program (CCADB) ยืนยันว่าคุณดาวน์โหลดไฟล์ถูกต้องโดยเปรียบเทียบ fingerprint กับค่าที่ CA เผยแพร่ (เพิ่ม -inform DER หากคุณดาวน์โหลด .der):

    openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256
    

    หากคุณไม่ต้องการติดตาม root คุณสามารถคัดลอก intermediate โดยตรงจาก output ของ -showcerts (บล็อก -----BEGIN CERTIFICATE----- ที่สอง) ไว้ใช้เป็น trust แล้วยอมรับว่าคุณต้องรีเฟรชเมื่อ CA หมุนเวียน intermediate -- บ่อยกว่า root มาก (ดูการแลกเปลี่ยนด้านล่าง)

  2. แปลงเป็น DER เหมือนเดิม:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. คัดลอก ca.der ไปยังกล้อง (filesystem หรือ ROMFS) แล้วโหลดเป็น trust anchor:

    import socket
    import ssl
    import ntptime
    
    ntptime.settime()                  # validity check needs the clock
    
    addr = socket.getaddrinfo("api.example.com", 443)[0][-1]
    sock = socket.socket()
    sock.connect(addr)
    
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.verify_mode = ssl.CERT_REQUIRED
    ctx.load_verify_locations(cafile="ca.der")
    ssock = ctx.wrap_socket(sock, server_hostname="api.example.com")
    

    server_hostname จำเป็นที่นี่: มันขับเคลื่อน SNI และเป็นชื่อที่ตรวจสอบกับ subjectAltName ของ server certificate

Tip

ทางลัดสำหรับกรณีทั่วไป Let's Encrypt เป็น public CA ที่ใช้งานมากที่สุด และทั้ง RSA และ ECDSA certificate ของ Let's Encrypt ในปัจจุบัน chain ไปที่ ISRG Root X1 (ดังที่ตัวอย่าง openmv.io ด้านบนแสดง) หาก server ที่กล้องของคุณพูดคุยด้วยใช้ Let's Encrypt คุณสามารถข้ามการตรวจสอบทั้งหมดได้: เพียงวาง isrgrootx1.der บนกล้องแล้ว load_verify_locations มัน

สิ่งนี้ ไม่ได้ ทำให้ TLS ใช้งานได้กับ ทุก ไซต์ server ที่ certificate มาจาก CA อื่น (DigiCert, Google Trust Services, Amazon, Sectigo, ...) จะยังล้มเหลวในการตรวจสอบ และเนื่องจากกล้องเชื่อถือ DER certificate เดียวต่อ ssl.SSLContext คุณไม่สามารถรวม root ทุกตัวเหมือนเบราว์เซอร์ได้ เมื่อไม่แน่ใจ ให้ระบุ CA จริงของ server ตามที่แสดงด้านบนและเชื่อถือ root นั้น

การเลือก certificate ที่จะเชื่อถือเป็นการแลกเปลี่ยน:

  • Root (แนะนำ) มีอายุยาวนาน -- มักเป็นหลายสิบปี -- ดังนั้น ca.der จะแทบไม่เปลี่ยนแปลง จำเป็นต้องให้ server ส่ง intermediate เพื่อให้ mbedTLS สร้าง path leaf → intermediate → trusted root ของคุณ ซึ่ง server สาธารณะที่กำหนดค่าถูกต้องแทบทั้งหมดทำ

  • Intermediate ก็ใช้ได้เช่นกัน และยังคงทำงานได้แม้ว่า server จะไม่ส่ง intermediate แต่ intermediate จะถูกหมุนเวียนบ่อยกว่า root มาก ดังนั้นคุณจะต้องรีเฟรช ca.der บ่อยขึ้น

  • Leaf เอง (certificate pinning) เข้มงวดที่สุด แต่ leaf จะเปลี่ยนในทุก renewal -- ประมาณทุก 90 วันสำหรับ Let's Encrypt -- ดังนั้นสิ่งนี้จึงสมเหตุสมผลเฉพาะเมื่อคุณควบคุม server ด้วยและสามารถส่ง pin ใหม่ไปยังกล้องทุกตัวพร้อมกันได้ นี่คือสิ่งที่ตัวอย่าง self-signed client ทำ

Note

ssl.SSLContext.load_verify_locations() รับ CA certificate ที่เข้ารหัส DER เพียงหนึ่งใบ ดังนั้นกล้องจึงเชื่อถือ anchor เดียวในเวลาเดียวกัน เพื่อเข้าถึง server ภายใต้ CA ที่แตกต่างกัน ให้ใช้ ssl.SSLContext แยกต่างหากต่อ anchor และเนื่องจาก certificate นั้นจะหมดอายุหรือถูกหมุนเวียนโดย CA ในที่สุด ให้ถือปฏิบัติเหมือน certificate อื่นๆ บนอุปกรณ์