ベスパライフ

日記・備忘録。バイク買いました。ベスパもってないです。

python3.4.2のSSLContextのデフォルト引数はなんなのか

SSLContextのデフォルト引数

公式のリファレンスでは、3系ではpython3.5.3以降の情報しか掲載されていません。
ということで実際のソースを見ることにします。

私の環境では以下のパスにありました。
/usr/lib/python3.4/ssl.py

class SSLContext(_SSLContext):
    """An SSLContext holds various SSL-related configuration options and
    data, such as certificates and possibly a private key."""

_SSLContextとは何か。

import _ssl             # if we can't import it, let the error propagate
(中略)
from _ssl import _SSLContext

_sslモジュールがなんなのか正確には不明ですが、おそらくdevelop環境か昔のモジュールのどちらかでしょう。将来修正する予定のモジュールを読み込んでいると思われます。

_sslはどこにあるのか。こういう場合は、コンソールを起動して読み込んで見たらパスがわかります。

>>> import _ssl
>>> _ssl
<module '_ssl' from '/usr/lib/python3.4/lib-dynload/_ssl.cpython-34m-arm-linux-gnueabihf.so'>

.so拡張子はLinuxで共有ライブラリ(Shared Object)を指すそうです。初めて知りました。
とりあえず_ssl.cpython-34m-arm-linux-gnueabihf.soを開いてみましたが、コンパイルされていて読めませんでした(cpythonて書いてあるし)。コメントっぽいのは読めますが、残念ながらよくわかりません。行き詰まりました。

ここで気づいたのですが、普通に呼び出してみたらどうなるのでしょうか。

>> ssl.SSLContext()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __new__() missing 1 required positional argument: 'protocol'

あっ。デフォルト引数ないんですね。そうですか。protocol引数に必ず値を指定しなくてはいけないようです。

create_default_context()を使用したとき

そもそもなぜSSLContextのデフォルト引数を調べようかと思ったかというと、create_default_context()を使用して呼び出していたからです。なので本来ソースを見るべきはこちらの関数だったというオチです。

def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None,
                           capath=None, cadata=None):
    """Create a SSLContext object with default settings.

    NOTE: The protocol and settings may change anytime without prior
          deprecation. The values represent a fair balance between maximum
          compatibility and security.
    """
    if not isinstance(purpose, _ASN1Object):
        raise TypeError(purpose)

    context = SSLContext(PROTOCOL_SSLv23)

    # SSLv2 considered harmful.
    context.options |= OP_NO_SSLv2

    # SSLv3 has problematic security and is only required for really old
    # clients such as IE6 on Windows XP
    context.options |= OP_NO_SSLv3

    # disable compression to prevent CRIME attacks (OpenSSL 1.0+)
    context.options |= getattr(_ssl, "OP_NO_COMPRESSION", 0)

PROTOCOL_SSLv23をセットしています。それにOP_NO_SSLv2やOP_NO_SSLv3のフラグを立てて、脆弱性のあるSSLv2等でネゴシエーションしないようにしています。
ではPROTOCOL_SSLv23はなんなのか。

ssl.PROTOCOL_SSLv23(原文) data:PROTOCOL_TLSエイリアスです。 バージョン 3.6 で撤廃: 代わりに PROTOCOL_TLS を使用してください。

ssl.PROTOCOL_TLS¶(原文) チャンネル暗号化プロトコルとして、クライアントとサーバの両方がサポートする中の、プロトコルバージョンが最も大きなものを選択します。その名前にも関わらず、このオプションは “SSL” とともに “TLSプロトコルも選択できます。 バージョン 2.7.13 で追加.

ssl.PROTOCOL_TLS(原文) チャンネル暗号化プロトコルとして、クライアントとサーバの両方がサポートする中の、プロトコルバージョンが最も大きなものを選択します。その名前にも関わらず、このオプションは “SSL” とともに “TLSプロトコルも選択できます。 バージョン 3.5.3 で追加.

ssl.PROTOCOL_TLSはバージョン3.5.3で追加されていますが、バージョン2.7.13でもすでに追加されています。バージョン2.7.13で追加されているということは、python3.4.2でも同じ挙動するはずと考えるのは甘いでしょうか。たぶん甘いでしょう。わざわざ3.5.3で追加と書いてあるのは、2系から3系に移植する際に実装を後回しにしている可能性があります。

これ以上正確な挙動を知ろうと思うなら、実際の挙動を見る他ないと思います。
たとえばgmailとSTARTTLSネゴシエーションをしたときの通信の様子は次のようになります(2017/10)

>> smtplib.SMTP("smtp.gmail.com", 587)
<smtplib.SMTP object at 0x7671df70>
>> M = smtplib.SMTP("smtp.gmail.com", 587)
>> M.set_debuglevel(True)
>> context = ssl.create_default_context()
>> M.starttls(context=context)
send: 'ehlo [127.0.1.1]\r\n'
reply: b'250-smtp.gmail.com at your service, [114.147.68.118]\r\n'
reply: b'250-SIZE 35882577\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-STARTTLS\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'smtp.gmail.com at your service, [114.147.68.118]\nS
IZE 35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMT
PUTF8'
send: 'STARTTLS\r\n'
reply: b'220 2.0.0 Ready to start TLS\r\n'
reply: retcode (220); Msg: b'2.0.0 Ready to start TLS'
(220, b'2.0.0 Ready to start TLS')