Chrome
Chromium
python3
SSL証明書

Chromeで旧シマンテック/ジオトラスト社のSSL証明書が信頼されたものでなくなる日に向けて...

昨年から、なんだかゴタゴタしていたやつ。
長めの期間で証明書を取得していると、再発行の該当になっているパターンがある。

信頼されなくなっちゃうパターン

ref: https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html

対象1:2016年5月31日以前に発行されたSSLサーバ証明書(ラピッドSSL、クイックSSLプレミアム)

Chrome66からSSLサーバ証明書が無効化される(2018年4月17日に正式版リリース予定)

  • 有効期間が2018年3月14日以前の場合は、対応不要
  • 有効期間が2018年3月15日以降の場合は、2017年12月1日以降に再発行が必要

対象2:2016年6月1日2017年11月30日までに発行されたSSLサーバ証明書(ラピッドSSL、クイックSSLプレミアム、セキュア・サーバID、セキュア・サーバID EV、グローバル・サーバID)

Chrome70からSSLサーバ証明書が無効化される(2018年10月23日に正式版リリース予定)

  • 有効期間が2018年9月12日以前の場合は、対応不要
  • 有効期間が2018年9月13日以降の場合は、2017年12月1日以降に再発行が必要

簡易的なチェカースクリプト作った

管理対象の証明書がそれなりにあると、手作業チェックは漏れもありそうなので、確認の意味もこめて、チェックスクリプト(簡易版)を作ってみた。
Gist:kacchan822/check_cert_chrome.py 記事末尾にも載っけてある

以下の具合で、対象1または2で対応が必要だと、それぞれCRITICAL-1CRITICAL-2と教えてくれる。対象1または2だけど、対応不要な場合も、一応WARNINGになる。

$ python3 check_cert_chrome.py www.example.com
[     OK     ] www.example.com: Cert was published at 2017-12-15 20:03:18. (Let's Encrypt, Let's Encrypt Authority X3)


$ python3 check_cert_chrome.py www.example.com
[  WARNING   ] www.example.com: Cert was published at 2016-03-16 00:00:00; before 2016-05-31. (GeoTrust Inc., RapidSSL SHA256 CA)

$ python3 check_cert_chrome.py www.example.com
[  WARNING   ] www.example.com: Cert was published at 2017-06-10 00:00:00; between 2016-06-01 and 2017-11-30. (Symantec Corporation, Symantec Class 3 Secure Server CA - G4)


$ python3 check_cert_chrome.py www.example.com
[ CRITICAL-1 ] www.example.com: Cert was published at 2016-03-16 00:00:00; before 2016-05-31. (GeoTrust Inc., RapidSSL SHA256 CA)

$ python3 check_cert_chrome.py www.example.com
[ CRITICAL-2 ] www.example.com: Cert was published at 2017-04-01 00:00:00; between 2016-06-01 and 2017-11-30. (Symantec Corporation, Symantec Class 3 Secure Server CA - G4)

すべての証明書を正しく判定できるわけではないので、あしからず…。

Gist:kacchan822/check_cert_chrome.py

check_cert_chrome.py
#!/usr/bin/env python3
# This software is released under the MIT License.
# http://opensource.org/licenses/mit-license.php

import datetime
import re
import socket
import ssl
import sys

timeout = 5


def get_cert(cn, port=443):
    """ 
        return: dict
            {'OCSP': ('http://ocsp.int-x3.letsencrypt.org',),
            'caIssuers': ('http://cert.int-x3.letsencrypt.org/',),
            'issuer': ((('countryName', 'US'),),
                        (('organizationName', "Let's Encrypt"),),
                        (('commonName', "Let's Encrypt Authority X3"),)),
            'notAfter': 'Mar 15 20:46:19 2018 GMT',
            'notBefore': 'Dec 15 20:46:19 2017 GMT',
            'serialNumber': '0467F6AD9DE0A1D776DC5688B22569094A49',
            'subject': ((('commonName', 'ip.ksn.cloud'),),),
            'subjectAltName': (('DNS', 'ip.ksn.cloud'),
                                ('DNS', 'ip4.ksn.cloud'),
                                ('DNS', 'ip6.ksn.cloud')),
            'version': 3}        
    """
    context = ssl.create_default_context()
    try:
        with context.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM),
                                server_hostname=cn) as conn:
            conn.settimeout(timeout)
            conn.connect((cn, port))
            return conn.getpeercert()
    except ssl.CertificateError as e:
        print('Errorr:', e)
        sys.exit(1)


def _conv_dt(datetime_string):
    cert_date_format = '%b %d %H:%M:%S %Y %Z'
    return datetime.datetime.strptime(datetime_string, cert_date_format)


def _get_cert_datetime(cert):
    return _conv_dt(cert['notBefore']), _conv_dt(cert['notAfter'])


def check_issuer(cert):
    if cert.get('issuer'):
        issuer = {key_val[0][0]: key_val[0][1] for key_val in cert.get('issuer')}
    else:
        issuer = {}
    issuer['checkFlag'] = True
    if re.match(r'(Symantec|GeoTrust)', issuer.get('organizationName', '')):
        issuer['checkFlag'] = False
    if re.match(r'RapidSSL', issuer.get('commonName', '')):
        issuer['checkFlag'] = False
    return issuer


def check_cert_datetime(cert):
    start, end = _get_cert_datetime(cert)
    if start < datetime.datetime(2016, 6, 1, 0, 0, 0):
        flag = 1
        msg = 'Cert was published at {}; before 2016-05-31.'
    elif (datetime.datetime(2016, 5, 31, 23, 59, 59) < start and
          start < datetime.datetime(2017, 12, 1, 0, 0, 0)):
        flag = 2
        msg = 'Cert was published at {}; between 2016-06-01 and 2017-11-30.'
    else:
        flag = 0
        msg = 'Cert was published at {}.'
    return flag, msg.format(str(start))


def main(cn):
    try:
        cn, port = cn.strip().split(':')
    except ValueError:
        cn = cn.strip()
        port = 443

    cert = get_cert(cn, int(port))
    issuer = check_issuer(cert)
    cert_datetime = check_cert_datetime(cert)
    cert_start, cert_end = _get_cert_datetime(cert)

    if not issuer['checkFlag']:
        if (cert_datetime[0] == 1 and
                cert_end > datetime.datetime(2016, 6, 1, 0, 0, 0)):
            state_msg = 'CRITICAL-1'
        elif (cert_datetime[0] == 2 and
              cert_end > datetime.datetime(2018, 9, 12, 23, 59, 59)):
            state_msg = 'CRITICAL-2'
        else:
            state_msg = 'WARNING'
    else:
        state_msg = 'OK'

    message = '[{: ^12}] {}: {} ({}, {})'.format(
        state_msg, cn, cert_datetime[1], issuer.get('organizationName', '-'),
        issuer.get('commonName', '-')
    )
    return message


if __name__ == '__main__':
    print(main(sys.argv[1]))

参考