11.10 在网络服务中加入SSL

问题

你想实现一个基于sockets的网络服务,客户端和服务器通过SSL协议认证并加密传输的数据。

解决方案

ssl 模块能为底层socket连接添加SSL的支持。ssl.wrap_socket() 函数接受一个已存在的socket作为参数并使用SSL层来包装它。例如,下面是一个简单的应答服务器,能在服务器端为所有客户端连接做认证。

  1. from socket import socket, AF_INET, SOCK_STREAM
  2. import ssl
  3.  
  4. KEYFILE = 'server_key.pem' # Private key of the server
  5. CERTFILE = 'server_cert.pem' # Server certificate (given to client)
  6.  
  7. def echo_client(s):
  8. while True:
  9. data = s.recv(8192)
  10. if data == b'':
  11. break
  12. s.send(data)
  13. s.close()
  14. print('Connection closed')
  15.  
  16. def echo_server(address):
  17. s = socket(AF_INET, SOCK_STREAM)
  18. s.bind(address)
  19. s.listen(1)
  20.  
  21. # Wrap with an SSL layer requiring client certs
  22. s_ssl = ssl.wrap_socket(s,
  23. keyfile=KEYFILE,
  24. certfile=CERTFILE,
  25. server_side=True
  26. )
  27. # Wait for connections
  28. while True:
  29. try:
  30. c,a = s_ssl.accept()
  31. print('Got connection', c, a)
  32. echo_client(c)
  33. except Exception as e:
  34. print('{}: {}'.format(e.__class__.__name__, e))
  35.  
  36. echo_server(('', 20000))

下面我们演示一个客户端连接服务器的交互例子。客户端会请求服务器来认证并确认连接:

  1. >>> from socket import socket, AF_INET, SOCK_STREAM
  2. >>> import ssl
  3. >>> s = socket(AF_INET, SOCK_STREAM)
  4. >>> s_ssl = ssl.wrap_socket(s,
  5. cert_reqs=ssl.CERT_REQUIRED,
  6. ca_certs = 'server_cert.pem')
  7. >>> s_ssl.connect(('localhost', 20000))
  8. >>> s_ssl.send(b'Hello World?')
  9. 12
  10. >>> s_ssl.recv(8192)
  11. b'Hello World?'
  12. >>>

这种直接处理底层socket方式有个问题就是它不能很好的跟标准库中已存在的网络服务兼容。例如,绝大部分服务器代码(HTTP、XML-RPC等)实际上是基于 socketserver 库的。客户端代码在一个较高层上实现。我们需要另外一种稍微不同的方式来将SSL添加到已存在的服务中:

首先,对于服务器而言,可以通过像下面这样使用一个mixin类来添加SSL:

  1. import ssl
  2.  
  3. class SSLMixin:
  4. '''
  5. Mixin class that adds support for SSL to existing servers based
  6. on the socketserver module.
  7. '''
  8. def __init__(self, *args,
  9. keyfile=None, certfile=None, ca_certs=None,
  10. cert_reqs=ssl.CERT_NONE,
  11. **kwargs):
  12. self._keyfile = keyfile
  13. self._certfile = certfile
  14. self._ca_certs = ca_certs
  15. self._cert_reqs = cert_reqs
  16. super().__init__(*args, **kwargs)
  17.  
  18. def get_request(self):
  19. client, addr = super().get_request()
  20. client_ssl = ssl.wrap_socket(client,
  21. keyfile = self._keyfile,
  22. certfile = self._certfile,
  23. ca_certs = self._ca_certs,
  24. cert_reqs = self._cert_reqs,
  25. server_side = True)
  26. return client_ssl, addr

为了使用这个mixin类,你可以将它跟其他服务器类混合。例如,下面是定义一个基于SSL的XML-RPC服务器例子:

  1. # XML-RPC server with SSL
  2.  
  3. from xmlrpc.server import SimpleXMLRPCServer
  4.  
  5. class SSLSimpleXMLRPCServer(SSLMixin, SimpleXMLRPCServer):
  6. pass
  7.  
  8. Here's the XML-RPC server from Recipe 11.6 modified only slightly to use SSL:
  9.  
  10. import ssl
  11. from xmlrpc.server import SimpleXMLRPCServer
  12. from sslmixin import SSLMixin
  13.  
  14. class SSLSimpleXMLRPCServer(SSLMixin, SimpleXMLRPCServer):
  15. pass
  16.  
  17. class KeyValueServer:
  18. _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys']
  19. def __init__(self, *args, **kwargs):
  20. self._data = {}
  21. self._serv = SSLSimpleXMLRPCServer(*args, allow_none=True, **kwargs)
  22. for name in self._rpc_methods_:
  23. self._serv.register_function(getattr(self, name))
  24.  
  25. def get(self, name):
  26. return self._data[name]
  27.  
  28. def set(self, name, value):
  29. self._data[name] = value
  30.  
  31. def delete(self, name):
  32. del self._data[name]
  33.  
  34. def exists(self, name):
  35. return name in self._data
  36.  
  37. def keys(self):
  38. return list(self._data)
  39.  
  40. def serve_forever(self):
  41. self._serv.serve_forever()
  42.  
  43. if __name__ == '__main__':
  44. KEYFILE='server_key.pem' # Private key of the server
  45. CERTFILE='server_cert.pem' # Server certificate
  46. kvserv = KeyValueServer(('', 15000),
  47. keyfile=KEYFILE,
  48. certfile=CERTFILE)
  49. kvserv.serve_forever()

