SSL(暗号化通信と証明書検証の仕組み)エラーだと思ったら、引数の型が違っていた。HTTPS調査で切り分けた日

SSL(暗号化通信と証明書検証の仕組み)エラーだと思ったら、引数の型が違っていた。HTTPS調査で切り分けた日

「HTTPSで取りに行くと、証明書検証に失敗する」

しろまえ殿は、外部サイトの取得処理で起きたエラーを見ていた。

最初は、証明書の問題に見えた。

だが、実際には一つの原因ではなかった。

証明書の束が足りない場合と、呼び出し方そのものが間違っている場合が重なっていた。

この二つを分けて見ないと、修正しても別の場所でまた止まる。

まず、TLS(通信を暗号化し、相手を確認する仕組み)を切らない

TLSは、通信相手が本物かを確認する仕組みだ。

ここで verify=False を足せば、検証を止めて通信だけ通すことはできる。

しかし、それは家の鍵が合わないから鍵穴を壊すようなものだ。

原因は隠れる。

安全も落ちる。

しろまえ殿が先に確認したのは、検証を無効にしていない通常のHTTPS接続だった。

それで接続できる場所と、失敗する場所を分けた。

この時点で、すべてを「証明書が悪い」と決めつけないことが大切になる。

証明書の束を、必要な通信だけに渡す

実行環境によっては、OSに入っているCAバンドル(信頼できる認証局の証明書集)に、相手の証明書チェーンを確認するためのルートが入っていないことがある。

この場合は、Mozillaの認証局一覧を収録した certifi(Pythonで使える証明書一覧)のバンドルを使える。

ただし、全体の通信設定を乱暴に変えるのではない。

requests.Session に検証用のバンドルを設定し、そのセッションを該当するリクエストへ渡す。

たとえば、病院の受付で使う身分証の確認先だけを正しい窓口に変えるようなものだ。

建物全体の警備を外す必要はない。

これで、CAバンドル不足という原因には、検証を保ったまま対処できる。

SSLの文字に引っ張られた

別の取得処理では、共有ヘルパーに verify という引数を渡していた。

ところが、そのヘルパーの定義には、その名前の引数がなかった。

発生していたのはSSLの検証失敗ではない。

Pythonの TypeError だった。

「SSLエラー」と見えても、実際の例外名と、呼び出し先の関数定義は確認する。

エラーメッセージに出てくる単語だけで犯人を決めない。

ここを直すため、共有ヘルパーを無理に変えず、既存のセッション引数を使う形へ呼び出し側を合わせた。

共通部品を一つ直して全サイトへ影響を広げるより、必要な通信だけを直す方が変更範囲を小さくできる。

失敗時の戻り値も確認する

もう一つ、見落としやすい罠があった。

共有ヘルパーは、リクエストに失敗したとき、結果の辞書ではなく False を返す仕様だった。

呼び出し側がそのまま response["response_body"] のように読むと、今度は「真偽値を添字で読めない」という別の例外になる。

たとえば、宅配便が届かなかったときに、不在票ではなく荷物そのものが届いた前提で箱を開けようとするようなものだ。

まず戻り値が成功形式かを確かめる。

失敗なら、そこで安全に処理を止める。

通信エラーと、失敗結果の扱いを分けておくと、後続の例外も減らせる。

一つのエラーを、三つの確認に分ける

しろまえ殿がこの調査で分けた確認は、次の三つだった。

1. TLS検証を無効にせず、CAバンドルの不足だけを補えるか。 2. 呼び出し側のキーワード引数が、関数定義と一致しているか。 3. 通信失敗時の戻り値を、呼び出し側が型確認しているか。

この順番なら、「証明書を直したのにまだ500になる」という状態でも、次の原因へ進める。

逆に、最初から検証を無効にしたり、共有ヘルパーの仕様を推測したりすると、問題の輪郭がぼやける。

しろまえ殿は、SSLという大きな看板の裏側を分解した。

正直、この看板だけを見ていた時間は長かった。

それでも、通信の安全を落とさず、例外の型まで確認していくと、修正箇所は小さくできた。

外部サイトとのHTTPS通信で止まったときは、まず検証を切らない。

証明書、引数、戻り値を一つずつ見る。

この順番を守るだけで、次の切り分けはかなり楽になるはずだ。