考虑到所有这些关于加密的信息,让我们把范围缩小一点,讨论一下 Python HTTPS 应用在真实的项目中的实际方式,加密只是事情的一半,访问安全网站时,需要两个主要组件:

  • 加密:将明文转换为密文并返回。
  • 身份认证:验证某人或事物是否名副其实。

你已经了解了关于加密的工作原理,但是如何身份认证?要了解真实项目中的身份认证,需要了解公钥基础结构(PKI)。PKI在安全生态系统中引入了另一个重要概念:证书。

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

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

就像护照一样,证书只有在由权威机构生成和认可的情况下才真正有用。你的浏览器不可能知道你在互联网上访问的每个站点的每个证书,相反,PKI依赖于一个称为证书颁发机构(CA)的概念。

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

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

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

TTP是如何在实践中处理证书的?过程如下:

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

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

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

根据Y,从时间A和时间B期间,我是X

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

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

基本上,这都是证书描述的。换句话说,有证书并不一定意味着你就是你所说的那个人,只是你让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行生成不同的生成器对象,但同样的基本原则与以前一样适用。你正在为CSR构建所有必需的属性。
  • 第27行用私钥签署CSR。
  • 第29至30行将CSR以PEM格式写入磁盘。

你会注意到,为了创建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,而颁发者基于证书颁发机构(CA)。
  • 第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-u 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的服务器,它运行着你自己的私钥-公钥对,私钥-公钥对是由你自己的证书颁发机构签署的!

现在,剩下要做的就是查询服务器。首先,需要对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)')))

这是一个非常糟糕的错误信息!这里的重要部分是信息证书验证失败:无法获取本地颁发者。你现在应该更熟悉这些词了。从本质上讲,它是这样说的:

`localhost:5683` gave me a certificate. I checked the issuer of the certificate it gave me, and according to all the Certificate Authorities I know about, that issuer is not one of them.

如果尝试使用浏览器打开你的网站,则会收到类似信息:

image-24

如果要避免此信息,你必须返回有关你的证书颁发机构!只需将请求指向你先前生成的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上安全通信的一些核心基础,现在已经了解了这些构建模块,你将成为一个更好、更安全的开发人员。

如果你对这些信息感兴趣,那你就走运了!你仅仅蜻蜓点水式地触及了每一层中所有的细微差别。安全世界不断发展,新的技术和漏洞也不断被发现。