使用Python探索理解HTTPS (翻译)

原文

你有没有想过为什么你可以通过互联网发送信用卡信息?你可能已经注意到了浏览器中url上的https://,但是它是什么,它如何保证你的信息安全?或者,你可能想创建一个python https应用程序,但是不确定这意味着什么。如何确保web应用程序是安全的?

可能会让你惊讶,你不必是一个安全专家来回答这些问题!在本教程中,你将获得有关各种因素的实用知识,这些因素组合在一起可以保证Internet上的通信安全。你将看到python https应用程序如何保持信息安全的具体示例。

在本教程中,你将学习如何:

  • 监控和分析网络流量
  • 应用加密技术确保数据安全
  • 描述公钥基础设施(PKI)的核心概念
  • 创建你自己的证书颁发机构
  • 构建Python HTTPS应用程序
  • 识别常见的Python HTTPS警告和错误

什么是HTTP?

在深入研究HTTPS及其在Python中的使用之前,了解其父类HTTP是很重要的。这个缩写是超文本传输协议的缩写,它是在你浏览你喜欢的网站时进行的大多数通信的基础。更具体地说,HTTP是用户代理(如你的web浏览器)与web服务器通信的方式,例如realpython.com网站. 以下是HTTP通信的简化图:

在这里插入图片描述

此图显示了计算机如何与服务器通信的简化版本。以下是每个步骤的分解:

  1. 你让你的浏览器转到http://someserver.com/link。
  2. 你的设备和服务器设置了TCP连接。
  3. 你的浏览器向服务器发送一个HTTP请求。
  4. 服务器接收HTTP请求并对其进行解析。
  5. 服务器用HTTP响应进行响应。
  6. 你的计算机接收、分析并显示响应。

这个步骤分解抓取了HTTP的基础知识。向服务器发出请求,服务器将返回响应。虽然HTTP不需要TCP,但它需要可靠的低级协议。实际上,这几乎总是TCP over IP(尽管Google正在尝试创建一个替代品)。如果你需要复习,那么可以查看Python中的Socket编程(指南)

就协议而言,HTTP是一种更简单的协议。它的目的是通过互联网发送内容,如HTML、视频、图像等。这是通过一个HTTP请求和响应完成的。HTTP请求包含以下元素:

  • method: 该方法描述客户端要执行的操作。静态内容的方法通常是GET,不过还有其他方法可用,比如POST、HEAD和DELETE。
  • path: 路径向服务器指示你要请求的网页。python的路径是https/示例。
  • version: 该版本是几种HTTP版本之一,如1.0、1.1或2.0。最常见的可能是1.1。
  • headers: 消息头有助于描述服务器的其他信息。
  • body: 主体向服务器提供来自客户端的信息。虽然这个字段不是必需的,但对于某些方法来说,通常有一个主体,比如POST。

这些是你的浏览器用于与服务器通信的工具。服务器用HTTP响应进行响应。HTTP响应包含以下元素:

  • version: 版本标识HTTP版本,它通常与请求的版本相同。
  • status code: 状态代码指示请求是否已成功完成。有相当多的状态码
  • status message: 状态消息提供了一个人类可读的消息,帮助描述状态代码。
  • headers: 消息头允许服务器使用关于请求的附加元数据来响应。这些与请求头的概念相同。
  • body: 正文承载内容。从技术上讲,这是可选的,但通常它包含有用的资源。

这些是HTTP的构建块。如果你有兴趣了解更多关于HTTP的信息,那么你可以查看一个概述页面来更深入地了解该协议。

什么是HTTPS?

既然你对HTTP有了更多的了解,那么什么是HTTPS?好消息是,你已经知道了!HTTPS代表安全超文本传输协议(HyperText Transfer Protocol Secure)。但本质上与HTTPS协议的含义相同。

HTTPS不会重写任何构建它的HTTP基础。相反,HTTPS由通过加密连接发送的常规HTTP组成。通常,这种加密的连接由TLS或SSL提供,这是加密协议,在信息通过网络发送之前对其进行加密。

注意:TLS和SSL是非常相似的协议,尽管SSL正在退出,TLS将取代它。这些协议之间的差异不在本教程的讨论范围之内。只要知道TLS是SSL的更新、更好的版本就足够了。

那么,为什么要制造这种分离呢?为什么不把复杂性引入HTTP协议本身呢?答案是可移植性。保护通信安全是一个重要而困难的问题,但是HTTP只是众多需要安全性的协议之一。在各种应用程序中还有无数其他应用程序:

  • 电子邮件
  • 即时消息
  • VoIP(IP语音)

还有其他!如果每一个协议都必须创建自己的安全机制,那么这个世界就不会那么安全,也会更加混乱。TLS通常被上述协议使用,它提供了一种通用的通信安全方法。

注意:这种协议分离是网络中的一个常见主题,以至于有了一个名字。OSI模型表示从物理介质一直到HTML呈现在这个页面上的通信!

你将在本教程中学习的几乎所有信息都将不仅仅适用于python https应用程序。你将学习安全通信的基础知识,以及它如何具体应用于HTTPS。

为什么HTTPS很重要?

安全通信对于提供安全的在线环境至关重要。随着世界上越来越多的人上网,包括银行和医疗网站,开发人员创建python https应用程序变得越来越重要。再说一次,HTTPS只是基于TLS或SSL的HTTP。TLS旨在为窃听者提供隐私。它还可以提供客户端和服务器的身份验证。

在本节中,你将通过执行以下操作来深入探讨这些概念:

  • 创建Python HTTPS服务器
  • 与Python HTTPS服务器通信
  • 捕捉这些通信
  • 分析这些信息

我们开始吧!

创建示例应用程序

假设你是一个很酷的Python俱乐部的领导者,叫做秘密松鼠。松鼠是秘密的,他们需要一个秘密的信息来参加他们的会议。作为领导者,你选择秘密消息,该消息在每次会议中都会更改。不过,有时候,你很难在会前与所有成员见面,告诉他们秘密信息!你决定设置一个秘密服务器,成员可以自己查看秘密消息。

