返回文章列表

TLS 憑證驗證與 Python HTTPX 應用

本文探討 TLS 憑證驗證機制,並以 Python HTTPX 函式庫示範如何正確組態憑證,包含伺服器憑證驗證、使用者端憑證設定及自定義 SSL 上下文。同時,文章也介紹瞭如何使用 HTTPX 進行自定義身份驗證和非同步 HTTP 請求,並解析了密碼學函式庫 cryptography 和 PyNaCl 的應用,涵蓋

網路安全 Web 開發

TLS 憑證驗證是現代網路安全根本,確保資料傳輸的機密性與完整性。本文除了介紹 TLS 憑證鏈、SNI 等核心概念外,更著重於 Python HTTPX 函式庫的實務應用,包含如何正確設定憑證路徑、自定義 SSL 內容以及使用者端憑證組態,避免不安全的 verify=False 做法。同時,文章也涵蓋了 HTTPX 的進階應用,例如自定義身份驗證機制與非同步 HTTP 請求處理,提升開發效率。此外,文章也探討了密碼學在安全架構中的重要性,並以 cryptography 和 PyNaCl 兩個 Python 函式庫為例,示範 Fernet 對稱加密、金鑰輪換、非對稱加密、數位簽章等實務技巧,提供開發者更全面的安全程式設計參考。

網路安全與憑證驗證

HTTP 安全模型依賴於憑證驗證機構(Certification Authorities, CAs)。CAs 對公鑰進行簽名,以確認其屬於特定的網域。為了實作金鑰輪換與復原,CAs 使用一箇中間簽名金鑰對公鑰進行簽名,而非直接使用根金鑰。

安全通訊的基礎:TLS與憑證驗證

在現代網路通訊中,安全是至關重要的議題。為了確保資料傳輸的安全性和完整性,TLS(Transport Layer Security)憑證驗證扮演了關鍵角色。本文將探討TLS的工作原理、憑證驗證的流程,以及如何在Python的HTTPX函式庫中組態和管理憑證。

TLS與憑證鏈

TLS是一種加密通訊協定,用於保護網路資料傳輸的安全。它的工作原理依賴於憑證鏈(Certificate Chain),其中每個憑證簽署下一個憑證,直到最終的伺服器憑證。這種鏈式結構確保了憑證的可信度。

伺服器名稱指示(SNI)與憑證請求

由於多個網域可能共用同一個IP地址,TLS協定使用伺服器名稱指示(SNI)來請求正確的憑證。客戶端在連線請求中以明文方式傳送希望連線的伺服器名稱,伺服器則回應相應的憑證,並使用密碼學證明其擁有與簽署公鑰相對應的私鑰。

使用者端憑證與雙向認證

除了伺服器端憑證驗證外,TLS還支援使用者端憑證,用於證明客戶端的身份。這種雙向認證機制在某些應用場景中非常有用,例如微服務架構中的服務間通訊。

在HTTPX中組態憑證驗證

HTTPX函式庫預設支援TLS憑證驗證。要確保連線的安全性,建議安裝certifi套件,它提供了Mozilla相容的根憑證。如果憑證驗證失敗,HTTPX將丟擲CERTIFICATE VALIDATE FAILED錯誤。

正確組態憑證驗證

雖然在某些情況下可能會建議透過設定verify=False來繞過憑證驗證,但這種做法破壞了TLS的核心假設:連線的加密性和防篡改性。因此,建議透過提供正確的憑證路徑給verify引數來進行驗證,例如:

client = httpx.Client(verify='/full/path/cert.pem')

自定義SSL上下文

在某些情況下,可能需要自定義SSL上下文以允許或禁止特定的SSL版本或加密套件。HTTPX允許透過ssl.SSLContext物件進行自定義:

import httpx
import ssl

ssl_context = ssl.create_default_context()
ssl_context.options |= ssl.OP_NO_TLSv1_3
client = httpx.Client(verify=ssl_context)

使用者端憑證組態

對於需要使用者端憑證的場景,可以透過cert引數進行組態。如果憑證和私鑰在同一個檔案中,可以直接指設定檔案路徑:

client = httpx.Client(cert="/path/to/pem/file.pem")

如果憑證和私鑰分開,則需要以元組形式提供:

client = httpx.Client(cert=("/path/to/client.cert", "/path/to/client.key"))

HTTPX 函式庫的高階應用:自定義身份驗證與非同步客戶端

