Python3 M2Cryptoライブラリを使用してSSL証明書の更新期限チェックをする

## 背景
Let's Encryptで証明書の自動更新をしているドメインで、マニュアルどおりにやっていれば、デフォルト設定で30日を切ったタイミングでSSL証明書の更新が行われるはずですが、更新が実施されているかチェックする必要性があったためスクリプトで実装しました。
実際には下記をチェック対象ドメインリストのコンフィグファイルを食わせるなどして運用しますが、実装に利用したM2Cryptoライブラリの説明があまりなかったので、実際に操作した記録を含めて書き起こしておきます。
本当は使い慣れてるurllibとかで実装できればよかったんですが、SSL証明書の検証ができる方法がよくわからなかったのでM2Cryptoライブラリを利用しました。

## 環境
* Amazon Linux
* Python3

## 利用するライブラリ
* datetime
* ssl
* M2Crypto

## Python実行環境
こんな感じでPyenvを利用してpython3環境を作成済み。

~~~
# pwd
/root/python3

# pyenv versions
  system
* 3.5.6 (set by /root/python3/.python-version)

# python -V
Python 3.5.6
~~~

## SSL証明書の期限確認
対話型のインタラクティブモードを利用してデータ状態などを確認しながら必要なものを揃えていきます。

~~~python
import ssl
import M2Crypto
import datetime

port = 443
hostname = 'www.qiita.com'      # ドメインは適当にご自身のものを。

cert = ssl.get_server_certificate((hostname, port))

x509 = M2Crypto.X509.load_cert_string(cert)

x509.get_subject().as_text()    # 'CN=qiita.com'
~~~

この辺から、あんまりドキュメントが見当たらなくてよくわからないので、使えるメソッドを掘り出しながら進めます。

~~~python
dir(x509)
# ['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
# '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_ptr', '_pyfree', 'add_ext', 'as_der', 'as_pem',
# 'as_text', 'check_ca', 'check_purpose', 'get_ext', 'get_ext_at', 'get_ext_count', 'get_fingerprint', 'get_issuer', 'get_not_after',
# 'get_not_before', 'get_pubkey', 'get_serial_number', 'get_subject', 'get_version', 'm2_x509_free', 'save', 'save_pem', 'set_issuer',
# 'set_issuer_name', 'set_not_after', 'set_not_before', 'set_pubkey', 'set_serial_number', 'set_subject', 'set_subject_name',
# 'set_version', 'sign', 'verify', 'x509']

~~~

使えそうなget_not_afterを発見。

~~~python
type(x509.get_not_after())         # <M2Crypto.ASN1.ASN1_TIME object at 0x7f26999f1940>

dir(x509.get_not_after())
# ['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
# '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_ptr', '_pyfree', '_ssl_months', 'asn1_time',
# 'get_datetime', 'm2_asn1_time_free', 'set_datetime', 'set_string', 'set_time']
~~~

datetimeで出力できそうな get_datetime なるものを発見。

~~~python
type(x509.get_not_after().get_datetime())  # <class 'datetime.datetime'>

x509.get_not_after().get_datetime()        # datetime.datetime(2020, 4, 30, 12, 0, tzinfo=<Timezone: UTC>)
~~~

datetimeでデータが取得できたので、後はdatetimeライブラリを利用してtimedeltaを求めてあげればOK

~~~python
exp_date = x509.get_not_after().get_datetime()

now = datetime.datetime.now()            # datetime.datetime(2019, 11, 29, 10, 52, 24, 89337)

exp_date - now
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't subtract offset-naive and offset-aware datetimes
~~~

datetime同士の引き算はタイムゾーンがくっついてるとできないそうです。
参考 : [Pythonのタイムゾーンの扱い](https://qiita.com/takaki@github/items/b1a380a0a644170f5885)
ということで、タイムゾーン情報を削って比較。

~~~python
remaining_time = exp_date.replace(tzinfo=None) - now       # datetime.timedelta(53, 15629, 910663)

remaining_time.days
152
~~~

無事SSL証明書の残り日数の取得ができました。

最終的なコードは以下。
割とコンパクト。

~~~python
import ssl
import M2Crypto
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import datetime

port = 443
hostname = 'www.qiita.com'     # 適宜書き換えるか引数を読み込んで下さい

cert = ssl.get_server_certificate((hostname, port))
x509 = M2Crypto.X509.load_cert_string(cert)
exp_date = x509.get_not_after().get_datetime()
now = datetime.datetime.now()
remaining_time = exp_date.replace(tzinfo=None) - now

print(remaining_time.days)
152
~~~

### 参考情報
* [Pythonのタイムゾーンの扱い](https://qiita.com/takaki@github/items/b1a380a0a644170f5885)
* [M2Crypto公式ドキュメント](https://m2crypto.readthedocs.io/en/latest/index.html)