注意:本教程中使用的示例代码不是为生产而设计的。它旨在帮助你学习HTTP和TLS的基础知识。请勿将此代码用于生产。下面的许多例子都有糟糕的安全实践。在本教程中,你将了解TLS,以及它可以帮助你更安全的一种方法。

您已经阅读了一些关于Real Python的教程,并决定使用一些您知道的依赖项:

  • 构建web应用程序的Flask
  • 作为生产服务器的uWSGI
  • 请求来执行你的服务器

要安装所有这些依赖项,可以使用pip:

$ pip install flask uwsgi requests

安装了依赖项之后,就可以开始编写应用程序了。在一个名为server.py,创建Flask应用程序:

# server.py
from flask import Flask

SECRET_MESSAGE = "fluffy tail"
app = Flask(__name__)

@app.route("/")
def get_secret_message():
    return SECRET_MESSAGE

每当有人访问服务器的根(/)路径时,这个Flask应用程序将显示秘密消息。这样,你就可以在机密服务器上部署应用程序并运行它:

$ uwsgi --http-socket 127.0.0.1:5683 --mount /=server:app

这个命令使用上面的Flask应用程序启动服务器。你从一个奇怪的端口开始,因为你不想让人们发现它,并拍拍自己的背,因为你这么鬼鬼祟祟!你可以通过访问http://localhost:5683在你的浏览器中。

既然《秘密松鼠》里的每个人都认识Python,你就决定帮助他们。你写了一个叫做client.py这将有助于他们获得秘密信息:

# client.py
import os
import requests

def get_secret_message():
    url = os.environ["SECRET_URL"]
    response = requests.get(url)
    print(f"The secret message is: {response.text}")

if __name__ == "__main__":
    get_secret_message()

只要设置了SECRET_URL环境变量,此代码就会打印出机密消息。在本例中,SECRET_URL是127.0.0.1:5683。所以,你的计划是给每个俱乐部成员一个秘密的网址,并告诉他们要保密和安全。

虽然这看起来不错,但请放心,事实并非如此!事实上,即使你在这个网站上输入用户名和密码,它仍然不安全。但是即使你的团队设法保证了URL的安全,你的秘密信息仍然不安全。为了演示为什么你需要了解一点有关监视网络流量的信息。为此,你将使用一个名为Wireshark的工具。

设置Wireshark

Wireshark是一种广泛用于网络和协议分析的工具。这意味着它可以帮助你查看网络连接上发生的事情。在本教程中,安装和设置Wireshark是可选的,但是如果你愿意,请随时关注。下载页面有几个可用的安装程序:

  • macOS 10.12及更高版本
  • Windows installer 64位
  • Windows installer 32位

如果你使用的是Windows或Mac,那么你应该能够下载相应的安装程序并按照提示进行操作。最后,你应该有一个正在运行的Wireshark。

如果你使用的是基于Debian的Linux环境,那么安装有点困难,但是仍然可以。您可以使用以下命令安装Wireshark:

$ sudo add-apt-repository ppa:wireshark-dev/stable
$ sudo apt-get update
$ sudo apt-get install wireshark
$ sudo wireshark

你会看到这样的屏幕:
wireshark
随着Wireshark的运行,是时候分析一些流量了!

你的数据不安全

当前客户端和服务器的运行方式不安全。HTTP将把所有的东西都清晰地发送给任何人看。这意味着,即使有人没有你的秘密网址,他们仍然可以看到你所做的一切,只要他们能监视你和服务器之间任何设备上的流量。

这对你来说应该是比较可怕的。毕竟,你不希望其他人出现在你秘密的松鼠会议上!你可以证明这一切正在发生。首先,如果服务器还没有运行,请启动它:

$ uwsgi --http-socket 127.0.0.1:5683 --mount /=server:app

这将在端口5683上启动烧瓶应用程序。接下来,你将在Wireshark中启动一个包捕获。此数据包捕获将帮助你查看进出服务器的所有流量。首先在Wireshark上选择环回Loopback:lo接口:
Wireshark loopback click
你可以看到环回Loopback:lo部分突出显示。这将指示Wireshark监视此端口的流量。你可以做得更好,并指定要捕获的端口和协议。你可以在捕获筛选器中键入端口5683,在显示筛选器中键入http
https://files.realpython.com/media/wireshark-port-5683-filter.3c86d723417d.png
绿色方框表示Wireshark对你输入的过滤器很满意(红色则表示输入有语法错误)。现在,你可以通过单击左上角的fin开始捕获:
https://files.realpython.com/media/wirehshark-click.d1a161bccdb9.png
单击此按钮将在Wireshark中生成一个新窗口:

https://files.realpython.com/media/wireshark-active-capture.17498bac9dac.png
这个新窗口相当简单,但底部的消息显示< live capture in progress >,这表示它正在工作。不要担心没有显示任何内容,因为这是正常的。为了让Wireshark报告任何事情,你的服务器上必须有一些活动。要获取一些数据,请尝试运行客户端:

$ SECRET_URL="http://127.0.0.1:5683" python client.py
The secret message is: fluffy tail

在执行client.py上面的代码,现在你应该可以看到Wireshark中的一些条目。如果一切顺利,那么你将看到两个条目,如下所示:
https://files.realpython.com/media/wireshark-http-two-entries.db53111bad3e.png
这两个条目代表发生的通信的两个部分。第一个是客户端对服务器的请求。单击第一个条目时,你将看到大量信息:
https://files.realpython.com/media/wireshark-http-request-1.ca48adcd8525.png
真是太多信息了!在顶部,仍然有HTTP请求和响应。一旦你选择了其中一个条目,你将看到中间和底部的行填充了信息。

中间一行提供了Wireshark能够为所选请求识别的协议的细目。这个分解允许你探索HTTP请求中实际发生了什么。下面是Wireshark在中间一行自上而下描述的信息的快速摘要:

  • 物理层:此行描述用于发送请求的物理接口。在你的例子中,这可能是你的环回接口的接口ID 0(lo)。
  • 以太网信息:这一行显示了第2层协议,其中包括源和目标MAC地址。
  • IPv4:此行显示源和目标IP地址(127.0.0.1)。
  • TCP:此行包括所需的TCP握手,以便创建可靠的数据管道。
  • HTTP:此行显示有关HTTP请求本身的信息。

