pipもyt-dlpも「CERTIFICATE_VERIFY_FAILED」で全滅した。原因はNorton(ノートン)で、その証明書を教えたら通った

運営者

僕のWindowsのPCでは、Python から HTTPS につなぐ処理が、軒並み証明書のエラーで止まります。
pip(Python の部品を入れる道具)も、yt-dlp(YouTube の字幕などを取る道具)も同じでした。
エラーには、次の文字が出ます。

CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate

原因は、ウイルス対策ソフトの Norton(ノートン)でした。
Norton が通信の中身を調べるために、途中で証明書を差し替えていたのです。

Norton の証明書を Python 側に教えると、通るようになりました。
pip には、Norton の証明書を足したファイルを --cert で渡しました。
yt-dlp は、--compat-options no-certifi を付けると通っています。

見た目は、ネットにつながらないときのエラーに近いものです。
僕の記録にも、プロキシの設定や部品の版を疑って無駄打ちしやすい、と注意を残しています。

Windows は知っていて、Python は知らない証明書

HTTPS の通信は、TLS という仕組みで暗号化されています。
つなぐ相手が本物かどうかは、相手が出す証明書で確かめます。
その証明書をさかのぼった先にある大元が、ルート証明書です。

Norton の通信スキャンは、相手の証明書を Norton が作ったものに差し替えます。
Norton のルート証明書は、Windows の証明書ストアには入っていました。
証明書ストアは、Windows が信頼する証明書の置き場です。

ところが、僕のPCの pip や requests、yt-dlp は、Windows の置き場ではなく certifi を見ていました。
certifi は、信頼するルート証明書の一覧を、Python の部品として配っているものです。
この一覧に、Norton の証明書は入っていません。
そのため、Python の道具だけが、発行元の証明書が見つからないと言って止まります。

公式の説明でも、requests(Python で通信をするときの定番の部品)は certifi の証明書を使うとあります。
yt-dlp も、certifi が入っていればそれを使う、と書いています。

Norton の証明書の実体は、C:\ProgramData\Norton\Antivirus\wscert.pem にありました。
PEM は、証明書を文字で書き出したファイルの形です。
環境変数(プログラムに外から設定を渡す値)の NODE_EXTRA_CA_CERTS も、同じファイルを指していました。
これは Node.js(JavaScript を動かす道具)に、追加で信頼する証明書を教える設定です。

発行元に Norton の名前が出ていた

Norton が原因だと確かめるには、つないだ相手の証明書の発行元を見ます。
11月10日に、部下役のAIに次の Python を流してもらいました。

import socket, ssl
ctx = ssl.create_default_context()
with socket.create_connection(("pypi.org", 443), timeout=15) as s:
    with ctx.wrap_socket(s, server_hostname="pypi.org") as t:
        print(t.getpeercert()["issuer"])

pypi.org は、pip が部品を取りに行く先です。
発行元には、Norton Web/Mail Shield Root と出ました。
部署の欄には、generated by Norton Antivirus for SSL/TLS scanning とあります。
wscert.pem の中身も、同じ名前でした。

Python 標準の ssl の既定の設定は、Windows では証明書ストアから読み込むと公式に書かれています。
そのため、Norton が差し替えていても、この書き方なら止まらずに発行元まで見られます。

pip と requests には、証明書をまとめたファイルを渡す

9月7日に、certifi の一覧に Norton の証明書を足したファイルを作りました。
requests で、これを使えばつながることも確かめています。

作り方は、certifi の一覧(cacert.pem)の後ろに wscert.pem をつなげるだけです。
PowerShell なら、次のように作れます。

$certifi = python -c "import certifi; print(certifi.where())"
$norton  = "C:\ProgramData\Norton\Antivirus\wscert.pem"
$out     = "$env:USERPROFILE\ca-bundle.pem"
[IO.File]::WriteAllText($out, [IO.File]::ReadAllText($certifi) + "`n" + [IO.File]::ReadAllText($norton))

pip には、--cert でこのファイルを渡します。

python -m pip install <入れたい部品> --cert "$env:USERPROFILE\ca-bundle.pem"

公式の説明では、--cert は pip が使う証明書の置き場を指定するものです。
環境変数の PIP_CERT や REQUESTS_CA_BUNDLE でも渡せる、とあります。