在現代軟體開發中,HTTP 請求的處理是一個常見且重要的任務。HTTPX 是一個強大的 Python 函式庫,它不僅支援同步和非同步 HTTP 請求,還提供了豐富的功能來自定義請求和處理回應。本篇文章將探討 HTTPX 的兩個高階應用:自定義身份驗證和非同步客戶端。

自定義身份驗證

在與 Web 服務互動時,身份驗證是一個關鍵的安全措施。HTTPX 支援多種身份驗證方法,包括基本身份驗證(Basic Auth)和自定義身份驗證方案。

基本身份驗證

基本身份驗證是一種簡單的身份驗證方法,透過在 HTTP 請求頭中新增 Authorization 欄位來實作。HTTPX 支援透過 httpx.BasicAuth 例項來進行基本身份驗證。

自定義身份驗證

對於更複雜的身份驗證需求,HTTPX 允許開發者透過子類別化 httpx.Auth 來實作自定義身份驗證邏輯。這種方法可以處理更複雜的身份驗證流程,例如需要多個步驟或特定協定的身份驗證。

import httpx
from urllib.parse import parse_qs, urlencode
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

def canonical_query_string(query):
    if not query:
        return ""
    parsed = parse_qs(query, keep_blank_values=True)
    return "?" + urlencode(parsed, doseq=True)

def to_canonical_url(raw_url):
    url = httpx.URL(raw_url)
    path = url.path or "/"
    query = canonical_query_string(url.query)
    return f"{url.scheme}://{url.netloc}{path}{query}"

class AWSv4Auth(httpx.Auth):
    def __init__(self, aws_session, region, service):
        self.aws_session = aws_session
        self.region = region
        self.service = service

    def auth_flow(self, request):
        aws_request = AWSRequest(
            method=request.method.upper(),
            url=to_canonical_url(str(request.url)),
            data=request.content,
        )
        credentials = self.aws_session.get_credentials()
        SigV4Auth(credentials, self.service, self.region).add_auth(aws_request)
        request.headers.update(aws_request.headers.items())
        yield request

# 使用範例
client = httpx.Client(
    auth=AWSv4Auth(
        aws_session=botocore.session.get_session(),
        region='us-east-1',
        service='es',
    ),
)

內容解密:

  1. canonical_query_string 函式:用於將 URL 中的查詢字串進行規範化處理,以符合 AWS 簽名協定的要求。
  2. to_canonical_url 函式:將原始 URL 轉換為規範化的 URL,包括協定、網域、路徑和查詢字串。
  3. AWSv4Auth 類別:繼承自 httpx.Auth,實作了 AWS V4 簽名協定的身份驗證邏輯。
  4. auth_flow 方法:是 httpx.Auth 的核心方法,用於定義身份驗證的流程。在此範例中,它負責對請求進行簽名並更新請求頭。

非同步客戶端

對於需要進行大量 HTTP 請求的應用程式,使用非同步請求可以顯著提高效能。HTTPX 提供了非同步客戶端(AsyncClient)來支援非同步 HTTP 請求。

import httpx
import asyncio
import datetime