当你展开超文本传输协议层时,你可以看到组成HTTP请求的所有信息:
https://files.realpython.com/media/wireshark-http-request-expanded.b832a33ff5d0.png
此图显示脚本的HTTP请求:

  • 方法:GET
  • 路径:/
  • 版本:1.1
  • 消息头:主机:127.0.0.1:5683,连接:保持活动状态,以及其他
  • 消息体:没有内容

你将看到的最后一行是数据的十六进制转储。你可能会注意到,在这个十六进制转储中,你实际上可以看到HTTP请求的各个部分。那是因为你的HTTP请求是公开发送的。但回答呢?如果单击HTTP响应,则会看到类似的视图:
在这里插入图片描述
同样,你有相同的三个部分。如果你仔细查看hex转储,那么你将看到明文形式的秘密消息!这对秘密松鼠来说是个大问题。这意味着任何有技术诀窍的人如果感兴趣,可以很容易地看到这些流量。那么,如何解决这个问题呢?答案是密码学。

密码学有何帮助?

在本节中,你将学习一种保持数据安全的方法,方法是创建自己的加密密钥,并在服务器和客户端上使用它们。虽然这不是你的最后一步,但它将帮助你为如何构建Python HTTPS应用奠定坚实的基础。

了解密码学基础知识

密码学是一种防止窃听者或对手进行通信的方法。另一种说明这一点的方法是获取普通信息,称为明文,并将其转换为加密文本,称为密文。

密码学一开始可能会让人望而生畏,但基本概念是很容易理解的。事实上,你可能已经练习过密码学了。如果你曾经和你的朋友使用过一种秘密语言,并在课堂上用它来传递笔记,那么你就已经练习过密码学了。(如果你还没有做到,那就别担心你马上就要做了。)

不管怎样,你需要将字符串“flufy tail”转换为无法理解的内容。一种方法是将某些字符映射到不同的字符上。一个有效的方法是将字符移回字母表中的一个位置。这样做看起来像这样:
https://files.realpython.com/media/alpha.12689a36982a.png

这幅图向你展示了如何从原始字母表转换为新字母表并返回。所以,如果你有消息ABC,那么你实际上会发送消息ZAB。如果你把这个应用到“毛茸茸的尾巴”,那么假设空间保持不变,你得到ekteex szhk。虽然它并不完美,但对于任何看到它的人来说,它可能看起来像胡言乱语。

祝贺你!你已经创建了在密码学中被称为密码的东西,它描述了如何将明文转换为密文并返回。在本例中,你的密码是用英语描述的。这种特殊类型的密码称为替代密码。从根本上说,这与谜机中使用的密码类型相同,尽管是一个简单得多的版本。

现在,如果你想把一条信息传给秘密松鼠,那么你首先需要告诉他们要移动多少个字母,然后给他们编码的信息。在Python中,可能如下所示:

CIPHER = {"a": "z", "A": "Z", "b": "a"} # And so on

def encrypt(plaintext: str):
    return "".join(CIPHER.get(letter, letter) for letter in plaintext)

这里,你已经创建了一个名为encrypt()的函数,它将获取明文并将其转换为密文。假设你有一个字典密码,它将所有字符都映射出来。类似地,你可以创建一个decrypt()

DECIPHER = {v: k for k, v in CIPHER.items()}

def decrypt(ciphertext: str):
    return "".join(DECIPHER.get(letter, letter) for letter in ciphertext)

此函数与encrypt()相反。它将接受密文并将其转换为明文。在这种形式的密码中,你有一个用户需要知道的特殊密钥,以便对消息进行加密和解密。对于上面的例子,这个键是1。也就是说,密码指示你应该将每个字母向后移动一个字符。密钥对于保密非常重要,因为任何拥有密钥的人都可以轻松地解密你的消息。

注意:虽然你可以使用此加密,但这仍然不是非常安全。使用频率分析,这个密码很快就被破解了,而且对于秘密松鼠来说太原始了。

在现代,密码学要先进得多。它依靠复杂的数学理论和计算机科学来保证安全。虽然这些密码背后的数学不在本教程的讨论范围之内,但底层的概念仍然是相同的。你有一个密码,它描述了如何将明文转换成密文。

替代密码和现代密码之间唯一真正的区别是,现代密码在数学上被证明是无法被窃听者破解的。现在,让我们看看如何使用你的新密码。

在Python HTTPS应用程序中使用加密技术

幸运的是,你不必是数学或计算机科学方面的专家才能使用密码学。Python还有一个secrets模块,可以帮助你生成加密安全的随机数据。在本教程中,你将了解一个名为cryptography的Python库。它在PyPI上可用,因此你可以使用pip安装它:

$ pip install cryptography

这将在你的虚拟环境中安装加密技术。安装了密码学之后,你现在可以使用Fernet方法以数学上安全的方式加密和解密内容。

回想一下你密码里的密钥是1。同样,你需要为Fernet创建一个关键点才能正常工作:

>>> from cryptography.fernet import Fernet
>>> key = Fernet.generate_key()
>>> key
b'8jtTR9QcD-k3RO9Pcd5ePgmTu_itJQt9WKQPzqjrcoM='

在这段代码中,你导入了Fernet并生成了一个密钥。这个密钥只是一堆字节,但是你要把这个密钥保密和安全是非常重要的。就像上面的替换示例一样,任何拥有此密钥的人都可以轻松地解密你的消息。

注意:在现实生活中,你应该把这把钥匙放在非常安全的地方。在这些示例中,查看密钥是很有帮助的,但这是一种不好的做法,尤其是如果你在公共网站上发布它!换言之,不要使用上面看到的密钥来保证安全

这个键的行为很像前面的键。它需要从密文到明文的转换。现在是有趣的时候了!你可以这样加密消息:

