Verifying a public server (camera as client) ============================================ Everything on the previous page about a client "already having the root" is true of browsers, phones and PCs -- it is **not** true of the camera. MicroPython's :mod:`ssl` ships with no built-in trust store: a freshly flashed camera trusts no CA at all, and the default (:data:`ssl.CERT_NONE`) verifies nothing and is wide open to a man-in-the-middle. So when the camera is the *client* connecting out to a public TLS server (an HTTPS API, an MQTT broker, ...) and you want it to truly verify that server, you have to supply the trust anchor yourself. The mechanics are the same as the self-signed client example on :doc:`self-signed`; the only difference is that the file you load is a real CA certificate instead of the peer's own certificate: #. **Get the CA certificate that anchors the server's chain.** "Anchors" means the certificate at (or near) the top of the server's chain that you choose as your starting point of trust. A TLS server sends its leaf and usually its intermediate(s); it never sends its root. You must obtain that trust anchor yourself and *independently of the server* -- simply trusting whatever a server hands you would defeat the entire point of verification. First find out which CA actually issued the server's certificate. For example, against ``openmv.io``:: openssl s_client -connect openmv.io:443 -showcerts < /dev/null The ``Certificate chain`` block lists each certificate with its subject (``s:``) and issuer (``i:``); newer OpenSSL also prints ``a:`` (key type) and ``v:`` (validity) lines you can ignore here:: 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 is the leaf (``openmv.io``), issued by the intermediate ``E8``. Entry 1 is that intermediate, issued by the root ``ISRG Root X1``. The issuer (``i:``) of the topmost entry names the root -- here ``ISRG Root X1``. (The intermediate is ``E8`` rather than the ``R10`` / ``R11`` you may have seen elsewhere because ``openmv.io`` uses an ECDSA certificate; Let's Encrypt signs ECDSA leaves with its ``E``-series intermediates and RSA leaves with its ``R``-series ones. Both chain to ``ISRG Root X1``.) OpenSSL also prints ``depth=`` lines and may report the root with ``Verification: OK``. That happens only because *your PC* already trusts ``ISRG Root X1`` -- the server did **not** send it (a server never sends its root), and the camera, having no trust store, will not have it either. That is exactly why you must supply it. Download *that* root from the CA's own published roots. Let's Encrypt catalogues all of theirs on the `Let's Encrypt certificates page `__; the direct file for ISRG Root X1 is `isrgrootx1.pem `__ (they also offer it pre-encoded as `isrgrootx1.der `__, which lets you skip the DER conversion in the next step). Other CAs publish theirs on a similar "root certificates" / "repository" page; the canonical public set is the `Mozilla CA program (CCADB) `__. Confirm you fetched the right file by comparing its fingerprint against the value the CA publishes (add ``-inform DER`` if you downloaded the ``.der``):: openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256 If you would rather not track a root, you can instead copy the **intermediate** straight out of the ``-showcerts`` output (the second ``-----BEGIN CERTIFICATE-----`` block), trust that, and accept that you must refresh it whenever the CA rotates the intermediate -- far more often than the root (see the trade-off below). #. **Convert it to DER**, exactly as before:: openssl x509 -in isrgrootx1.pem -outform DER -out ca.der #. **Copy** ``ca.der`` to the camera (filesystem or ROMFS) and load it as the 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`` is required here: it drives SNI and is the name checked against the server certificate's ``subjectAltName``. .. tip:: **Common-case shortcut.** Let's Encrypt is the most widely used public CA, and both its RSA and ECDSA certificates currently chain to ISRG Root X1 (as the ``openmv.io`` example above shows). If the servers your camera talks to use Let's Encrypt, you can skip the inspection entirely: just put `isrgrootx1.der `__ on the camera and ``load_verify_locations`` it. This does **not** make TLS work to *every* site. A server whose certificate comes from a different CA (DigiCert, Google Trust Services, Amazon, Sectigo, ...) will still fail verification, and because the camera trusts a single DER certificate per :class:`ssl.SSLContext` you cannot bundle every root the way a browser does. When in doubt, identify the server's actual CA as shown above and trust that root. Which certificate you trust is a trade-off: * **The root** (recommended). Long-lived -- often decades -- so ``ca.der`` rarely changes. It requires the server to send its intermediate so mbedTLS can build the path leaf → intermediate → your trusted root; virtually every correctly configured public server does. * **The intermediate.** Also works, and keeps working even if a server omits the intermediate, but intermediates are rotated far more often than roots, so you will have to refresh ``ca.der`` more frequently. * **The leaf itself** (certificate pinning). Tightest, but the leaf changes on every renewal -- roughly every 90 days for Let's Encrypt -- so this only makes sense when you also control the server and can push the new pin to every camera in lockstep. This is exactly what the self-signed client example does. .. note:: :meth:`ssl.SSLContext.load_verify_locations` takes a single DER-encoded CA certificate, so the camera trusts exactly one anchor at a time. To reach servers under different CAs, use a separate :class:`ssl.SSLContext` per anchor. And because that certificate will itself eventually expire or be rotated by the CA, treat it like any other certificate on the device -- see :doc:`operations`.