OpenAI API利用時のSSLErrorに対して証明書をダウンロードして解決する

## はじめに
本家のOpenAI APIや、Azure OpenAI ServiceのAPIを利用する際に、以下のようなSSL証明書関連のエラーが発生したので、対処法をいろいろ調べた。
※このエラーは、業務利用PCにおける社外ネットワークへの通信に対してプロキシを課している場合で起こり得るのだろうと想像します。もし業務利用時に当該エラーが発生している場合は、所属の情報システム部門やセキュリティ担当に一旦確認されてもよいと思いました。

```
APIConnectionError: Error communicating with OpenAI: HTTPSConnectionPool(host='<HOSTNAME>.openai.azure.com', port=443): Max retries exceeded with url: //openai/deployments/<MODEL-NAME>/chat/completions?api-version=<API-VERSION> (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1007)')))
```

↓※他に、SSL証明書関連でよく出るエラーの例
```よくあるエラー
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='hogehoge.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

requests.exceptions.SSLError: HTTPSConnectionPool(host='hogehoge.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, 
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

```




## 解決策（暫定対策）
HTTPS通信の仕組みはここではあまり解説しませんが、`requests`モジュールは`certifi`モジュール内のpemファイルを参照して、通信時にサーバから受領した証明書が、正しく認証局によって署名されているかどうかを検証するそうです。
逆に、`certifi`モジュール内のpemファイルを使って検証できないと、当該エラーが発砲されるようです。

業務利用PCなどでプロキシが噛んでいるなどの特殊な事情が絡む場合、当該証明書の検証が、`certifi`モジュール内のpemファイルではできず、エラーが発砲されるようです。

この問題を解消するために、requestsモジュールが適切なpemファイルを参照できるように設定すれば解決できるようです。

その解決策は以下の2パターンが考えられます
- requestsモジュールが参照するpemファイルを編集する
- requestsモジュールが参照するpemファイルのファイルパスを変更する

一時的な暫定対策として手っ取り早いのは、以下の流れになります
- そのサイトにWebブラウザでアクセスして、証明書を手動でダウンロードし、
- ①requestsモジュールが参照するpemファイルを直接編集する場合は
  - ダウンロードした証明書を証明書をテキストエディタで開いてコピーし、
  - certifiモジュール内のpemファイルにコピペして上書きするか、
- ②requestsモジュールが参照するpemファイルのファイルパスを変更する場合は
  - requestsモジュールが参照するファイルパスをREQUESTS_CA_BUNDLE環境変数として指定する（※requestsモジュールの他に、curlやopenssl(またはssl)モジュールの環境変数も指定する）


### 証明書のダウンロード方法（Edgeの場合）
#### OpenAI APIの場合
以下に記載の通り。

1. https://api.openai.com/v1/engines にアクセスし、認証は無記名でキャンセルして閉じる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507249/54b69317-11f6-d107-754c-10ff977badb2.png)

1. アドレスバーの近くにあるカギマークをクリックし、さらに右上の証明書っぽいアイコンをクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507249/67306b5c-3023-7599-9268-68ade2ee3502.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507249/63e09b6d-be16-875e-bb51-335261f949e6.png)


1. 「詳細」タブに切替え、ツリーの中で一番の親要素である「certadmin」を選択しエクスポートする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507249/bec49871-3fc7-7722-08e8-09846bf70b5a.png)

1. このファイルをテキストエディタで開いて、certifiモジュール内のpemファイルにコピペするか、
1. ソースコード中で、以下のように当該ファイルを指定するように環境変数を上書きする
```py
import os
os.environ["REQUESTS_CA_BUNDLE"] = "./certadmin.crt"  # requestsモジュールが参照するパス
os.environ["SSL_CERT_FILE"] = "./certadmin.crt"  # openssl,sslモジュールが参照するパス
os.environ["CURL_CA_BUNDLE"] = "./certadmin.crt"  # curlモジュールが参照するパス
```

※参考、openaiモジュールでは、0.x系ではrequestsモジュールを用いており、1.x系ではsslモジュールを使うように変更されたようです。

#### Azure OpenAI Serviceの場合