>>> my_cipher = Fernet(key)
>>> ciphertext = my_cipher.encrypt(b"fluffy tail")
>>> ciphertext
b'gAAAAABdlW033LxsrnmA2P0WzaS-wk1UKXA1IdyDpmHcV6yrE7H_ApmSK8KpCW-6jaODFaeTeDRKJMMsa_526koApx1suJ4_dQ=='

在这段代码中,你已经创建了一个名为my_cipher的Fernet对象,你可以使用它来加密消息。请注意,你的秘密消息“fluffy tail”需要是bytes对象才能对其进行加密。加密之后,你可以看到密文是一个很长的字节流。

多亏了费内特,这个密文在没有钥匙的情况下无法被操纵或读取!这种类型的加密要求服务器和客户端都可以访问密钥。当双方都需要相同的密钥时,这称为对称加密。在下一节中,你将看到如何使用这种对称加密来保证数据的安全。

你的数据是安全的

现在,你已经了解了Python中加密的一些基础知识,可以将这些知识应用到服务器上。创建一个名为symmetric_server.py的新文件:

# symmetric_server.py
import os
from flask import Flask
from cryptography.fernet import Fernet

SECRET_KEY = os.environb[b"SECRET_KEY"]
SECRET_MESSAGE = b"fluffy tail"
app = Flask(__name__)

my_cipher = Fernet(SECRET_KEY)

@app.route("/")
def get_secret_message():
    return my_cipher.encrypt(SECRET_MESSAGE)

这段代码将你的最初的服务器代码与你在上一节中使用的Fernet对象相结合。密钥现在作为bytes对象使用os.environb从环境中读取。有了服务器,你现在可以将注意力集中在客户机上。将以下内容粘贴到symmetric_client.py:

# symmetric_client.py
import os
import requests
from cryptography.fernet import Fernet

SECRET_KEY = os.environb[b"SECRET_KEY"]
my_cipher = Fernet(SECRET_KEY)

def get_secret_message():
    response = requests.get("http://127.0.0.1:5683")

    decrypted_message = my_cipher.decrypt(response.content)
    print(f"The codeword is: {decrypted_message}")

if __name__ == "__main__":
    get_secret_message()

再次,这是一个修改后的代码,将你以前的客户机与Fernet加密机制相结合。get_secret_message() 执行以下操作:

  • 向服务器发出请求。
  • 从响应中获取原始字节。
  • 尝试解密原始字节。
  • 打印解密的消息。

如果你同时运行服务器和客户端,那么你将看到你成功地加密和解密你的机密消息:

$ uwsgi --http-socket 127.0.0.1:5683 \
    --env SECRET_KEY="8jtTR9QcD-k3RO9Pcd5ePgmTu_itJQt9WKQPzqjrcoM=" \
    --mount /=symmetric_server:app

在这个调用中,你再次在端口5683上启动服务器。这一次,你将传递一个密钥,该密钥必须至少是一个32长度的base64编码字符串。重新启动服务器后,你现在可以查询它:

$ SECRET_KEY="8jtTR9QcD-k3RO9Pcd5ePgmTu_itJQt9WKQPzqjrcoM=" python symmetric_client.py
The secret message is: b'fluffy tail'

哇哈!你可以对邮件进行加密和解密。如果你尝试使用无效的密钥运行此程序,则会收到一个错误:

$ SECRET_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" python symmetric_client.py
Traceback (most recent call last):
  File ".../cryptography/fernet.py", line 104, in _verify_signature
    h.verify(data[-32:])
  File ".../cryptography/hazmat/primitives/hmac.py", line 66, in verify
    ctx.verify(signature)
  File ".../cryptography/hazmat/backends/openssl/hmac.py", line 74, in verify
    raise InvalidSignature("Signature did not match digest.")
cryptography.exceptions.InvalidSignature: Signature did not match digest.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "symmetric_client.py", line 16, in <module>
    get_secret_message()
  File "symmetric_client.py", line 11, in get_secret_message
    decrypted_message = my_cipher.decrypt(response.content)
  File ".../cryptography/fernet.py", line 75, in decrypt
    return self._decrypt_data(data, timestamp, ttl)
  File ".../cryptography/fernet.py", line 117, in _decrypt_data
    self._verify_signature(data)
  File ".../cryptography/fernet.py", line 106, in _verify_signature
    raise InvalidToken
cryptography.fernet.InvalidToken

所以,你知道加密和解密是有效的。但安全吗?是的,是的。为了证明这一点,你可以返回Wireshark并使用与以前相同的过滤器开始新的捕获。完成捕获设置后,再次运行客户端代码:

$ SECRET_KEY="8jtTR9QcD-k3RO9Pcd5ePgmTu_itJQt9WKQPzqjrcoM=" python symmetric_client.py
The secret message is: b'fluffy tail'

你已经完成了另一个成功的HTTP请求和响应,再一次在Wireshark中看到这些消息。由于机密消息只在响应中传输,因此可以单击该消息查看数据:
https://files.realpython.com/media/wireshark-symmetric-http-response.67ab18c74759.png
在这张图片的中间一行,你可以看到实际传输的数据:

gAAAAABdlXSesekh9LYGDpZE4jkxm4Ai6rZQg2iHaxyDXkPWz1O74AB37V_a4vabF13fEr4kwmCe98Wlr8Zo1XNm-WjAVtSgFQ==

令人惊叹的!这意味着数据是加密的,窃听者不知道消息内容到底是什么。不仅如此,这还意味着他们可能会花费大量的时间来暴力破解这些数据,而且他们几乎永远不会成功。
你的数据是安全的!但是等一下,当你以前使用Python HTTPS应用程序时,你不必知道任何关于密钥的信息。这是因为HTTPS不完全使用对称加密。事实证明,分享秘密是个难题。
要证明这一概念,请导航到http://127.0.0.1:5683,你将看到加密的响应文本。这是因为你的浏览器不知道你的秘密密码。那么python https应用程序是如何工作的呢?这就是非对称加密发挥作用的地方。

如何共享密钥?

在上一节中,你了解了如何使用对称加密在数据通过Internet时保持安全。尽管对称加密是安全的,但它并不是Python-HTTPS应用程序用来保证数据安全的唯一加密技术。对称加密引入了一些不容易解决的基本问题。