使用这个服务器时,你可以使用普通的 xmlrpc.client 模块来连接它。只需要在URL中指定 https: 即可,例如:

  1. >>> from xmlrpc.client import ServerProxy
  2. >>> s = ServerProxy('https://localhost:15000', allow_none=True)
  3. >>> s.set('foo','bar')
  4. >>> s.set('spam', [1, 2, 3])
  5. >>> s.keys()
  6. ['spam', 'foo']
  7. >>> s.get('foo')
  8. 'bar'
  9. >>> s.get('spam')
  10. [1, 2, 3]
  11. >>> s.delete('spam')
  12. >>> s.exists('spam')
  13. False
  14. >>>

对于SSL客户端来讲一个比较复杂的问题是如何确认服务器证书或为服务器提供客户端认证(比如客户端证书)。不幸的是,暂时还没有一个标准方法来解决这个问题,需要自己去研究。不过,下面给出一个例子,用来建立一个安全的XML-RPC连接来确认服务器证书:

  1. from xmlrpc.client import SafeTransport, ServerProxy
  2. import ssl
  3.  
  4. class VerifyCertSafeTransport(SafeTransport):
  5. def __init__(self, cafile, certfile=None, keyfile=None):
  6. SafeTransport.__init__(self)
  7. self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
  8. self._ssl_context.load_verify_locations(cafile)
  9. if certfile:
  10. self._ssl_context.load_cert_chain(certfile, keyfile)
  11. self._ssl_context.verify_mode = ssl.CERT_REQUIRED
  12.  
  13. def make_connection(self, host):
  14. # Items in the passed dictionary are passed as keyword
  15. # arguments to the http.client.HTTPSConnection() constructor.
  16. # The context argument allows an ssl.SSLContext instance to
  17. # be passed with information about the SSL configuration
  18. s = super().make_connection((host, {'context': self._ssl_context}))
  19.  
  20. return s
  21.  
  22. # Create the client proxy
  23. s = ServerProxy('https://localhost:15000',
  24. transport=VerifyCertSafeTransport('server_cert.pem'),
  25. allow_none=True)

服务器将证书发送给客户端,客户端来确认它的合法性。这种确认可以是相互的。如果服务器想要确认客户端,可以将服务器启动代码修改如下:

  1. if __name__ == '__main__':
  2. KEYFILE='server_key.pem' # Private key of the server
  3. CERTFILE='server_cert.pem' # Server certificate
  4. CA_CERTS='client_cert.pem' # Certificates of accepted clients
  5.  
  6. kvserv = KeyValueServer(('', 15000),
  7. keyfile=KEYFILE,
  8. certfile=CERTFILE,
  9. ca_certs=CA_CERTS,
  10. cert_reqs=ssl.CERT_REQUIRED,
  11. )
  12. kvserv.serve_forever()

为了让XML-RPC客户端发送证书,修改 ServerProxy 的初始化代码如下:

  1. # Create the client proxy
  2. s = ServerProxy('https://localhost:15000',
  3. transport=VerifyCertSafeTransport('server_cert.pem',
  4. 'client_cert.pem',
  5. 'client_key.pem'),
  6. allow_none=True)

讨论

试着去运行本节的代码能测试你的系统配置能力和理解SSL。可能最大的挑战是如何一步步的获取初始配置key、证书和其他所需依赖。

我解释下到底需要啥,每一个SSL连接终端一般都会有一个私钥和一个签名证书文件。这个证书包含了公钥并在每一次连接的时候都会发送给对方。对于公共服务器,它们的证书通常是被权威证书机构比如Verisign、Equifax或其他类似机构(需要付费的)签名过的。为了确认服务器签名,客户端回保存一份包含了信任授权机构的证书列表文件。例如,web浏览器保存了主要的认证机构的证书,并使用它来为每一个HTTPS连接确认证书的合法性。对本小节示例而言,只是为了测试,我们可以创建自签名的证书,下面是主要步骤:

bash % openssl req -new -x509 -days 365 -nodes -out server_cert.pem
-keyout server_key.pem
Generating a 1024 bit RSA private key……………………………………++++++…++++++writing new private key to ‘server_key.pem’
You are about to be asked to enter information that will be incorporatedinto your certificate request.What you are about to enter is what is called a Distinguished Name or a DN.There are quite a few fields but you can leave some blankFor some fields there will be a default value,If you enter ‘.’, the field will be left blank.
Country Name (2 letter code) [AU]:USState or Province Name (full name) [Some-State]:IllinoisLocality Name (eg, city) []:ChicagoOrganization Name (eg, company) [Internet Widgits Pty Ltd]:Dabeaz, LLCOrganizational Unit Name (eg, section) []:Common Name (eg, YOUR name) []:localhostEmail Address []:bash %

