14.4.5. 公開サーバーの検証(クライアントとしてのカメラ)

前のページで述べた、クライアントが「すでにルートを持っている」という話は、ブラウザ、スマートフォン、PCには当てはまりますが、カメラには 当てはまりません。MicroPythonの ssl には組み込みのトラストストアが付属しておらず、フラッシュしたばかりのカメラはどのCAも一切信頼していません。さらにデフォルト(ssl.CERT_NONE)では何も検証されず、中間者攻撃に対して無防備な状態です。したがって、カメラが クライアント として公開TLSサーバー(HTTPS API、MQTTブローカーなど)に接続し、そのサーバーを本当に検証したい場合は、トラストアンカーを自分で用意する必要があります。

仕組みは 自己署名証明書 の自己署名クライアントの例と同じです。唯一の違いは、読み込むファイルがピア自身の証明書ではなく実際のCA証明書であるという点です:

  1. サーバーのチェーンを固定するCA証明書を入手します。 「固定する(Anchors)」とは、サーバーのチェーンの最上部(またはその近く)にあり、信頼の起点として選ぶ証明書のことを指します。TLSサーバーはリーフ証明書と通常はその中間証明書を送信しますが、ルート証明書は決して送信しません。このトラストアンカーは自分で、しかも サーバーとは独立に 入手しなければなりません。サーバーが渡してくるものを単純に信頼してしまっては、検証の意味がまったくなくなってしまいます。

    まず、サーバーの証明書を実際に発行したCAがどれかを確認します。例えば openmv.io に対しては次のようにします:

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

    Certificate 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 ページ で公開しており、ISRG Root X1の直接ファイルは isrgrootx1.pem です(DERにあらかじめエンコードされた isrgrootx1.der も提供されています)。他のCAも同様の「ルート証明書」/「リポジトリ」ページで公開しており、標準的な公開セットは Mozilla CAプログラム(CCADB) です。正しいファイルを取得できたかどうかは、そのフィンガープリントをCAが公開している値と比較して確認してください(.der をダウンロードした場合は -inform DER を追加します):

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

    ルートを管理するのが面倒な場合は、代わりに -showcerts の出力から 中間証明書 を直接コピーし(2つ目の -----BEGIN CERTIFICATE----- ブロック)、それを信頼することもできます。ただしその場合、CAが中間証明書をローテーションするたびに更新が必要になることを受け入れる必要があります。これはルートよりもはるかに頻繁に起こります(下記のトレードオフを参照)。

  2. DERに変換します。 これまでと同じ手順です:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. ca.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 と照合される名前になります。

Tip

よくあるケースのショートカット。 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 ごとに1つのDER証明書しか信頼しないため、ブラウザのようにすべてのルートをバンドルすることはできません。迷った場合は、上記のようにサーバーの実際のCAを特定し、そのルートを信頼してください。

どの証明書を信頼するかはトレードオフです:

  • ルート(推奨)。長期間有効で、しばしば数十年に及ぶため、ca.der はめったに変わりません。mbedTLSが リーフ → 中間 → 信頼するルート というパスを構築できるよう、サーバーが中間証明書を送信する必要がありますが、正しく構成された公開サーバーであればほぼ確実にそうしています。

  • 中間証明書。 これも機能し、サーバーが中間証明書を省略した場合でも動作し続けます。ただし、中間証明書はルートよりもはるかに頻繁にローテーションされるため、ca.der をより頻繁に更新する必要があります。

  • リーフ証明書そのもの(証明書ピンニング)。最も厳格ですが、リーフは更新のたびに変わります(Let's Encryptでは約90日ごと)。したがって、これはサーバーも自分で管理しており、すべてのカメラに新しいピンを一斉にプッシュできる場合にのみ意味があります。これはまさに自己署名クライアントの例で行っていることです。

注釈

ssl.SSLContext.load_verify_locations() は1つのDERエンコードされたCA証明書を受け取るため、カメラは一度に正確に1つのアンカーのみを信頼します。異なるCAに属するサーバーに到達するには、アンカーごとに別々の ssl.SSLContext を使用してください。また、その証明書自体もいずれは期限切れになるか、CAによってローテーションされるため、デバイス上の他のすべての証明書と同様に扱ってください。