注意:请记住,对称加密要求在客户端和服务器之间有一个共享密钥。不幸的是,安全性只和最弱的链接一样难工作,而弱链接在对称加密中尤其是灾难性的。一旦一个人泄露了密钥,那么每个密钥都会被泄露。可以肯定的是,任何安全系统在某个时候都会受到危害。

那么,你怎么换钥匙?如果你只有一个服务器和一个客户端,那么这可能是一个快速的任务。但是,随着客户机和服务器的增多,为了有效地更改密钥和保护机密,需要进行越来越多的协调。

而且,你每次都要选择一个新的秘密。在上面的示例中,你看到了一个随机生成的密钥。你几乎不可能试着让人们记住那把钥匙。随着客户机和服务器数量的增加,你可能会使用更容易记住和猜测的密钥。

如果你能改变你的钥匙,那么你还有一个问题要解决。如何共享初始密钥?在Secret Squirrels示例中,通过物理访问每个成员来解决这个问题。你可以亲自告诉每个成员这个秘密,让他们保守秘密,但要记住,有人是最薄弱的环节。

现在,假设你从另一个物理位置向Secret Squirrels添加一个成员。你如何与这个成员分享这个秘密?每次钥匙换了,你都让他们坐飞机来找你吗?如果你能把密钥放在你的服务器上并自动共享它就好了。不幸的是,这将破坏加密的全部目的,因为任何人都可以获得密钥!

当然,你可以给每个人一个初始主密钥来获取机密消息,但是现在你遇到的问题是以前的两倍。如果你的头受伤了,别担心!你不是唯一一个。

你需要的是让从未交流过的两方拥有共同的秘密。听起来不可能,对吧?幸运的是,三个叫拉尔夫·梅克尔、惠特菲尔德·迪菲和马丁·赫尔曼的人支持你。他们帮助证明了公钥加密,也就是非对称加密是可能的。

注:虽然惠特菲尔德·迪菲和马丁·赫尔曼是最早发现这一计划的人,但1997年有消息称,在GCHQ工作的三名男子,分别是詹姆斯·H·埃利斯、克利福德·考克斯和马尔科姆·J·威廉森,曾在七年前展示过这种能力!

非对称加密允许两个从未交流过的用户共享一个共同的秘密。理解基本原理最简单的方法之一是使用颜色类比。假设你有以下场景:
https://files.realpython.com/media/dh-initial.6b8a9b7877c3.png
在这个图表中,你试图与一只你从未见过的秘密松鼠交流,但是间谍可以看到你发送的所有信息。你知道对称加密并希望使用它,但首先需要共享一个秘密。幸运的是,你们俩都有私钥。不幸的是,你不能发送你的私钥,因为间谍会看到它。你是做什么的?

你需要做的第一件事就是和你的伴侣在颜色上达成一致,比如黄色:
https://files.realpython.com/media/dh-2.f5ab3fbf2421.png
注意这里间谍可以看到共享的颜色,你和秘密松鼠也可以。共享颜色实际上是公开的。现在你和Secret Squirrel可以把你的私钥和共享的颜色结合起来:
https://files.realpython.com/media/dh-3.ad7db1b0f304.png
你的颜色组合成绿色,而神秘松鼠的颜色组合成橙色。两人都已完成共享颜色,现在需要彼此共享组合颜色:
https://files.realpython.com/media/dh-4.9d2ac2bff7c9.png
你现在有了你的私钥和秘密松鼠的组合颜色。同样,秘密松鼠有他们的私钥和你的组合颜色。你和神秘松鼠很快就把你们的颜色结合起来了。

然而,间谍只有这些组合的颜色。要想弄清楚你的原色是非常困难的,即使是给定初始的共享颜色。间谍得去商店买很多不同的蓝色来试试。即便如此,也很难知道他们在组合后是否看到了正确的绿色阴影!简而言之,你的私钥仍然是私有的。

但是你和那只神秘的松鼠呢?你还没有一个合二为一的秘密!这就是你的私钥回来的地方。如果将私钥与从“秘密松鼠”接收到的组合颜色组合在一起,则两种颜色都将相同:
https://files.realpython.com/media/dh-5.57ffde26feed.png
现在,你和秘密松鼠有着相同的秘密颜色。你现在已经成功地与一个完全陌生的人分享了一个安全的秘密。这对于公钥密码术的工作原理非常准确。此事件序列的另一个常见名称是Diffie-Hellman密钥交换。密钥交换由以下部分组成:

  • 私钥是示例中的私有颜色。
  • 公钥是你共享的组合颜色。

私钥是你始终保持私有的东西,而公钥可以与任何人共享。这些概念直接映射到pythonhttps应用程序的真实世界。既然服务器和客户机有了一个共享的秘密,你可以使用旧的pal对称加密来加密所有进一步的消息!

注意:公钥密码也依赖于一些数学来进行颜色混合。Diffie-Hellman密钥交换的Wikipedia页面有很好的解释,但是深入的解释不在本教程的范围之内。

当你通过安全网站(如本网站)进行通信时,你的浏览器和服务器使用以下相同的原则来设置安全通信:

  • 你的浏览器从服务器请求信息。
  • 你的浏览器和服务器交换公钥。
  • 你的浏览器和服务器生成共享私钥。
  • 你的浏览器和服务器使用此共享密钥通过对称加密对消息进行加密和解密。

幸运的是,你不需要实现这些细节。有许多内置的和第三方的库可以帮助你保持客户端和服务器通信的安全。

HTTPS在现实世界中是什么样的?

考虑到所有关于加密的信息,让我们缩小一点,讨论一下python https应用程序在现实世界中是如何工作的。加密只是事情的一半。访问安全网站时,需要两个主要组件:

  • 加密将明文转换为密文并返回。
  • 身份验证验证一个人或一件事是否是他们所说的人或事。

你已经听过很多关于加密是如何工作的,但是身份验证呢?要了解真实世界中的身份验证,你需要了解公钥基础设施。PKI在安全生态系统中引入了另一个重要概念,称为证书

