14.4.5. 공개 서버 검증(클라이언트로서의 카메라)¶
이전 페이지에서 클라이언트가 “이미 루트를 가지고 있다”고 한 모든 내용은 브라우저, 휴대폰, PC에는 해당되지만 카메라에는 해당되지 않습니다. MicroPython의 ssl 에는 내장 신뢰 저장소가 전혀 없습니다. 갓 플래시된 카메라는 어떤 CA도 신뢰하지 않으며, 기본값(ssl.CERT_NONE)은 아무것도 검증하지 않아 중간자 공격에 완전히 노출되어 있습니다. 따라서 카메라가 공개 TLS 서버(HTTPS API, MQTT 브로커 등)로 나가는 연결을 맺는 클라이언트 역할을 할 때, 그 서버를 진정으로 검증하려면 신뢰 앵커를 직접 제공해야 합니다.
동작 원리는 자체 서명 인증서 의 자체 서명 클라이언트 예제와 동일합니다. 유일한 차이는 로드하는 파일이 상대방 자신의 인증서가 아니라 실제 CA 인증서라는 점입니다:
서버 체인을 고정하는 CA 인증서를 구하십시오. “고정”이란 신뢰의 출발점으로 선택하는, 서버 체인의 최상위(또는 그 근처)에 있는 인증서를 의미합니다. TLS 서버는 자신의 리프와 보통 중간 인증서를 보내지만, 루트는 절대 보내지 않습니다. 그 신뢰 앵커는 서버와는 독립적으로 직접 구해야 합니다. 서버가 건네주는 것을 그대로 신뢰한다면 검증의 의미 자체가 무너지기 때문입니다.
먼저 어떤 CA가 실제로 서버의 인증서를 발급했는지 확인하십시오. 예를 들어
openmv.io에 대해:openssl s_client -connect openmv.io:443 -showcerts < /dev/nullCertificate chain블록은 각 인증서를 그 주체(s:)와 발급자(i:)와 함께 나열합니다. 최신 OpenSSL은a:(키 유형)과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
항목 0은 리프(
openmv.io)로, 중간 인증서E8에 의해 발급되었습니다. 항목 1은 그 중간 인증서로, 루트ISRG Root X1에 의해 발급되었습니다. 최상위 항목의 발급자(i:)가 루트의 이름을 나타냅니다. 여기서는ISRG Root X1입니다. (중간 인증서가 다른 곳에서 보았을 수 있는R10/R11이 아니라E8인 이유는openmv.io가 ECDSA 인증서를 사용하기 때문입니다. Let’s Encrypt는 ECDSA 리프를E계열 중간 인증서로, RSA 리프를R계열 중간 인증서로 서명합니다. 둘 다ISRG Root X1로 체인이 이어집니다.)OpenSSL은
depth=줄도 출력하며 루트를Verification: OK로 보고할 수 있습니다. 이는 단지 여러분의 PC가 이미ISRG Root X1을 신뢰하기 때문에 발생하는 것이며, 서버는 그것을 보내지 않았습니다(서버는 결코 자신의 루트를 보내지 않습니다). 또한 트러스트 스토어가 없는 카메라 역시 그것을 가지고 있지 않을 것입니다. 바로 이것이 여러분이 직접 그것을 제공해야 하는 이유입니다.그 루트를 CA가 직접 게시한 루트 목록에서 다운로드하십시오. Let’s Encrypt는 자신의 모든 루트를 Let’s Encrypt certificates page 에 정리해 두었습니다. ISRG Root X1의 직접 파일은 isrgrootx1.pem 입니다(미리 인코딩된 isrgrootx1.der 형태로도 제공됩니다). 다른 CA들도 유사한 “root certificates” / “repository” 페이지에 자신의 루트를 게시합니다. 표준적인 공개 집합은 Mozilla CA program (CCADB) 입니다. 올바른 파일을 받았는지는 그 지문을 CA가 게시한 값과 비교하여 확인하십시오(
.der를 다운로드했다면-inform DER을 추가하십시오):openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256루트를 관리하고 싶지 않다면, 대신
-showcerts출력에서 중간 인증서(두 번째-----BEGIN CERTIFICATE-----블록)를 그대로 복사하여 그것을 신뢰할 수 있습니다. 다만 CA가 중간 인증서를 교체할 때마다 루트보다 훨씬 더 자주 이를 갱신해야 한다는 점을 감수해야 합니다(아래의 절충 사항 참조).이전과 똑같이 DER로 변환하십시오
openssl x509 -in isrgrootx1.pem -outform DER -out ca.derca.der을 카메라(파일 시스템 또는 ROMFS)로 복사하고 신뢰 앵커로 로드하십시오: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과 대조하여 확인되는 이름입니다.
팁
일반적인 경우의 지름길. Let’s Encrypt는 가장 널리 사용되는 공개 CA이며, 현재 RSA와 ECDSA 인증서 모두 ISRG Root X1로 체인이 이어집니다(위의 openmv.io 예제에서 보듯이). 카메라가 통신하는 서버들이 Let’s Encrypt를 사용한다면 검사를 완전히 건너뛸 수 있습니다. 그냥 isrgrootx1.der 을 카메라에 넣고 load_verify_locations 하면 됩니다.
이렇게 한다고 모든 사이트에 대해 TLS가 동작하는 것은 아닙니다. 인증서가 다른 CA(DigiCert, Google Trust Services, Amazon, Sectigo 등)에서 발급된 서버는 여전히 검증에 실패하며, 카메라는 ssl.SSLContext 당 단일 DER 인증서만 신뢰하므로 브라우저처럼 모든 루트를 묶어서 넣을 수는 없습니다. 확실하지 않을 때는 위에서 설명한 대로 서버의 실제 CA를 파악하고 그 루트를 신뢰하십시오.
어떤 인증서를 신뢰할지는 절충의 문제입니다:
루트(권장). 수명이 길어서 – 종종 수십 년 –
ca.der은 거의 바뀌지 않습니다. 이 방식은 mbedTLS가 리프 → 중간 → 신뢰하는 루트로 경로를 구성할 수 있도록 서버가 중간 인증서를 보내야 하는데, 올바르게 구성된 거의 모든 공개 서버가 그렇게 합니다.중간 인증서. 이 또한 동작하며 서버가 중간 인증서를 생략하더라도 계속 동작하지만, 중간 인증서는 루트보다 훨씬 더 자주 교체되므로
ca.der을 더 자주 갱신해야 합니다.리프 자체(인증서 피닝). 가장 엄격하지만, 리프는 갱신할 때마다 – Let’s Encrypt의 경우 대략 90일마다 – 바뀌므로, 이 방식은 서버도 직접 제어하면서 새 핀을 모든 카메라에 동시에 푸시할 수 있을 때에만 의미가 있습니다. 자체 서명 클라이언트 예제가 바로 이렇게 합니다.
참고
ssl.SSLContext.load_verify_locations() 는 단일 DER 인코딩 CA 인증서를 받으므로 카메라는 한 번에 정확히 하나의 앵커만 신뢰합니다. 서로 다른 CA에 속한 서버에 도달하려면 앵커마다 별도의 ssl.SSLContext 를 사용하십시오. 그리고 그 인증서 자체도 결국에는 만료되거나 CA에 의해 교체되므로, 장치에 있는 다른 모든 인증서와 마찬가지로 취급하십시오.