【譯】通俗易懂的HTTPS

翻譯:老齊python

你有沒有想過爲何你能夠經過互聯網發送你的信用卡信息?你可能已經注意到了瀏覽器地址欄中的https:// ,但它是什麼?它如何保證你的信息安全?或者你可能想要建立一個Python HTTPS應用程序,但你並不徹底肯定這意味着什麼。如何確保你的web應用是安全的?git

你可能會驚訝地發現,不用成爲安全專家,也能能回答這些問題!在本文中,你就能獲得相關的知識,這些知識組合在一塊兒,可確保網絡通訊安全。你將看到一些具體示例,這些示例展現了Python HTTPS如何保證信息安全。github

在本文中,你將學到:web

  • 監視和分析網絡流量
  • 應用加密技術保證數據安全
  • 描述公鑰(PKI)的核心概念
  • 建立你本身的證書頒發機構
  • 構建Python HTTPS應用程序
  • 識別常見的Python HTTPS警告和錯誤

什麼是HTTP?

在深刻了解HTTPS及其在Python中的使用以前,瞭解它的上一代HTTP是很重要的。HTTP是HyperText Transfer Protocol(超文本傳輸協議)的縮寫,它支持瀏覽網站時的通訊。更具體地說,HTTP是用戶端(如web瀏覽器)與web服務器(如itdiffer.com)通訊的方式。下面是HTTP通訊的簡化圖:算法

這個圖表顯示了計算機與服務器通訊的流程,下面對每一步給予分解說明:flask

  1. 告訴瀏覽器訪問http://qiwsir.github.io/。
  2. 你的設備和服務器創建了TCP鏈接。
  3. 瀏覽器向服務器發送HTTP請求。
  4. 服務器接收HTTP請求並對其進行解析。
  5. 服務器藉助HTTP響應產生反應。
  6. 計算機接收、解析並顯示響應。

這個分解說明包含了HTTP的基本知識,向服務器發出請求,服務器返回響應。雖然HTTP不須要TCP,但它確實須要可靠的低級協議。在實踐中,幾乎老是基於IP實現TCP(儘管谷歌試圖建立一個替代品)。瀏覽器

就協議而言,HTTP是最簡單的協議之一。它的設計目的是經過互聯網發送內容,如HTML、視頻、圖像等,這都是經過HTTP請求和響應完成的。HTTP請求包含如下元素:安全

  • 請求方法:描述客戶端要執行操做的方法,靜態內容的方法一般是GET,此外還有其餘可用的方法,如POST、HEAD和DELETE。
  • 路徑:向服務器指示要請求的網頁。例如,此頁面的路徑是/python-https。
  • 版本:HTTP的版本,如1.0、1.1或2.0。最多見的多是1.1。
  • headers:描述服務器的其餘信息。
  • body:向服務器提供來自客戶端的信息。雖然這個字段不是必需的,可是某些方法要求有提交的內容,好比POST。

這些是瀏覽器用於與服務器通訊的內容,服務器藉助HTTP響應產生反應,並返回以下信息:bash

  • HTTP版本,該版本一般與請求的版本相同。
  • 狀態代碼:指示是否已成功完成了請求。狀態代碼有不少。
  • 狀態消息:提供有助於描述狀態代碼的可讀消息。
  • headers:容許服務器使用關於請求的附加元數據進行響應。
  • body:承載着內容。從技術上講,這是可選的,但它一般包含一個有用的資源。

這些是HTTP的組成。服務器

什麼是HTTPS?

如今你對HTTP有了詳細瞭解,那麼,什麼是HTTPS?好消息是,你已經知道了!HTTPS,即Hyper Text Transfer Protocol over SecureSocket Layer,超文本傳輸安全協議。從根本上說,HTTPS與HTTP是相同的協議,但它也意味着通訊是安全的。

HTTPS不會重寫它所構建的任何HTTP基礎,相反,HTTPS由經過加密鏈接發送的常規HTTP組成。一般,這種加密鏈接由TLS或SSL提供,它們是在信息經過網絡發送以前對其進行加密的協議。

注意:TLS和SSL是很是類似的協議,儘管SSL正在退出,TLS將取代它。這些協議中的差別不在本文的範圍內。只要知道TLS是SSL的更新、更好的版本就足夠了。

那麼,爲何要有HTTP和HTTPS兩種呢?爲何不把加密引入HTTP協議自己呢?答案是可移植性。保護通訊安全是一個重要而困難的問題,但HTTP只是許多須要安全性的協議之一。在網絡上,除了網頁訪問以外,還有其餘的許多應用:

  • E-mail
  • 即時通信
  • VoIP