证书就像互联网的护照。像计算机世界的大多数东西一样,它们只是文件中的数据块。一般来说,证书包括以下信息:

  • 颁发给:标识证书的所有者
  • 颁发者:标识证书的颁发者
  • 有效期:标识证书有效的时间范围

就像护照一样,证书只有在由某个权威机构生成和认可时才真正有用。让你的浏览器知道你在因特网上访问的每一个站点的每一个证书是不切实际的。相反,PKI依赖于一个称为证书颁发机构(CA)的概念。

证书颁发机构负责颁发证书。在PKI中,它们被认为是可信的第三方(TTP)。本质上,这些实体充当证书的有效权限。假设你想访问另一个国家,你有一本护照,上面有你所有的信息。外国的移民官员怎么知道你的护照上有有效信息?

如果你自己填写所有的信息并签字,那么每个你想访问的国家的移民官员都需要亲自认识你,并且能够证明那里的信息确实是正确的。

另一种处理此问题的方法是将所有信息发送到可信第三方(TTP)。TTP会对你提供的信息进行彻底调查,核实你的申请,然后在你的护照上签字。事实证明,这是更实际的,因为移民官员只需要知道可信的第三方。

TTP场景是在实践中如何处理证书。过程如下:

  • 创建证书签名请求(CSR):这类似于填写签证信息。
  • 将CSR发送给可信的第三方(TTP):这就像把你的信息发送到签证申请办公室一样。
  • 验证你的信息:不知何故,TTP需要验证你提供的信息。作为一个例子,看看Amazon如何验证所有权。
  • 生成一个公钥:TTP签署你的CSR。这相当于TTP签署你的签证。
  • 签发已验证的公钥:这相当于你在邮件中收到签证。

请注意,CSR以加密方式与你的私钥绑定。因此,这三个信息公钥、私钥和证书颁发机构都以某种方式相关。这将创建所谓的信任链,因此你现在有了一个可用于验证身份的有效证书。

大多数情况下,这只是网站所有者的责任。网站所有者将遵循所有这些步骤。在这个过程结束时,他们的证书上写着:

From time A to time B I am X according to Y

这句话就是证书真正告诉你的。变量可按如下方式填写:

  • A是有效的开始日期和时间。
  • B是有效的结束日期和时间。
  • X是服务器的名称。
  • Y是证书颁发机构的名称。

从根本上说,这是证书描述的全部内容。换言之,拥有证书并不一定意味着你就是你所说的你自己,只是你必须承认你是你所说的自己。这就是可信第三方的“可信”部分。

TTP需要在客户机和服务器之间共享,以便每个人都对HTTPS握手感到高兴。你的浏览器自动安装了许多证书颁发机构。要查看它们,请执行以下步骤:

  • Chrome:转到“设置”>“高级”>“隐私和安全”>“管理证书”>“权限”。
  • Firefox:前往“设置”>“首选项”>“隐私和安全性”>“查看证书”>“权限”。

这涵盖了在现实世界中创建python https应用程序所需的基础设施。在下一节中,你将把这些概念应用到你自己的代码中。你将浏览最常见的示例,并成为你自己的秘密松鼠证书颁发机构!

Python的HTTPS应用程序是什么样子的?

现在,你已经了解了制作python https应用程序所需的基本部分,现在是时候将所有这些部分一个一个地绑定到应用程序上了。这将确保服务器和客户端之间的通信是安全的。

可以在你自己的机器上设置整个PKI基础设施,这正是你在本节中要做的。这不像听起来那么难,所以别担心!成为一个真正的证书颁发机构要比执行下面的步骤困难得多,但你将了解到,运行自己的CA所需要的或多或少都是这样的。

成为证书颁发机构

证书颁发机构只不过是一个非常重要的公钥和私钥对。要成为CA,只需生成一个公钥和私钥对。

注意:成为一个供公众使用的CA是一个非常艰难的过程,尽管有很多公司遵循了这个过程。然而,到本教程结束时,你将不会成为这些公司中的一员!

你的初始公钥和私钥对将是一个自签名证书。你是在生成初始机密,因此,如果你实际上要成为CA,则此私钥的安全性至关重要。 如果某人可以访问CA的公钥和私钥对,那么他们可以生成一个完全有效的证书,并且除了停止信任你的CA外,你无法采取任何措施来检测出该问题。

有了这个警告,你可以立即生成证书。 对于初学者,你需要生成一个私钥。 将以下内容粘贴到名为pki_helpers.py的文件中:

# pki_helpers.py
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

def generate_private_key(filename: str, passphrase: str):
    private_key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )

    utf8_pass = passphrase.encode("utf-8")
    algorithm = serialization.BestAvailableEncryption(utf8_pass)

    with open(filename, "wb") as keyfile:
        keyfile.write(
            private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.TraditionalOpenSSL,
                encryption_algorithm=algorithm,
            )
        )

    return private_key

generate_private_key() 使用RSA生成私钥。 这是代码的细分:

  • 第2至4行导入了该功能正常运行所需的库。
  • 第7至9行使用RSA生成私钥。 幻数65537和2048只是两个可能的值。 你可以阅读有关为什么或只是相信这些数字有用的更多信息。
  • 第11至12行设置了要在你的私钥上使用的加密算法。
  • 第14到21行将你的私钥以指定的文件名写入磁盘。 使用提供的密码对该文件进行加密。

成为自己的CA的下一步是生成一个自签名的公共密钥。你可以绕过证书签名请求(CSR)并立即构建公共密钥。 将以下内容粘贴到pki_helpers.py中:

# pki_helpers.py
from datetime import datetime, timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes

def generate_public_key(private_key, filename, **kwargs):
    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, kwargs["country"]),
            x509.NameAttribute(
                NameOID.STATE_OR_PROVINCE_NAME, kwargs["state"]
            ),
            x509.NameAttribute(NameOID.LOCALITY_NAME, kwargs["locality"]),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, kwargs["org"]),
            x509.NameAttribute(NameOID.COMMON_NAME, kwargs["hostname"]),
        ]
    )

    # Because this is self signed, the issuer is always the subject
    issuer = subject

    # This certificate is valid from now until 30 days
    valid_from = datetime.utcnow()
    valid_to = valid_from + timedelta(days=30)

    # Used to build the certificate
    builder = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(valid_from)
        .not_valid_after(valid_to)
    )

    # Sign the certificate with the private key
    public_key = builder.sign(
        private_key, hashes.SHA256(), default_backend()
    )

    with open(filename, "wb") as certfile:
        certfile.write(public_key.public_bytes(serialization.Encoding.PEM))

    return public_key

