传输层安全协议(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, ssl
def 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, textwrap
import ctypes
from pprint import pprint
def 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 cert
class PySSLSocket(ctypes.Structure):
"""The first few fields of a PySSLSocket (see Python's 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 socket's 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:\n{}'.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,就应该始终使用工具对那些具有不同参数集的链接进行测试。
最后的最后,寒假快结束了。
祝大家天天开心,新的一年更加的万事胜意。