每項應用都有專門的協議,若是每一個協議都必須建立本身的安全機制,那麼這個世界就會變得更加不安全,也會更加混亂。TLS是上述協議中經常使用的一種安全通訊方法。

在下文中,你將學習到的幾乎全部內容都不只僅適用於Python HTTPS應用,此外,還將學習安全通訊的基礎知識,以及它如何具體應用於HTTPS。

爲何HTTPS很重要?

通訊安全對於提供安全的在線環境相當重要。隨着包括銀行和醫療站點在內的愈來愈多的網絡應用,對於開發人員來講,建立Python HTTPS應用變得愈來愈重要。一樣,HTTPS只是TLS或SSL上的HTTP,TLS的設計是爲了保護隱私不被竊聽,它還能夠提供客戶端和服務器的身份驗證。

在本文中,你將經過執行如下操做深刻探討這些概念:

  • 建立Python HTTPS服務器
  • 與Python HTTPS服務器通訊
  • 捕獲這些通訊
  • 分析這些消息

咱們開始吧!

建立示例

假設你是一個叫作祕密松鼠的酷Python俱樂部的領導,松鼠,做爲機密,須要以加密信息的方式發佈給會議。做爲領導,你要選擇發佈的加密信息,每次會議都會更改這個信息。不過,有時候,你很難在會前和全部會員見面,告訴他們此信息!你決定設置一個祕密服務器,成員能夠在其中只能看到發給他們的加密信息。

你已經學習了一些關於真正Python的知識(若是尚未學習,推薦《Python大學實用教程》(電子工業出版社)),並安裝以下模塊:

  • 用於構建web應用程序的Flask
  • 做爲生產服務器的uWSGI
  • 向服務器發起請求的requests

要安裝全部這些,可使用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應用程序將顯示SECRET_MESSAGE的內容。這樣一來,就能夠在祕密服務器上部署應用程序並運行它:

$ uwsgi --http-socket 127.0.0.1:5683 --mount /=server:app
複製代碼

此命令旨在啓動的服務器上使用上面的Flask應用,所使用的端口有點奇怪(5683),由於你不但願別人能找到它,爲本身的「鬼鬼祟祟」感到慶幸!能夠經過訪問瀏覽器訪問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_URL127.0.0.1:5683。因此,你的計劃是給每一個俱樂部成員一個祕密的網址,告訴他們要保密和安全。

雖然這可能看起來不錯,但這樣作還不夠!事實上,即便你在這個網站上輸入用戶名和密碼,它仍然是不安全的。甚至你的團隊設法保證了URL的安全,你的祕密消息也還不安全。爲了說明爲何你須要瞭解一些有關監視網絡流量的信息,你須要使用一個名爲Wireshark的工具。

設置Wireshark

Wireshark是一個應用普遍的網絡和協議分析工具,這它能夠幫助你瞭解網絡鏈接上發生的事情。安裝和設置Wireshark對於本文是可選的,可是若是你想繼續學習,請安裝和使用它。下載頁提供了幾個安裝程序:

  • macOS 10.12及更高版本
  • 64位Windows安裝程序
  • 32位Windows安裝程序

若是你使用的是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發送的全部東西,任何人均可以清楚地看到。這意味着,即便某人沒有你的SECRET_URL,他仍然能夠看到你所作的一切,只要他能夠監視你和服務器之間的任何設備上的流量。

這對你來講應該比較可怕。畢竟,你不想別人出如今你的祕密松鼠會議上!下面證實這種狀況是真實發生的。首先,若是服務器還沒有運行,請啓動它:

$ uwsgi --http-socket 127.0.0.1:5683 --mount /=server:app
複製代碼

這將在端口5683上啓動Flask應用。接下來,你將在Wireshark中開始數據包捕獲。此數據包捕獲將幫助你查看進出服務器的全部流量。首先在Wireshark上選擇Loopback:lo接口:

你能夠看到Loopback:lo部分突出顯示,這指示Wireshark監視此端口的流量。你能夠作得更好,並指定要捕獲的端口和協議,能夠在捕獲篩選器中鍵入port 5683,在顯示篩選器中鍵入http

綠色框表示Wireshark對你鍵入的篩選器感到滿意。如今你能夠單擊左上角的按鈕開始捕獲:

單擊此按鈕將在Wireshark中生成一個新窗口:

這個新窗口至關簡單,但底部的消息顯示<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中看到一些條目。若是一切順利,那麼你將看到兩個相似於如下內容的條目:

這兩個記錄表示發生通訊的兩個部分。第一個是客戶機對服務器的請求。當你單擊第一個條目時,你將看到大量信息:

不少信息!在頂部,仍然有HTTP請求和響應。選擇其中一個條目後,你將看到中間和底部的行填充了信息。

中間一行提供了協議的詳細信息,Wireshark可以爲所選的請求標識這些信息。這個詳細信息容許你探索HTTP請求中實際發生的事情。Wireshark在中間一行從上到下描述了一些信息,下面是這些信息的快速摘要:

  • 物理層:描述用於發送請求的物理接口。
  • 以太網信息:向用戶顯示的第2層協議,其中包括源和目標MAC地址。
  • IPv4:顯示源和目標IP地址(127.0.0.1)。
  • TCP:包括所需的TCP握手,以便建立可靠的數據管道。
  • HTTP:顯示關於HTTP請求自己的信息。

當你展開超文本傳輸協議層時,能夠看到構成HTTP請求的全部信息:

此圖顯示腳本的HTTP請求:

  • Method: GET
  • Path: /
  • Version: 1.1
  • Headers: Host: 127.0.0.1:5683, Connection: keep-alive, and others
  • Body: No body

你看到的最後一行是十六進制的數據轉儲。在這個十六進制轉儲中,你可能會注意到:你實際上能夠看到HTTP請求的各個部分。那是由於你的HTTP請求是公開發送的。可是回覆呢?若是單擊HTTP響應,則會看到一個相似的視圖:

一樣,也有那三個部分。若是你仔細看這個十六進制轉儲文件,會看到明文的祕密消息!這對祕密松鼠來講是個大問題。這意味着,若是有興趣的話,任何有專門技術知識的人均可以很容易地看到這個數據流。那麼,你怎麼解決這個問題呢?答案是密碼學。

密碼學有什麼幫助?

在本節中,你將學習一種保護數據安全的方法,即建立本身的加密密鑰並在服務器和客戶機上使用它們。雖然這不是你的最後一步,但它將幫助你爲學會構建Python HTTPS應用程序奠基堅實的基礎。

瞭解密碼學基礎知識

密碼學是一種保護通訊免受竊聽或攻擊的方法。另外一種說法是,你獲取正常的信息(稱爲明文),而後把它轉換成加密的文本(稱爲密文)。

密碼學一開始可能很嚇人,但基本概念是很容易理解的。事實上,你之前可能已經練習過密碼學。若是你曾經和你的朋友有過一種祕密語言,並在課堂上用它來傳遞筆記,那麼你就已經練習過密碼學。(若是你還沒作到,別擔憂,你即將作到。)

無論什麼理由,如今你須要把字符串fluffy tail轉換成一些難以理解的東西。一種方法是將某些字符映射到不一樣的字符上,還有一種有效的方法是將字母向後移動一個位置,這種作法看起來是這樣的:

此圖顯示如何從原始字母表轉換爲新字母表並返回。因此,若是你的信息是ABC,那麼實際上發送的信息將會是ZAB。若是把這個應用到fluffy tail上,且長度不變,就獲得ekteex szhk,雖然並不完美,但任何人看到都會以爲它是胡言亂語。