requests を使うスクリプトなら、環境変数の REQUESTS_CA_BUNDLE に同じファイルを入れます。

$env:REQUESTS_CA_BUNDLE = "$env:USERPROFILE\ca-bundle.pem"

部下役のAIに調べものを頼むときも、依頼文にこの指定を書いていました。
Python で HTTPS を使うときはこれを付ける、という一文です。

11月10日にも、何も付けない pip と requests は同じエラーで止まりました。
このファイルを渡すと、どちらも通っています。

まとめたファイルは写しなので、certifi を更新しても中身は古いままです。
もう1つ、pip の版にも気をつけたいところです。
11月10日の時点で、僕のPCの pip は 22.2.1 でした。
公式の説明では、24.2 からは certifi に加えて、システムの証明書も既定で使います。
新しい pip なら、このエラー自体が出ないかもしれません(ここは推測です)。

yt-dlp は no-certifi で Windows の証明書を使わせる

yt-dlp には、証明書のファイルを渡すオプションがありません。
公式の説明書(README)のオプションの一覧にも、見当たりませんでした。

代わりに付けるのが --compat-options no-certifi です。
公式の説明には、certifi が入っていればそれを使い、システムの証明書を使うなら no-certifi を、とあります。
システムの証明書は、Windows なら証明書ストアのことです。
Norton の証明書も、そこに入っています。

python -m yt_dlp --compat-options no-certifi <動画のURL>

8月31日に整えた文字起こしの道具にも、この指定を組み込みました。
11月10日にも、付けないと同じエラーになり、付けると通ることを確かめました。

自分で書くスクリプトは urllib.request に寄せた

9月26日のメモには、requests は証明書エラーになると書いています。
そのため、新しいスクリプトも、それまでと同じく urllib.request で書くことにしました。
urllib.request は、Python に最初から入っている通信の部品です。

独自の設定を渡さずに使うと、Windows の証明書ストアを見に行きます。
声の読み上げや、YouTube の数字を取るスクリプトは、この書き方で通っています。

import urllib.request
with urllib.request.urlopen("https://pypi.org/simple/pip/") as r:
    print(r.status)

11月10日にこの形で試すと、何も足さずに 200(成功)が返りました。

certifi そのものを書き換える手は断られた

certifi の一覧そのものに書き足せば、何も付けずに済みそうに見えます。
一覧の実体は、site-packages(Python の部品が入るフォルダ)の certifi の下にある cacert.pem です。
部下役のAIがこの手を取ろうとしたところ、AIの道具の側で拒否されました。
仮に書き換えられても、certifi を更新すれば上書きされると見ています。

証明書の確認そのものを切る手もあります。
requests の verify=False や、yt-dlp の --no-check-certificates です。
requests の公式の説明には、確認を切るとサーバーが出すどんな証明書も受け入れる、とあります。
11月10日に見たかぎり、僕の道具のコードには、こうした書き方は1つもありませんでした。

winget の msstore も 0x8a15005e で止まった

Norton の差し替えは、winget にも響いていました。
winget は、Windows でアプリを取り寄せて入れるための道具です。

9月13日に Node.js を winget で入れたとき、msstore の証明書エラーが出ています。
msstore は、Microsoft Store のアプリを取り寄せる先です。
--source winget を付けて、取り寄せ先を winget だけに絞ると入りました。

winget install --id Gyan.FFmpeg -e --source winget

これは、僕の動画づくりの道具が FFmpeg(動画を変換する道具)の入れ方として出している案内です。
公式の説明では、--source は探す先を指定した取り寄せ先に絞る指定です。

Codex アプリの不具合を調べていた9月26日の記録にも、msstore が 0x8a15005e で止まると残っています。
そのとき画面には、次の行が出ています。

0x8a15005e : サーバー証明書が、予期された値のいずれにも一致しませんでした。

Codex アプリは、Microsoft Store のアプリから入れ直しました。
証明書の確認を弱める設定には、手を付けていません。

winget の公式の一覧では、0x8A15005E は「サーバーの証明書が、想定したどの値とも一致しなかった」という意味です。
Norton が証明書を差し替えたことで、この照合から外れたと見ています。

参考

Codex アプリを入れ直した日のことは、こちらに書きました。
Codexアプリが丸のまま起動しない(Windows)。裏のapp-serverを止めたら開いた

記事URLをコピーしました