async def async_calls():
    before = datetime.datetime.now()
    async with httpx.AsyncClient() as async_client:
        fut1 = async_client.get("https://httpbin.org/delay/3?param=async-first")
        fut2 = async_client.get("https://httpbin.org/delay/3?param=async-second")
        responses = await asyncio.gather(fut1, fut2)
    delta = datetime.datetime.now() - before
    r1, r2 = responses
    results1 = r1.json()
    results2 = r2.json()
    print(delta // datetime.timedelta(seconds=1))
    print(results1["args"]["param"], results2["args"]["param"])

asyncio.run(async_calls())

內容解密:

  1. async_calls 函式:定義了一個非同步函式,用於傳送兩個非同步 HTTP GET 請求。
  2. httpx.AsyncClient:用於建立一個非同步 HTTP 客戶端。
  3. asyncio.gather:用於平行執行多個非同步任務,並等待它們全部完成。
  4. await:用於等待非同步操作的完成。

密碼學在安全架構中的重要性

密碼學是許多安全架構中的必要組成部分。然而,僅僅在程式碼中加入密碼學並不能使其更加安全;必須謹慎處理諸如秘密金鑰生成、金鑰儲存和明文管理等主題。正確設計安全軟體是複雜的,尤其是涉及到密碼學時。

本章節僅介紹 Python 中密碼學的基本工具及其使用方法。

Fernet 加密技術

cryptography 模組支援 Fernet 加密標準。Fernet 是一種對稱加密技術,不支援部分或串流解密。它需要讀取整個密鑰並傳回整個明文。這使得它適合用於名稱、文字檔案甚至圖片。然而,影片和磁碟映像檔並不適合使用 Fernet。

Fernet 的密碼學引數是由領域專家選擇的,他們研究了可用的加密方法和已知的最佳攻擊方法。使用 Fernet 的一個優點是避免了成為專家的必要。然而,為了完整性,需要注意的是,Fernet 標準使用 AES-128 在 CBC 填充中使用 PKCS7,並且 HMAC 使用 SHA256 進行身份驗證。

Fernet 標準也得到了 Go、Ruby 和 Erlang 的支援,因此有時適合與其他語言進行資料交換。它的設計使得使用它時很難出錯。

Fernet 金鑰生成與使用

>>> k = fernet.Fernet.generate_key()
>>> type(k)
<class 'bytes'>

金鑰是一小串位元組。安全管理金鑰非常重要;密碼學的安全性取決於其金鑰。如果金鑰儲存在檔案中,則該檔案應該具有最小的許可權,並且最好儲存在加密的檔案系統上。

>>> frn = fernet.Fernet(k)

Fernet 類別使用金鑰進行初始化,並確保金鑰的有效性。

Fernet 加密與解密

>>> encrypted = frn.encrypt(b"x marks the spot")
>>> encrypted[:10]
b'gAAAAABb1'

加密很簡單。它接受一串位元組並傳回加密的字串。需要注意的是,加密後的字串比原始字串長。它還使用秘密金鑰進行簽名,這意味著對加密字串的篡改是可以檢測到的,並且 Fernet API 透過拒絕解密該字串來處理這種情況。解密後獲得的值是可信的。它確實是由擁有秘密金鑰的人加密的。

>>> frn.decrypt(encrypted)
b'x marks the spot'

解密的方式與加密相同。Fernet 包含版本標記,因此如果發現漏洞,可以將標準遷移到不同的加密和雜湊系統。

Fernet 的額外功能

Fernet 加密總是將當前日期新增到簽名和加密的資訊中。因此,可以限制訊息的年齡,然後再解密。

>>> frn.decrypt(encrypted, ttl=5)

如果加密資訊(有時稱為令牌)的年齡超過五秒,則解密會失敗。這對於防止重放攻擊很有用。重放攻擊是指捕捉並重放先前加密的令牌,而不是新的有效令牌。

MultiFernet 金鑰輪換

MultiFernet 類別支援無縫的金鑰輪換。它接受一個秘密金鑰列表,並使用第一個秘密金鑰進行加密,但嘗試使用任何一個秘密金鑰進行解密。

PyNaCl 加密技術

PyNaCl 是對 libsodium C 函式庫的封裝,libsodium 是 Daniel J. Bernstein 的 libnacl 的分支。PyNaCl 支援對稱和非對稱加密。然而,由於 cryptography 模組支援 Fernet 對稱加密,PyNaCl 的主要用途是非對稱加密。

非對稱加密的基本概念

非對稱加密的思想是存在私鑰和公鑰。可以從私鑰輕鬆計算出公鑰,但反之則不行;這就是所謂的非對稱性。公鑰可以用於加密資料,而私鑰則用於解密。

內容解密:

  1. Fernet 金鑰生成:使用 Fernet.generate_key() 方法生成金鑰,金鑰是一小串位元組。
  2. Fernet 初始化:使用生成的金鑰初始化 Fernet 物件,確保金鑰的有效性。
  3. Fernet 加密:使用 encrypt 方法對資料進行加密,傳回加密後的字串。
  4. Fernet 解密:使用 decrypt 方法對加密後的字串進行解密,傳回原始資料。
  5. ttl 引數:在解密時可以使用 ttl 引數限制訊息的年齡,防止重放攻擊。
  6. MultiFernet:支援無縫的金鑰輪換,使用多個秘密金鑰進行加密和解密。
  7. PyNaCl:用於非對稱加密,支援私鑰和公鑰的生成和使用。

公鑰密碼學與PyNaCl實作

公鑰密碼學基礎原理

在現代密碼學實踐中,公鑰密碼學扮演著至關重要的角色。它根據一對金鑰:公鑰和私鑰。其中,公鑰可以公開發布,而私鑰則必須嚴格保密。這種非對稱加密機制允許兩種基本操作:使用公鑰加密資料,只有對應的私鑰才能解密;或者使用私鑰簽名資料,然後用公鑰驗證簽名。

加密與簽名的雙重保障

現代密碼學不僅重視資料的保密性,也同等重視資料的完整性和真實性。因此,像libsodium及其Python封裝函式庫PyNaCl這樣的密碼學函式庫,都強制要求在加密資料時進行簽名,並在解密時驗證簽名。這種設計有效地防止了中間人攻擊和資料篡改。

使用PyNaCl進行加密與解密

生成私鑰與公鑰

首先,我們需要生成一對金鑰。私鑰可以透過PrivateKey.generate()方法生成,而公鑰則可以從私鑰派生出來。

from nacl.public import PrivateKey

# 生成私鑰
private_key = PrivateKey.generate()

# 將私鑰編碼為位元組串,以便儲存
encoded_private_key = private_key.encode()

# 從位元組串重建私鑰
reconstructed_private_key = PrivateKey(encoded_private_key)

# 驗證重建的私鑰與原始私鑰是否相同
print(reconstructed_private_key == private_key)  # True

# 從私鑰生成公鑰
public_key = private_key.public_key

# 將公鑰編碼為位元組串
encoded_public_key = public_key.encode()

# 從位元組串重建公鑰
reconstructed_public_key = PublicKey(encoded_public_key)

# 驗證重建的公鑰與原始公鑰是否相同
print(reconstructed_public_key == public_key)  # True

內容解密:

  1. 生成私鑰:使用PrivateKey.generate()生成一個新的私鑰。
  2. 儲存私鑰:將私鑰編碼為位元組串,以便於儲存或傳輸。
  3. 重建私鑰:從儲存的位元組串中重建私鑰。
  4. 生成公鑰:從私鑰中派生出對應的公鑰。
  5. 儲存公鑰:將公鑰編碼為位元組串,以便於分發或儲存。

使用Box類別進行加密與解密

PyNaCl的Box類別結合了私鑰和公鑰,用於加密和簽名資料。

from nacl.public import PrivateKey, PublicKey, Box

# 生成傳送者和接收者的私鑰
sender_private_key = PrivateKey.generate()
receiver_private_key = PrivateKey.generate()

# 取得對應的公鑰
sender_public_key = sender_private_key.public_key
receiver_public_key = receiver_private_key.public_key

# 建立用於加密的Box物件
encryption_box = Box(sender_private_key, receiver_public_key)

# 加密訊息
encrypted_message = encryption_box.encrypt(b"x marks the spot")

# 建立用於解密的Box物件
decryption_box = Box(receiver_private_key, sender_public_key)

# 解密訊息
decrypted_message = decryption_box.decrypt(encrypted_message)

print(decrypted_message)  # b'x marks the spot'

內容解密:

  1. 建立加密Box:使用傳送者的私鑰和接收者的公鑰建立Box物件,用於加密和簽名。
  2. 加密訊息:使用encryption_box.encrypt()方法加密訊息。
  3. 建立解密Box:使用接收者的私鑰和傳送者的公鑰建立Box物件,用於解密和驗證簽名。
  4. 解密訊息:使用decryption_box.decrypt()方法解密訊息並驗證簽名。

使用PyNaCl進行簽名與驗證

除了加密/解密外,PyNaCl還提供了簽名功能,用於驗證資料的真實性和完整性。

from nacl.signing import SigningKey

# 生成簽名私鑰
signing_key = SigningKey.generate()

# 將簽名私鑰編碼為位元組串
encoded_signing_key = signing_key.encode()

# 從位元組串重建簽名私鑰
reconstructed_signing_key = SigningKey(encoded_signing_key)

# 取得驗證公鑰
verify_key = signing_key.verify_key

# 將驗證公鑰編碼為位元組串
encoded_verify_key = verify_key.encode()

# 從位元組串重建驗證公鑰
reconstructed_verify_key = VerifyKey(encoded_verify_key)

# 簽名訊息
message = b"The number you shall count is three"
signed_message = signing_key.sign(message)

# 驗證簽名
verified_message = reconstructed_verify_key.verify(signed_message)

print(verified_message)  # b'The number you shall count is three'

內容解密:

  1. 生成簽名私鑰:使用SigningKey.generate()生成簽名私鑰。
  2. 儲存簽名私鑰:將簽名私鑰編碼為位元組串,以便於安全儲存。
  3. 取得驗證公鑰:從簽名私鑰中取得對應的驗證公鑰。
  4. 簽名訊息:使用signing_key.sign()方法對訊息進行簽名。
  5. 驗證簽名:使用verify_key.verify()方法驗證簽名的真實性。