1. 所望のAzure OpenAI Service([https://oai.azure.com/portal](https://oai.azure.com/portal/))にアクセスし，上述と同様にダウンロードする

1. 残りの手順は上述と同様なので省略

## 実装例
自分用のメモ。python-dotenvを使った環境変数の外出し化含む実装例
```.env
AZURE_OPENAI_ENDPOINT=https://HOSTNAME.openai.azure.com/
AZURE_OPENAI_KEY=YOUR-AZUREOPENAI-KEY
REQUESTS_CA_BUNDLE=./certadmin.crt
SSL_CERT_FILE=./certadmin.crt
```

```py:load_envs.py
import os
from os.path import dirname, join

from dotenv import load_dotenv

load_dotenv(join(dirname(__file__), ".env"))

```

```py:chat.py
# %%
# https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/chatgpt?tabs=python&pivots=programming-language-chat-completions
import os

from openai import AzureOpenAI

import load_envs

client = AzureOpenAI(
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    api_version="2023-05-15",
)

response = client.chat.completions.create(
    model = "deployment_name".
    messages=[
        {"role": "system", "content": "Assistant is a large language model trained by OpenAI."},
        {"role": "user", "content": "Who were the founders of Microsoft?"},
    ],
)

print(response)
print(response.model_dump_json(indent=2))
print(response.choices[0].message.content)

# %%

```

## リスク
一時的な対処であるため、よくよく気を付ける。

>あなたが提案した解決策は一時的な対処としては有効かもしれませんが、以下に示すようにいくつかの重要な懸念事項があります。
>
>1. **セキュリティリスク**：自己署名証明書を信頼することは、セキュリティ上のリスクを引き起こす可能性があります。自己署名証明書は、信頼できる認証局から発行されたものではなく、証明書の所有者が誰であるかを検証するための信頼のしくみがありません。そのため、中間者攻撃（MITM攻撃）のリスクがあります。これは、攻撃者が通信の途中でデータを盗むか、改ざんする可能性があるということです。
>
>2. **永続性の問題**：ブラウザで取得した証明書は、その時点での証明書情報を反映したものであり、証明書の更新や変更があった場合には追従できません。その結果、再度証明書エラーが発生する可能性があります。
>
>3. **環境依存**：この方法は、プログラムが実行されるすべての環境に対して同様に証明書を設定する必要があります。これは、デプロイプロセスをより複雑にし、エラーを生じさせる可能性があります。
>
>この問題を解決する最善の方法は、正規の証明書を使用することです。自己署名証明書がサーバにインストールされている理由を調査し、可能であれば信頼できる認証局から発行された証明書に置き換えることを検討してください。問題が解決しない場合は、APIプロバイダに連絡してサポートを求めると良いでしょう。


## （参考）openaiモジュールでverify_ssl_certsオプションをオフにする【うまくいかない】
`requests`モジュール単体で使用する場合、デバッグを目的として一時的に`verify=False`オプションを指定することで回避することも可能ですが、一般的には推奨されません。
```py
requests.get("https://www.google.com", verify=False)
```

これに似たようなオプションがOpenAIにも記述されているものの、
```py
openai.verify_ssl_certs = False
```

実際openaiモジュールそのもののソースコードそのものを見ると、「もしFalseにしても効きません、証明書は常にverifyされます」と書いてあるので、意味がありません。
（※互換性のためや、自or他ライブラリとのドキュメントの一貫性を保つためや、何かしらの開発者の意図など考えられますが、いかんせん効かないものは効かない）
```py
verify_ssl_certs = True # No effect. Certificates are always verified.
```

https://community.openai.com/t/how-can-i-disable-ssl-verification-when-using-openai-api-in-python/110837/2

## 会社でプロキシサーバ云々が絡むケース（Netskopeなど）

会社でプロキシサーバを導入していて、これが影響しているケースがあります。対処法に対する理解を助けるためにまず、通信の仕組みの解説をします。

通常、HTTPS通信では、正しく安全に通信されているかどうかを担保するために、「証明書」と呼ばれる仕組みを使います。

サーバ管理者は、認証局（CA）から証明書を発行してもらい、コンテンツと一緒にこの証明書をユーザに配信します。

ユーザは、あらかじめ信頼されたルート証明機関の証明書ストアを持っており、サーバ側からコンテンツと一緒に配信された証明書が、確かに認証局（CA）によって署名されたものであるかどうかを確認するために、この証明書ストアを使って検証します。

この証明書ストアは、元々、通信用のモジュールはそれぞれで検証用のリストを抱えて持っていたそうですが、個々に持っていると、更新が煩雑で大変なので、外部のモジュールとして任せるようになったようです。
※例えば、requestsモジュールの場合は、certifiモジュールの中のpemを見て検証するという仕組みだそうです。

ただし、会社でプロキシサーバが導入されている場合、プロキシサーバが通信を中継する際に、独自のSSL証明書を使用することがあるようで、通信モジュールが参照する独自に持っている証明書リストでは役不足になる場合があるようです。

このような場合、PC自体には、会社から指定されている独自の証明書をPCに追加でインストールしているはずで、普段の通信はPC本体にインストールされている証明書のリストを使うため、上手く検証ができ、通信もできるようですが、上述のように、各モジュールがPC本体の証明書リストではなく、独自の証明書リストを使用する場合にｈエラーが発生することがあります。（※もしかしたら説明が正確ではないかも）

Netskopeの場合、公式から提示されている方法を使って回避することができます。具体的には、以下参照サイトに示される方法を使って、PCに登録されている証明書のリストをpemファイルとしてエクスポートし、当該pemファイルのパスを環境変数として設定（というか、各モジュールに予め設定されているでデフォルトの参照先を上書き）することで、検証エラーを解消することができます。
- 追加する環境変数
    - REQUESTS_CA_BUNDLE（＝requestsモジュール用）
    - SSL_CERT_FILE（＝opensslモジュール用）
    - CURL_CA_BUNDLE（＝curlモジュール用）


※詳細は以下の記事を参照してください。

https://community.netskope.com/next-gen-swg-2/configuring-cli-based-tools-and-development-frameworks-to-work-with-netskope-ssl-interception-7015

https://docs.netskope.com/en/configuring-cli-based-tools-and-development-frameworks-to-work-with-netskope-ssl-interception/


## 参考

https://dev.classmethod.jp/articles/how-to-manage-ca-root-certs-for-requets-library/

https://community.openai.com/t/ssl-certificate-verify-failed/32442/39

https://ninthcode.net/230401-chatgpt-apierror/