祝賀你!你已經建立了在密碼學中稱爲密碼的東西,它描述瞭如何將明文轉換爲密文並返回。在這種狀況下,你的密碼是用英語描述的。這種特殊類型的密碼稱爲替換密碼。基本上,這與Enigma機器(en.wikipedia.org/wiki/Enigma…

如今,若是你想把信息傳給祕密松鼠,那麼你首先須要告訴它們要移動多少個字母,而後把編碼的信息發給它們。在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()的函數,它將獲取明文並將其轉換爲密文。想象一下,你有一本字典CIPHER,它把全部的字符都標出來了。相似地,你能夠建立一個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庫,能夠用pip安裝它:

$ pip install cryptography
複製代碼

安裝了cryptography以後,你如今可使用Fernet方法以數學上安全的方式加密和解密。

記得你密碼裏的密鑰是1。一樣,你須要建立一個密鑰,以便讓Fernet正常運行:

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

在這段代碼中,導入了Fernet並生成了一個密鑰。密鑰只是一個bytes對象,可是保持密鑰的機密性和安全性是很是重要的。就像上面的替換示例同樣,任何具備此密鑰的人均可以輕鬆地解密你的信息。

注意:在現實生活中,你會把這個密鑰保管得很安全。在這些例子中,查看密鑰是有幫助的。但這是一個糟糕的作法,特別是若是你在公共網站上發佈它!換言之,不要使用你在上面看到的確切的密鑰來得到你想要的安全性。

這個密鑰的運行方式與前面的密鑰很類似,用它能夠將明文轉換爲密文,而且可以解密返回明文。如今是有趣的部分了!你能夠加密以下信息:

>>> 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對象才能對其進行加密。加密後,能夠看到「密文」是一個長字節流。

多虧了Fernet,這個密文沒有密鑰就不能被操做或閱讀!這種加密要求服務器和客戶端都有權訪問密鑰。當雙方都須要相同的密鑰時,這稱爲對稱加密。在下一節中,你將看到如何使用這種對稱加密來保證數據的安全。

確保數據安全

如今,你已經瞭解了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對象組合在一塊兒。如今使用os.environb將密鑰做爲bytes對象從環境變量中讀取。掃清了服務器方面的障礙以後,你如今能夠專一於客戶端。將如下內容粘貼到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上啓動服務器。這一次,傳入的SECRET_KEY 必須至少是長度爲32的base64編碼字符串。從新啓動服務器後,你如今能夠查詢它:

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

哇!你已經實現加密和解密了。若是嘗試使用無效的SECRET_KEY運行此操做,則會出現錯誤:

$ 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中看到這些信息。因爲加密信息只在響應中傳輸,你能夠單擊該信息查看數據:

在圖片的中間一行,能夠看到實際傳輸的數據:

gAAAAABdlXSesekh9LYGDpZE4jkxm4Ai6rZQg2iHaxyDXkPWz1O74AB37V_a4vabF13fEr4kwmCe98Wlr8Zo1XNm-WjAVtSgFQ==
複製代碼

棒極了!這意味着數據是加密的,竊聽者不知道信息內容其實是什麼。不只如此,這也意味着他們可能會花費大量的時間試圖暴力破解這些數據,並且他們幾乎永遠不會成功。

你的數據是安全的!可是等一下——之前使用Python HTTPS應用時,不須要知道任何關於鑰匙的事情。這是由於HTTPS不專門使用對稱加密。事實證實,分享祕密是個難題。

要證實這個概念,請在瀏覽器中輸入http://127.0.0.1:5683,你將看到加密的響應文本。這是由於你的瀏覽器對你的密鑰一無所知。那麼Python HTTPS應用程序究竟是如何工做的呢?這就是非對稱加密發揮做用的地方。

如何共享密鑰?

在上一節中,你瞭解瞭如何使用對稱加密來保證數據在Internet上的安全。儘管對稱加密是安全的,但它並非Python HTTPS應用用來保證數據安全的惟一加密技術。對稱加密引入了一些不易解決的基本問題。

注意:記住,對稱加密要求在客戶端和服務器之間有一個共享密鑰。不幸的是,安全性的工做強度取決於最弱的連接,而在對稱加密中,弱連接尤爲具備災難性。一旦一我的泄露了密鑰,那麼每一個密鑰都會泄露。能夠確定的是,任何安全系統在某個時候都會受到損害。

那麼,你怎麼改變密鑰?若是你只有一個服務器和一個客戶端,這多是一個快速的任務。然而,隨着客戶端和服務器的增多,爲了有效地更改密鑰和保護信息,須要進行愈來愈多的協調。

並且,你每次都要選擇一個新的加密方式。在上面的示例中,你看到一個隨機生成的密鑰,幾乎不可能試着讓人們記住那個密鑰。隨着客戶端和服務器數量的增加,可能會使用更容易記住和猜想的密鑰。

若是處理好了更改密鑰的問題,那麼還有一個問題要解決,如何分享你的初始密鑰?在祕密松鼠示例中,你經過對每一個成員進行物理訪問來解決了這個問題,能夠親自把密鑰告訴每一個成員,讓他們保守祕密,但要記住,有人會是最薄弱的環節。

如今,假設你從另外一個物理位置向祕密松鼠會添加一個成員,如何與這個會員分享這個祕密?每次更改密鑰時,你都讓他們搭飛機去找你嗎?若是你能把密鑰放在你的服務器上並自動共享,那就太好了。不幸的是,這會挫敗加密的所有目的,由於任何人均可以獲得密鑰!

固然,你能夠給每一個人一個初始的主密鑰來獲取祕密信息,但如今你遇到的問題是之前的兩倍。若是你爲之頭痛,別擔憂!你不是惟一一個。

你須要的是兩個從未交流過的人有一個共同的祕密。聽起來不可能,對吧?幸運的是,有三我的:拉爾夫·梅克爾、惠特菲爾德·迪菲和馬丁·赫爾曼,他們支持你,他們證實了公鑰加密(也就是所謂的非對稱加密)是可能的。

注:雖然惠特菲爾德·迪菲和馬丁·赫爾曼被普遍認爲是第一個發現這一計劃的人,但據1997年的披露,在GCHQ工做的三人:詹姆斯·H·埃利斯、克利福德·考克斯和馬爾科姆·J·威廉森早在七年前就展現了這種功能!

非對稱加密容許兩個從未有過通訊的用戶共享一個共同的祕密。理解基本原理的最簡單方法之一是使用顏色類比。假設你有如下場景:

在這個圖表中,你試圖與一個你從未見過的「祕密松鼠」成員交流,但間諜能夠看到你發送的全部信息。你知道對稱加密而且想使用它,可是首先須要共享一個密鑰。幸運的是,大家倆都有私鑰。不幸的是,你不能發送你的私鑰,由於間諜會看到它。那你怎麼辦?

你須要作的第一件事就是贊成使用你的夥伴的顏色,好比黃色:

注意這裏間諜能夠看到共享的顏色,你和祕密松鼠也能夠。共享顏色其實是公開的。如今,你和祕密松鼠將你的私鑰與共享顏色結合起來:

你的顏色組合成綠色,而祕密松鼠的顏色組合成橙色。大家兩個都使用了共享顏色,如今大家須要彼此共享組合的顏色:

你如今有了你的私鑰和祕密松鼠的顏色組合。一樣地,祕密松鼠有他們的私鑰和你的組合顏色。你和祕密松鼠很快就把大家的顏色組合起來了。

然而,間諜只有這兩種顏色。要想弄清楚你的原色是很是困難的,即便給定了最初的共享顏色。間諜得去商店買不少不一樣的藍顏料來試試。即便這樣,也很難知道他們在組合後是否看到了具備正確深淺度的綠色!簡而言之,你的私鑰仍然是私鑰。

可是你和那個「祕密松鼠」成員呢?大家仍然沒有一個共同的祕密!這是你的私鑰從新派上用場的地方。若是你把你的私鑰和你從祕密松鼠那裏獲得的顏色組合在一塊兒,那麼你倆最終會獲得相同的顏色:

如今,你和這個「祕密松鼠」成員有着相同的祕密顏色。你如今已經成功地和一個徹底陌生的人分享了一個安全的祕密。這對於公鑰密碼的工做方式來講是驚人的精確。這個事件序列還有另外一個通用名稱:Diffie-Hellman密鑰交換。密鑰交換由如下部分組成:

  • 私鑰是示例中的私用顏色。
  • 公鑰是你共享的組合顏色。

私鑰是你始終保持私有的東西,而公鑰能夠與任何人共享。這些概念直接映射到Python HTTPS應用程序的現實世界。既然服務器和客戶端有了一個共享的祕密,你可使用你的「老夥計」對稱加密來對全部信息進行加密!

注意:公鑰密碼術也依賴於一些數學知識來進行顏色混合。Diffie-Hellman密鑰交換的維基百科詞條有很好的解釋,可是深刻的解釋不在本文的範圍以內。

當你經過安全網站(如本網站)進行通訊時,你的瀏覽器和服務器使用這些相同的原則設置安全通訊:

  • 瀏覽器從服務器請求信息。
  • 瀏覽器和服務器交換公鑰。
  • 瀏覽器和服務器生成共享私鑰。
  • 瀏覽器和服務器使用此共享密鑰經過對稱加密對消息進行加密和解密。

幸運的是,你不須要實現這些細節。有許多內置庫和第三方庫能夠幫助你保持客戶端和服務器通訊的安全。

真實的HTTPS

考慮到全部這些關於加密的信息,讓咱們把範圍縮小一點,討論一下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
複製代碼

tu

這裏有一個新的函數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.
複製代碼

若是嘗試使用瀏覽器打開你的網站,則會收到相似信息:

若是要避免此信息,你必須返回有關你的證書頒發機構!只需將請求指向你先前生成的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上安全通訊的一些核心基礎,如今已經瞭解了這些構建模塊,你將成爲一個更好、更安全的開發人員。

若是你對這些信息感興趣,那你就走運了!你僅僅走馬觀花式地觸及了每一層中全部的細微差異。安全世界不斷髮展,新的技術和漏洞也不斷被發現。

原文連接:realpython.com/python-http…

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

相關文章
相關標籤/搜索