憑證和 PKI:在網際網路上建立信任
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 9 minutes reading.
1. 為什麼要關心這個問題?
你造訪 https://bank.example.com。你的瀏覽器顯示了一個鎖標誌。它怎麼知道這真的是你的銀行而不是攻擊者?
答案是憑證——將公鑰綁定到身分的數位文件。沒有它們,HTTPS 將毫無意義。
理解 PKI 能幫助你:
- 偵錯 TLS 憑證錯誤
- 為你的應用程式設定 HTTPS
- 理解為什麼「直接點擊忽略警告」是危險的
- 知道何時以及如何使用用戶端憑證
2. 定義
數位憑證(具體來說是 X.509)綁定了:
- 公鑰
- 身分(網域名稱、組織等)
- 來自可信機構的簽章
公鑰基礎建設 (PKI) 是一個系統,包括:
- 核發憑證的憑證授權機構 (CA)
- 核發和撤銷憑證的政策
- 包含可信根憑證的信任庫
憑證 = 「我,可信 CA,證明
公鑰 XYZ 屬於 bank.example.com」
+ CA 的簽章3. 憑證內部有什麼
X.509 憑證結構
Certificate:
Version: 3
Serial Number: 04:00:00:00:00:01:2F:4E:E1:5B:3D
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=DigiCert Global Root CA
Validity:
Not Before: Mar 08 12:00:00 2023 GMT
Not After: Mar 08 12:00:00 2024 GMT
Subject: CN=www.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
EC Public Key:
pub: 04:AB:CD:...
ASN1 OID: prime256v1
X509v3 Extensions:
Subject Alternative Name:
DNS:www.example.com
DNS:example.com
Basic Constraints:
CA:FALSE
Key Usage:
Digital Signature, Key Encipherment
Extended Key Usage:
TLS Web Server Authentication關鍵欄位解釋
Subject:憑證是給誰的
CN=www.example.com(通用名稱)
O=Example Inc(組織)
Issuer:誰簽署了憑證
為此身分擔保的 CA
Validity:憑證何時有效
Not Before / Not After 日期
Subject Alternative Name (SAN):
涵蓋的其他網域名稱
現代瀏覽器要求這個
Key Usage:
金鑰可以用於什麼
數位簽章、金鑰加密等
Extended Key Usage:
更具體的用途
TLS Web 伺服器認證
程式碼簽章等4. 信任鏈
信任如何運作
根 CA(自簽章,在瀏覽器信任庫中)
│
│ 簽署
▼
中繼 CA 憑證
│
│ 簽署
▼
終端實體憑證(你的網站)
瀏覽器驗證:
1. 終端憑證由中繼 CA 簽署
2. 中繼 CA 由根 CA 簽署
3. 根 CA 在信任庫中
4. 所有憑證有效(未過期、未撤銷)為什麼需要中繼 CA?
安全性:根金鑰極其珍貴
- 儲存在離線 HSM 中
- 很少使用(只用於簽署中繼 CA)
- 如果洩露,整個 PKI 崩潰
靈活性:
- 如果中繼 CA 洩露可以撤銷
- 不同中繼 CA 用於不同目的
- 更短的有效期
縱深防禦:
- 多一層需要攻破
- 可以輪換中繼 CA 而不改變根 CA5. 憑證授權機構 (CA)
CA 做什麼
1. 驗證憑證申請者的身分
- 網域驗證 (DV):證明你控制網域
- 組織驗證 (OV):驗證組織存在
- 延伸驗證 (EV):廣泛的法律驗證
2. 核發憑證
- 用 CA 的私鑰簽章
- 包含適當的限制
3. 維護撤銷資訊
- CRL(憑證撤銷清單)
- OCSP(線上憑證狀態協定)
4. 保護他們的金鑰
- 根金鑰在 HSM 中
- 嚴格的存取控制
- 定期稽核主要 CA
商業:
- DigiCert
- Sectigo(前身 Comodo)
- GlobalSign
- Entrust
免費:
- Let's Encrypt(自動化 DV 憑證)
- ZeroSSL
私有:
- 你組織的內部 CA
- 用於內部服務、用戶端憑證6. 取得憑證
使用 Let’s Encrypt(免費、自動化)
# 安裝 certbot
sudo apt install certbot
# 取得憑證(獨立模式)
sudo certbot certonly --standalone -d example.com -d www.example.com
# 憑證檔案:
# /etc/letsencrypt/live/example.com/fullchain.pem (憑證 + 中繼憑證)
# /etc/letsencrypt/live/example.com/privkey.pem (私鑰)
# 自動續期(每天執行兩次)
sudo certbot renew使用 ACME 協定(程式化方式)
# 使用 acme 函式庫的概念範例
from acme import client, messages
from cryptography.hazmat.primitives.asymmetric import ec
# 產生帳戶金鑰
account_key = ec.generate_private_key(ec.SECP256R1())
# 建立 ACME 用戶端
acme_client = client.ClientV2(
directory='https://acme-v02.api.letsencrypt.org/directory',
key=account_key
)
# 請求網域憑證
order = acme_client.new_order(['example.com'])
# 完成驗證(證明網域所有權)
for auth in order.authorizations:
challenge = get_http_challenge(auth)
# 部署驗證回應
# ...
acme_client.answer_challenge(challenge)
# 完成並取得憑證
certificate = acme_client.finalize_order(order, csr)7. 憑證驗證
瀏覽器檢查什麼
1. 簽章鏈有效
└─ 每個憑證由其核發者簽章
2. 根 CA 可信
└─ 在瀏覽器/作業系統信任庫中
3. 憑證未過期
└─ 目前時間在有效期內
4. 憑證未被撤銷
└─ 檢查 CRL 或 OCSP
5. 網域匹配
└─ Subject 或 SAN 包含該網域
6. 金鑰用途適當
└─ TLS Web 伺服器認證
7. 憑證政策滿足
└─ 各種 X.509 限制驗證程式碼範例
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import ExtensionOID
import ssl
import socket
import datetime
def validate_server_certificate(hostname, port=443):
"""驗證伺服器的憑證"""
# 取得憑證
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
der_cert = ssock.getpeercert(binary_form=True)
cert = x509.load_der_x509_certificate(der_cert, default_backend())
# 檢查有效期
now = datetime.datetime.utcnow()
if now < cert.not_valid_before or now > cert.not_valid_after:
return False, "憑證已過期或尚未生效"
# 檢查主體替代名稱
try:
san = cert.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
names = san.value.get_values_for_type(x509.DNSName)
if hostname not in names:
return False, f"主機名稱 {hostname} 不在 SAN 中"
except x509.ExtensionNotFound:
return False, "沒有 SAN 延伸"
return True, "憑證有效"
# 檢查
valid, message = validate_server_certificate("www.google.com")
print(f"有效: {valid}, 訊息: {message}")8. 憑證撤銷
為什麼撤銷很重要
需要撤銷的情境:
- 私鑰洩露
- 憑證核發錯誤
- 網域所有權變更
- 組織不再可信
沒有撤銷檢查:
- 持有被盜金鑰的攻擊者可以冒充網站
- 直到憑證自然過期(最長 398 天!)CRL vs OCSP
CRL(憑證撤銷清單):
┌────────────────────────────────┐
│ 已撤銷序號的大清單 │
│ 定期下載 │
│ 可能有幾兆位元組 │
│ 更新之間有延遲 │
└────────────────────────────────┘
OCSP(線上憑證狀態協定):
┌────────────────────────────────┐
│ 查詢:「這個憑證被撤銷了嗎?」 │
│ 回應:「是/否」 │
│ 即時 │
│ 隱私問題(CA 看到查詢) │
└────────────────────────────────┘
OCSP Stapling:
┌────────────────────────────────┐
│ 伺服器取得 OCSP 回應 │
│ 附加到 TLS 握手 │
│ 用戶端取得新鮮證明 │
│ 沒有隱私洩露給 CA │
└────────────────────────────────┘9. 常見憑證問題
憑證錯誤和解決方案
錯誤:NET::ERR_CERT_DATE_INVALID
原因:憑證已過期或尚未生效
修復:續期憑證,檢查伺服器時鐘
錯誤:NET::ERR_CERT_AUTHORITY_INVALID
原因:CA 不可信
修復:使用知名 CA,或安裝根憑證
錯誤:NET::ERR_CERT_COMMON_NAME_INVALID
原因:網域不在憑證中
修復:取得包含正確 SAN 的憑證
錯誤:SSL_ERROR_BAD_CERT_DOMAIN
原因:主機名稱不匹配
修復:使用正確的主機名稱存取
錯誤:憑證鏈不完整
原因:缺少中繼憑證
修復:設定伺服器使用完整鏈偵錯憑證
# 檢視憑證詳情
openssl s_client -connect example.com:443 -showcerts
# 檢查憑證日期
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# 檢查憑證鏈
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -issuer -subject
# 驗證憑證
openssl verify -CAfile ca-bundle.crt certificate.pem10. 用戶端憑證
用途
伺服器憑證:伺服器向用戶端證明身分
用戶端憑證:用戶端向伺服器證明身分
用例:
- 服務間認證的雙向 TLS (mTLS)
- 無需密碼的使用者認證
- 物聯網裝置認證
- VPN 認證設定 mTLS
import ssl
import socket
# 伺服器端
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain('server.crt', 'server.key')
server_context.load_verify_locations('client-ca.crt')
server_context.verify_mode = ssl.CERT_REQUIRED # 要求用戶端憑證
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(('0.0.0.0', 8443))
sock.listen()
with server_context.wrap_socket(sock, server_side=True) as ssock:
conn, addr = ssock.accept()
# conn.getpeercert() 回傳用戶端憑證
# 用戶端
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.load_cert_chain('client.crt', 'client.key')
client_context.load_verify_locations('server-ca.crt')
with socket.create_connection(('server.example.com', 8443)) as sock:
with client_context.wrap_socket(sock, server_hostname='server.example.com') as ssock:
# 使用雙向認證連線
pass11. 自簽憑證
何時使用
適合:
- 開發和測試
- 內部服務(使用私有 CA)
- 學習和實驗
不適合:
- 公開網站
- 被外部用戶端使用的正式環境 API
- 任何信任重要的地方建立自簽憑證
# 產生私鑰
openssl genrsa -out server.key 2048
# 產生自簽憑證
openssl req -new -x509 -key server.key -out server.crt -days 365 \
-subj "/CN=localhost"
# 帶 SAN 延伸(現代瀏覽器要求)
openssl req -new -x509 -key server.key -out server.crt -days 365 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"12. 憑證透明度
它解決什麼問題
如果 CA 為你的網域核發了憑證會怎樣?
- 你可能永遠不知道
- 攻擊者可以攔截流量
- CA 洩露是災難性的
憑證透明度:
- 所有憑證記錄到公開日誌
- 你可以監控你的網域的日誌
- 錯誤核發的憑證可被偵測如何運作
1. CA 核發憑證
2. CA 提交到 CT 日誌
3. 日誌回傳簽署憑證時間戳記 (SCT)
4. 憑證包含 SCT
5. 瀏覽器驗證 SCT 存在
6. 網域擁有者監控日誌
工具:
- crt.sh(搜尋 CT 日誌)
- Google 憑證透明度
- Cert Spotter(監控)13. 本章小結
三點要記住:
憑證將金鑰綁定到身分。 憑證是 CA 的簽章聲明,表明公鑰屬於特定網域或實體。
信任來自根 CA。 你的瀏覽器信任約 100 個根 CA。那些 CA 簽署中繼 CA,中繼 CA 簽署終端憑證。如果任何環節斷裂,信任就失敗了。
憑證管理是營運工作。 續期、撤銷和鏈設定是持續的工作。盡可能使用 Let’s Encrypt 自動化。
14. 下一步
我們已經介紹了非對稱密碼學、簽章和憑證。但還有另一個基本原語:確保資料沒有被修改,而不需要證明誰修改了它。
在下一篇文章中:HMAC 和資料完整性——當你已經共享金鑰時,如何用對稱金鑰偵測篡改。