在创建证书的时候,各个值的设定可以是任意的,但是”Common Name“的值通常要包含服务器的DNS主机名。如果你只是在本机测试,那么就使用”localhost“,否则使用服务器的域名。

—–BEGIN RSA PRIVATE KEY—–MIICXQIBAAKBgQCZrCNLoEyAKF+f9UNcFaz5Osa6jf7qkbUl8si5xQrY3ZYC7juunL1dZLn/VbEFIITaUOgvBtPv1qUWTJGwga62VSG1oFE0ODIx3g2Nh4sRf+rySsx2L4442nx0z4O5vJQ7k6eRNHAZUUnCL50+YvjyLyt7ryLSjSuKhCcJsbZgPwIDAQABAoGAB5evrr7eyL4160tM5rHTeATlaLY3UBOe5Z8XN8Z6gLiB/ucSX9AysviVD/6F3oD6z2aL8jbeJc1vHqjt0dC2dwwm32vVl8mRdyoAsQpWmiqXrkvP4Bsl04VpBeHwQt8xNSW9SFhceL3LEvw9M8i9MV39viih1ILyH8OuHdvJyFECQQDLEjl2d2ppxND9PoLqVFAirDfX2JnLTdWbc+M11a9Jdn3hKF8TcxfEnFVs5Gav1MusicY5KB0ylYPbYbTvqKc7AkEAwbnRBO2VYEZsJZp2X0IZqP9ovWokkpYx+PE4+c6MySDgaMcigL7vWDIHJG1CHudD09GbqENasDzyb2HAIW4CzQJBAKDdkv+xoW6gJx42Auc2WzTcUHCAeXR/+BLpPrhKykzbvOQ8YvS5W764SUO1u1LWs3G+wnRMvrRvlMCZKgggBjkCQQCGJewto2+a+WkOKQXrNNScCDE5aPTmZQc5waCYq4UmCZQcOjkUOiN3ST1U5iuxRqfbV/yX6fw0qh+fLWtkOs/JAkA+okMSxZwqRtfgOFGBfwQ8/iKrnizeanTQ3L6scFXICHZXdJ3XQ6qUmNxNn7iJ7S/LDawo1QfWkCfD9FYoxBlg—–END RSA PRIVATE KEY—–

服务器证书文件server_cert.pem内容类似下面这样:

—–BEGIN CERTIFICATE—–MIIC+DCCAmGgAwIBAgIJAPMd+vi45js3MA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJbGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMzAxMTExODQyMjdaFw0xNDAxMTExODQyMjdaMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJbGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAmawjS6BMgChfn/VDXBWs+TrGuo3+6pG1JfLIucUK2N2WAu47rpy9XWS5/1WxBSCE2lDoLwbT79alFkyRsIGutlUhtaBRNDgyMd4NjYeLEX/q8krMdi+OONp8dM+DubyUO5OnkTRwGVFJwi+dPmL48i8re68i0o0rioQnCbG2YD8CAwEAAaOBwTCBvjAdBgNVHQ4EFgQUrtoLHHgXiDZTr26NMmgKJLJLFtIwgY4GA1UdIwSBhjCBg4AUrtoLHHgXiDZTr26NMmgKJLJLFtKhYKReMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJbGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAPMd+vi45js3MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAFci+dqvMG4xF8UTnbGVvZJPIzJDRee6Nbt6AHQo9pOdAIMAuWsGCplSOaDNdKKzl+b2UT2Zp3AIW4Qd51bouSNnR4M/gnr9ZD1ZctFd3jS+C5XRpD3vvcW5lAnCCC80P6rXy7d7hTeFu5EYKtRGXNvVNd/06NALGDflrrOwxF3Y=—–END CERTIFICATE—–

在服务器端代码中,私钥和证书文件会被传给SSL相关的包装函数。证书来自于客户端,私钥应该在保存在服务器中,并加以安全保护。

在客户端代码中,需要保存一个合法证书授权文件来确认服务器证书。如果你没有这个文件,你可以在客户端复制一份服务器的证书并使用它来确认。连接建立后,服务器会提供它的证书,然后你就能使用已经保存的证书来确认它是否正确。

服务器也能选择是否要确认客户端的身份。如果要这样做的话,客户端需要有自己的私钥和认证文件。服务器也需要保存一个被信任证书授权文件来确认客户端证书。

如果你要在真实环境中为你的网络服务加上SSL的支持,这小节只是一个入门介绍而已。你还应该参考其他的文档,做好花费不少时间来测试它正常工作的准备。反正,就是得慢慢折腾吧~ ^_^

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p10_add_ssl_to_network_services.html