Python網路編程中的TLS/SSL。
傳輸層安全協議(TLS)算是如今互聯網上應用最廣泛的加密方法。
TLS的前身是安全套接層(SSL),現代互聯網的許多協議基礎協議都是使用TLS來驗證伺服器身份,並保護傳輸過程中的數據。
TLS能保護的信息包括:與請求URL之間的HTTPS鏈接以及以及返回內容、密碼或cookie等可能在套接字雙向傳遞的認證信息。
下面的信息無法使用TLS保護:
- 本機與遠程主機都是可見的,地址信息在每個數據包的IP頭信息中以純文本的形式表示。
- 客戶端與伺服器的埠號同樣在每個TCP頭信息中可見。
- 客戶端為了獲取伺服器的IP地址,可能會先進行DNS查詢。該查詢在通過網路發送時也是可見的。
通過TLS加密的套接字向任何一方傳遞數據塊的時候,觀察者都可以看到數據塊的大小。儘管TLS會試圖隱藏確切的位元組數,但是觀察者仍然能看到傳輸數據塊的大致規模。同樣,也可以看到請求和響應的整體模式。
關於TLS怎麼被設計出來的,那些問題這裡就不說,下面說一下生成證書。
Python標準庫中並沒有提供私鑰生成或者證書籤名的相關操作。如果需要進行與這兩項相關的操作,那麼必須使用其他工具。openssl命令行工具就很流行而且很好用。
自己創建證書,通常要先生成兩部分信息:第一部分是人工生成的,另一部分是由機器生成。人工生成的信息。人工生成的信息對證書中的描述的實體進行了文本說明,而機器會使用操作系統提供的真正的隨機演算法精心生成一個秘鑰。
你也可以把手寫的實體描述保存在一個版本控制文件中,以便今後查看。當然,你也可以直接在彈出的openssl命令提示符中輸入實體描述的相關欄位。
然後我們說一下TLS負載移除。
這裡面先說另外一個點,為什麼要直接在Python應用程序中直接進行加密操作,而不是直接使用工具。如果在另外一個埠運行這些工具的話,就可以通過它們對客戶端的連接作出響應。
因此,在Python應用程序提供TLS支持的時候有兩種選擇:方案一是使用一個單獨的守護進程或者服務提供TLS支持。方案二則是直接在Python編寫的伺服器代碼中使用提供TLS功能的OpenSSL庫。相比較於方案二,方案一更易於升級或者維護。
下面說下Python3.4之後的默認上下文,Python標準庫是對OpenSSL庫進行封裝。當然,Python社區也在研究其他密碼學的項目,包括pyOpenSSL。
Python3.4引入了ssl.create_default_context()函數,這樣我們就可以輕鬆在Python應用程序中安全使用TLS。
這是一個簡單的客戶端和伺服器,通過TLS套接字進行安全通信的方法。
import argparse, socket, ssldef client(host, port, cafile=None): purpose = ssl.Purpose.SERVER_AUTH context = ssl.create_default_context(purpose, cafile=cafile) raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) raw_sock.connect((host, port)) print(Connected to host {!r} and port {}.format(host, port)) ssl_sock = context.wrap_socket(raw_sock, server_hostname=host) while True: data = ssl_sock.recv(1024) if not data: break print(repr(data))def server(host, port, certfile, cafile=None): purpose = ssl.Purpose.CLIENT_AUTH context = ssl.create_default_context(purpose, cafile=cafile) context.load_cert_chain(certfile) listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listener.bind((host, port)) listener.listen(1) print(Listening at interface {!r} and port {}.format(host, port)) raw_sock, address = listener.accept() print(Connection from host {!r} and port {}.format(*address)) ssl_sock = context.wrap_socket(raw_sock, server_side=True) ssl_sock.sendall(Simple is better than complex..encode(ascii)) ssl_sock.close()if __name__ == __main__: parser = argparse.ArgumentParser(description=Safe TLS client and server) parser.add_argument(host, help=hostname or IP address) parser.add_argument(port, type=int, help=TCP port number) parser.add_argument(-a, metavar=cafile, default=None, help=authority: path to CA certificate PEM file) parser.add_argument(-s, metavar=certfile, default=None, help=run as server: path to server PEM file) args = parser.parse_args() if args.s: server(args.host, args.port, args.s, args.a) else: client(args.host, args.port, args.a)
從上面看出,為一個套接字提供安全通信只需要三個步驟。
- 第一步是TLS上下文對象。對象中保存了我們對證書的認證與加密演算法選擇的偏好設置。
- 第二步是調用上下文對象的wrap_socket()方法,表示讓OpenSLL庫負責控制我們的TCP鏈接。然後與通信對方交換必要的握手信息,並建立加密鏈接。
- 最後一步是使用wrap_socket()調用返回的ssl_sock對象,進行所有的後續通信。
另外,套接字包裝的變體有很多,這裡就不再說了。另外不再詳細說的就是,如果對數據安全性要求很高的話,可能需要自己指定OpenSLL確切使用的加密演算法,而不使用create_default_context()函數提供的默認值。
有一個問題就是,如何配置TLS加密演算法及選項,以防止通信對方使用這些協議,以防止通信對方使用較弱的協議版本、加密演算法或者是像壓縮 這種可能降低協議安全性的選項。
這個配置可以通過下面的方式來解決:
- 第一種是特定於庫的API調用
- 第二種是直接傳遞一個包含了配置選項的SSLContext對象
然後再看支持TLS的協議:
- http.client
- smtplib
- poplib
- imaplib
- ftplib
- nntplib
下面看一個腳本,這個腳本創建了一個加密鏈接,然後列印出這個鏈接的特性。
先看一下如何獲取配置信息:
- getpeercert()
- cipher()
- compression()
為了儘可能列印出這些特性,所以使用了ctypes來獲取正在使用的TLS協議的信息。這段代碼是讓我們連接到一個自己構建的客戶端或伺服器,並了解它們支持的或不支持的加密演算法與協議。
import argparse, socket, ssl, sys, textwrapimport ctypesfrom pprint import pprintdef open_tls(context, address, server=False): raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if server: raw_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) raw_sock.bind(address) raw_sock.listen(1) say(Interface where we are listening, address) raw_client_sock, address = raw_sock.accept() say(Client has connected from address, address) return context.wrap_socket(raw_client_sock, server_side=True) else: say(Address we want to talk to, address) raw_sock.connect(address) return context.wrap_socket(raw_sock)def describe(ssl_sock, hostname, server=False, debug=False): cert = ssl_sock.getpeercert() if cert is None: say(Peer certificate, none) else: say(Peer certificate, provided) subject = cert.get(subject, []) names = [name for names in subject for (key, name) in names if key == commonName] if subjectAltName in cert: names.extend(name for (key, name) in cert[subjectAltName] if key == DNS) say(Name(s) on peer certificate, *names or [none]) if (not server) and names: try: ssl.match_hostname(cert, hostname) except ssl.CertificateError as e: message = str(e) else: message = Yes say(Whether name(s) match the hostname, message) for category, count in sorted(context.cert_store_stats().items()): say(Certificates loaded of type {}.format(category), count) try: protocol_version = SSL_get_version(ssl_sock) except Exception: if debug: raise else: say(Protocol version negotiated, protocol_version) cipher, version, bits = ssl_sock.cipher() compression = ssl_sock.compression() say(Cipher chosen for this connection, cipher) say(Cipher defined in TLS version, version) say(Cipher key has this many bits, bits) say(Compression algorithm in use, compression or none) return certclass PySSLSocket(ctypes.Structure): """The first few fields of a PySSLSocket (see Pythons Modules/_ssl.c).""" _fields_ = [(ob_refcnt, ctypes.c_ulong), (ob_type, ctypes.c_void_p), (Socket, ctypes.c_void_p), (ssl, ctypes.c_void_p)]def SSL_get_version(ssl_sock): """Reach behind the scenes for a sockets TLS protocol version.""" lib = ctypes.CDLL(ssl._ssl.__file__) lib.SSL_get_version.restype = ctypes.c_char_p address = id(ssl_sock._sslobj) struct = ctypes.cast(address, ctypes.POINTER(PySSLSocket)).contents version_bytestring = lib.SSL_get_version(struct.ssl) return version_bytestring.decode(ascii)def lookup(prefix, name): if not name.startswith(prefix): name = prefix + name try: return getattr(ssl, name) except AttributeError: matching_names = (s for s in dir(ssl) if s.startswith(prefix)) message = Error: {!r} is not one of the available names:
{}.format( name, .join(sorted(matching_names))) print(fill(message), file=sys.stderr) sys.exit(2)def say(title, *words): print(fill(title.ljust(36, .) + + .join(str(w) for w in words)))def fill(text): return textwrap.fill(text, subsequent_indent= , break_long_words=False, break_on_hyphens=False)if __name__ == __main__: parser = argparse.ArgumentParser(description=Protect a socket with TLS) parser.add_argument(host, help=hostname or IP address) parser.add_argument(port, type=int, help=TCP port number) parser.add_argument(-a, metavar=cafile, default=None, help=authority: path to CA certificate PEM file) parser.add_argument(-c, metavar=certfile, default=None, help=path to PEM file with client certificate) parser.add_argument(-C, metavar=ciphers, default=ALL, help=list of ciphers, formatted per OpenSSL) parser.add_argument(-p, metavar=PROTOCOL, default=SSLv23, help=protocol version (default: "SSLv23")) parser.add_argument(-s, metavar=certfile, default=None, help=run as server: path to certificate PEM file) parser.add_argument(-d, action=store_true, default=False, help=debug mode: do not hide "ctypes" exceptions) parser.add_argument(-v, action=store_true, default=False, help=verbose: print out remote certificate) args = parser.parse_args() address = (args.host, args.port) protocol = lookup(PROTOCOL_, args.p) context = ssl.SSLContext(protocol) context.set_ciphers(args.C) context.check_hostname = False if (args.s is not None) and (args.c is not None): parser.error(you cannot specify both -c and -s) elif args.s is not None: context.verify_mode = ssl.CERT_OPTIONAL purpose = ssl.Purpose.CLIENT_AUTH context.load_cert_chain(args.s) else: context.verify_mode = ssl.CERT_REQUIRED purpose = ssl.Purpose.SERVER_AUTH if args.c is not None: context.load_cert_chain(args.c) if args.a is None: context.load_default_certs(purpose) else: context.load_verify_locations(args.a) print() ssl_sock = open_tls(context, address, args.s) cert = describe(ssl_sock, args.host, args.s, args.d) print() if args.v: pprint(cert)
到了這裡,這篇文章就結束了。
要注意的是,一旦我們在自己的應用程序中實現了TLS,就應該始終使用工具對那些具有不同參數集的鏈接進行測試。
最後的最後,寒假快結束了。
祝大家天天開心,新的一年更加的萬事勝意。
推薦閱讀:
※金融民工已承認文章不實,請VNPY對Quicklib不實文章已經造成惡劣影響,請公開做出歉意說明吧 不實文章還有
※正則表達式如何匹配網頁裡面的漢字?
※python中動態獲取cookies
※Python字典里的5個黑魔法
※ABSP第七章:[lesson25 - ?, +, *, 和 {} ,正則表達式句法以及貪婪/非貪婪匹配]