在这里,你有一个新函数generate_public_key(),它将生成一个自签名的公共密钥。 此代码的工作原理如下:

  • 第2到5行是功能正常运行所必需的导入。
  • 第8至18行建立了有关证书主题的信息。
  • 第21行使用相同的颁发者和主题,因为这是自签名证书。
  • 第24到25行表示此公共密钥有效的时间范围。 在这种情况下,需要30天。
  • 第28至36行将所有必需的信息添加到公共密钥构建器对象,然后需要对其进行签名。
  • 第38至41行用私钥对公钥进行签名。
  • 第43至44行将公共密钥写出到文件名中。

使用这两个函数,你可以在Python中非常快速地生成私钥和公钥对:

>>> from pki_helpers import generate_private_key, generate_public_key
>>> private_key = generate_private_key("ca-private-key.pem", "secret_password")
>>> private_key
<cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7ffbb292bf90>
>>> generate_public_key(
...   private_key,
...   filename="ca-public-key.pem",
...   country="US",
...   state="Maryland",
...   locality="Baltimore",
...   org="My CA Company",
...   hostname="my-ca.com",
... )
<Certificate(subject=<Name(C=US,ST=Maryland,L=Baltimore,O=My CA Company,CN=logan-ca.com)>, ...)>

从pki_helpers导入辅助函数后,首先生成私钥并将其保存到文件ca-private-key.pem中。 然后,你将该私钥传递到generate_public_key() 中以生成你的公钥。 在目录中,现在应该有两个文件:

$ ls ca*
ca-private-key.pem ca-public-key.pem

恭喜你! 你现在可以成为证书颁发机构。

信任你的服务器

使服务器成为受信任的第一步是为你生成证书签名请求(CSR)。 在现实世界中,CSR将发送到实际的证书颁发机构,例如Verisign或Let’s Encrypt。 在此示例中,你将使用刚刚创建的CA。

从上面将生成CSR的代码粘贴到pki_helpers.py文件中:

# pki_helpers.py
def generate_csr(private_key, filename, **kwargs):
    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, kwargs["country"]),
            x509.NameAttribute(
                NameOID.STATE_OR_PROVINCE_NAME, kwargs["state"]
            ),
            x509.NameAttribute(NameOID.LOCALITY_NAME, kwargs["locality"]),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, kwargs["org"]),
            x509.NameAttribute(NameOID.COMMON_NAME, kwargs["hostname"]),
        ]
    )

    # Generate any alternative dns names
    alt_names = []
    for name in kwargs.get("alt_names", []):
        alt_names.append(x509.DNSName(name))
    san = x509.SubjectAlternativeName(alt_names)

    builder = (
        x509.CertificateSigningRequestBuilder()
        .subject_name(subject)
        .add_extension(san, critical=False)
    )

    csr = builder.sign(private_key, hashes.SHA256(), default_backend())

    with open(filename, "wb") as csrfile:
        csrfile.write(csr.public_bytes(serialization.Encoding.PEM))

    return csr

在大多数情况下,此代码与生成原始公钥的方式相同。 主要区别如下:

  • 第16至19行设置了备用DNS名称,该名称对你的证书有效。
  • 第21至25行生成了一个不同的构建器对象,但是相同的基本原理仍然适用。 你正在为企业社会责任建立所有必需的属性。
  • 第27行使用私钥在你的CSR上签名。
  • 第29至30行以PEM格式将CSR写入磁盘。

你会注意到,要创建CSR,首先需要一个私钥。 幸运的是,你可以在创建CA的私钥时使用相同的generate_private_key()。 使用上述功能和定义的先前方法,你可以执行以下操作:

>>> from pki_helpers import generate_csr, generate_private_key
>>> server_private_key = generate_private_key(
...   "server-private-key.pem", "serverpassword"
... )
>>> server_private_key
<cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f6adafa3050>
>>> generate_csr(
...   server_private_key,
...   filename="server-csr.pem",
...   country="US",
...   state="Maryland",
...   locality="Baltimore",
...   org="My Company",
...   alt_names=["localhost"],
...   hostname="my-site.com",
... )
<cryptography.hazmat.backends.openssl.x509._CertificateSigningRequest object at 0x7f6ad5372210>

在控制台中运行这些步骤之后,应该以两个新文件结束:

  • server-private-key.pem:你服务器的私钥
  • server-csr.pem:服务器的CSR

你可以从控制台查看新的CSR和私钥:

$ ls server*.pem
server-csr.pem  server-private-key.pem

有了这两个文档,你现在就可以开始对密钥进行签名的过程了。 通常,此步骤中会进行大量验证。 在现实世界中,CA将确保你拥有my-site.com,并要求你以各种方式对其进行证明。

由于在这种情况下你是CA,因此可以避免麻烦的是创建自己的经过验证的公共密钥。 为此,你需要在pki_helpers.py文件中添加另一个功能:

# pki_helpers.py
def sign_csr(csr, ca_public_key, ca_private_key, new_filename):
    valid_from = datetime.utcnow()
    valid_until = valid_from + timedelta(days=30)

    builder = (
        x509.CertificateBuilder()
        .subject_name(csr.subject)
        .issuer_name(ca_public_key.subject)
        .public_key(csr.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(valid_from)
        .not_valid_after(valid_until)
    )

    for extension in csr.extensions:
        builder = builder.add_extension(extension.value, extension.critical)

    public_key = builder.sign(
        private_key=ca_private_key,
        algorithm=hashes.SHA256(),
        backend=default_backend(),
    )

    with open(new_filename, "wb") as keyfile:
        keyfile.write(public_key.public_bytes(serialization.Encoding.PEM))

此代码看起来与generate_ca.py文件中的generate_public_key()非常相似。 实际上,它们几乎是相同的。 主要区别如下:

  • 第8至9行的主题名称基于CSR,而颁发者基于证书颁发机构。
  • 第10行这次是从CSR获取公钥。
  • 第16至17行复制在CSR上设置的所有扩展名。
  • 第20行使用CA的私钥在公钥上签名。

下一步是启动Python控制台并使用sign_csr()。 你需要加载CSR和CA的私钥和公钥。 首先加载你的CSR:

>>> from cryptography import x509
>>> from cryptography.hazmat.backends import default_backend
>>> csr_file = open("server-csr.pem", "rb")
>>> csr = x509.load_pem_x509_csr(csr_file.read(), default_backend())
>>> csr
<cryptography.hazmat.backends.openssl.x509._CertificateSigningRequest object at 0x7f68ae289150>

在本部分代码中,你将打开server-csr.pem文件,并使用x509.load_pem_x509_csr()创建csr对象。 接下来,你需要加载CA的公钥:

>>> ca_public_key_file = open("ca-public-key.pem", "rb")
>>> ca_public_key = x509.load_pem_x509_certificate(
...   ca_public_key_file.read(), default_backend()
... )
>>> ca_public_key
<Certificate(subject=<Name(C=US,ST=Maryland,L=Baltimore,O=My CA Company,CN=logan-ca.com)>, ...)>

再一次,你创建了一个ca_public_key对象,sign_csr()可以使用该对象。 x509模块提供了方便的load_pem_x509_certificate()帮助。 最后一步是加载CA的私钥:

>>> from getpass import getpass
>>> from cryptography.hazmat.primitives import serialization
>>> ca_private_key_file = open("ca-private-key.pem", "rb")
>>> ca_private_key = serialization.load_pem_private_key(
...   ca_private_key_file.read(),
...   getpass().encode("utf-8"),
...   default_backend(),
... )
Password:
>>> private_key
<cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f68a85ade50>

此代码将加载你的私钥。 回想一下,你的私钥已使用你指定的密码加密。 使用这三个组件,你现在可以签署CSR并生成经过验证的公共密钥:

>>> from pki_helpers import sign_csr
>>> sign_csr(csr, ca_public_key, ca_private_key, "server-public-key.pem")

运行此命令后,你的目录中应该有三个服务器密钥文件:

$ ls server*.pem
server-csr.pem  server-private-key.pem  server-public-key.pem

哟! 过去那是很多工作。 好消息是,既然你已经拥有了私钥和公钥对,则无需更改任何服务器代码即可开始使用它。

使用最初的server.py文件,运行以下命令以启动全新的Python HTTPS应用程序:

$ uwsgi \
    --master \
    --https localhost:5683,\
            logan-site.com-public-key.pem,\
            logan-site.com-private-key.pem \
    --mount /=server:app

恭喜你! 现在,你将具有一个运行Python的启用HTTPS的服务器,该服务器带有你自己的私钥-公钥对,该服务器由你自己的证书颁发机构签名!

注意:Python的HTTPS身份验证公式还有另一面,那就是客户端。也可以为客户端证书设置证书验证。这需要更多的工作,在企业外部是看不到的。然而,客户端身份验证是一个非常强大的工具。

现在,剩下要做的就是查询你的服务器。 首先,你需要对client.py代码进行一些更改:

# client.py
import os
import requests

def get_secret_message():
    response = requests.get("https://localhost:5683")
    print(f"The secret message is {response.text}")

if __name__ == "__main__":
    get_secret_message()

之前代码的唯一变化是从http到https。 如果你尝试运行此代码,则会遇到错误:

$ python client.py
...
requests.exceptions.SSLError: \
    HTTPSConnectionPool(host='localhost', port=5683): \
    Max retries exceeded with url: / (Caused by \
    SSLError(SSLCertVerificationError(1, \
    '[SSL: CERTIFICATE_VERIFY_FAILED] \
    certificate verify failed: unable to get local issuer \
    certificate (_ssl.c:1076)')))

那真是令人讨厌的错误消息! 这里的重要部分是消息certificate verify failed: unable to get local issuer(证书验证失败:无法获取本地颁发者)。 这些词现在应该让你更加熟悉。 本质上,这是在说以下内容:

localhost:5683给了我一个证书。 我检查了颁发给我的证书的颁发者,根据我所知道的所有证书颁发机构,该颁发者不是其中之一。

如果你尝试使用浏览器导航到你的网站,则会收到类似的消息:
https://files.realpython.com/media/unsafe-chrome.dedcf7161bb6.png
如果要避免显示此消息,则必须告知有关证书颁发机构的要求! 你需要做的只是将请求指向你之前生成的ca-public-key.pem文件:

# client.py
def get_secret_message():
    response = requests.get("http://localhost:5683", verify="ca-public-key.pem")
    print(f"The secret message is {response.text}")

完成之后,你应该能够成功运行以下命令:

$ python client.py
The secret message is fluffy tail

巴适! 你已经制作了功能齐全的Python HTTPS服务器,并成功对其进行了查询。你和秘密松鼠现在拥有可以安全快乐地来回交易的消息!

结论

在本教程中,你已经了解了当今Internet上安全通信的一些核心基础。 了解了这些基本要素后,你将成为一个更好,更安全的开发人员。

在本教程中,你已经对以下几个主题有所了解:

  • 密码学
  • HTTPS和TLS
  • 公钥基础设施
  • 证明书

如果这个信息让你感兴趣,那你就走运了!你几乎没有触及每一层的细微差别。安全世界在不断发展,新技术和漏洞不断被发现。如果你还有问题,可以在下面的评论区或Twitter上联系。

相关视频教程(部分免费)

关于作者

嗨,我是Logan,一个开源贡献者,Real Python的作者之一,软件开发人员,一直在努力做得更好。随时伸出援手,让我们一起变得更好!

Hi, I’m Logan, an open source contributor, writer for Real Python, software developer, and always trying to get better. Feel free to reach out and let’s get better together!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值