chundev
日期:2026-05-23
CSR(Certificate Signing Request,PKCS#10)是一個自帶簽名的 blob,裡面有「公開的 public key + 你要的 subject 名稱」並用對應的 private key 簽過。CA 收到後驗證自簽名(證明你真的有那把 private key)、再用 CA 自己的 private key 重新簽出一張正式 cert。整個流程的目的只有一個——讓 private key 永遠不離開申請者。理解這點就理解了 PKI 大部分的設計。
很自然的問題:「我要憑證,直接叫 CA 給我一張不就好了?為什麼要產什麼 CSR?」
想像沒有 CSR 的流程:
我 → CA:「請給我 foo.example.com 的憑證」
CA → 我:「好,那我幫你產一對 key pair,cert 跟 private key 一起給你」
問題:CA 知道你的 private key。任何 server-side 的事故(DB 被駭、員工內鬼、log 不小心 dump key)都會洩漏 key,攻擊者就能假冒你。
CSR 翻轉這件事:
我(在本機產 key pair)→ 留下 private key,從 public key 做出 CSR
我 → CA:「請簽這張 CSR」
CA:驗 CSR 自簽名 → 確認身份(out of band)→ 用 CA's private key 簽出 cert → 回給我
我:拿 cert 配上**從來沒給過任何人的 private key**,就能對外提供 TLS
Private key 從生成到使用全程留在我這台機器——這是 PKI 整個設計的核心。CSR 是達成這個 invariant 的具體機制。
CSR 是 ASN.1 DER 編碼、PEM base64 包裝的結構(PKCS#10 標準):
-----BEGIN CERTIFICATE REQUEST-----
MIICsDCCAZgCAQAwOTELMAkGA1UEBhMCVFcxETAPBgNVBAoMCEtZTU8gRGV2MRcw
FQYDVQQDDA5mbG93LmRldi5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
...
-----END CERTIFICATE REQUEST-----
openssl req -in server.csr -noout -text 看內容:
Certificate Request:
Data:
Version: 1 (0x0)
Subject: C=TW, O=KYMO Dev, CN=foo.example.local
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:b3:b3:1d:17:5c:05:bc:9e:6c:b8:f1:69:fe:2b:
...
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:foo.example.local, IP Address:192.168.1.10
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
7b:5b:7c:91:c8:6f:fc:53:62:5a:9d:e8:...
三個欄位最重要:
Requested Extensions(例如 SAN)是給 CA 參考的「我希望你也加上這些」,但 CA 完全有權決定要不要照搬。
這個簽名是用 CSR 申請者的 private key 對「Data 區塊」算出的。CA 可以用 CSR 裡 Subject Public Key Info 那把 public key 自己驗這個簽名。驗過代表:「對方真的有那把 private key」——不然簽不出這個 signature。
這是 PKI 的 proof of possession 設計:在不傳 private key 的前提下,證明你有它。
可以親自驗:
openssl req -in server.csr -noout -verify
# Certificate request self-signature ok
CA 在簽之前一定會跑這個檢查。沒過 = 拒簽。
openssl x509 -req \
-in server.csr \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-days 365 \
-copy_extensions copy \
-out server.crt
CA 做了五件事:
注意 CSR 完成任務後就丟掉了——cert 跟 CSR 是兩個獨立的 X.509 結構,cert 不會夾帶 CSR。
CSR 走的時候帶走 public key,cert 回來的時候帶回同一個 public key(CA 只是換了個 wrapper)。但實際操作中可能發生:
所以把新 cert 寫入 nginx 之前必須驗:「這張 cert 內含的 public key,跟我 host 上的 private key 真的配對嗎?」
實作上比公開金鑰整個比對更快的做法是比 modulus(RSA 的話):
CERT_MOD=$(openssl x509 -in server.crt -noout -modulus | openssl md5 | awk '{print $NF}')
KEY_MOD=$(openssl rsa -in server.key -noout -modulus | openssl md5 | awk '{print $NF}')
[ "$CERT_MOD" = "$KEY_MOD" ] || { echo "ERROR: not a pair"; exit 1; }
RSA 的 modulus n = p*q 是 public 資訊;cert 跟 key 的 modulus 一致 = 是同一對。md5 hash 只是縮短比對長度,不是安全用途(兩邊都來自我們,不是攻擊面)。
ECDSA 的情況改比 -pubout(拉出 public key)後做 md5:
CERT_PUB=$(openssl x509 -in server.crt -noout -pubkey | openssl md5)
KEY_PUB=$(openssl pkey -in server.key -pubout | openssl md5)
可以自己跑一遍體驗 PKI:
# 1. 產 server private key(不能離開這台機器)
openssl genrsa -out server.key 2048
chmod 600 server.key
# 2. 從 key 產 CSR(subject + 可選 SAN)
openssl req -new -key server.key -out server.csr \
-subj "/C=TW/O=Demo/CN=foo.example.local" \
-addext "subjectAltName=DNS:foo.example.local,IP:192.168.1.10"
# 3. 把 CSR 給 CA(這裡自己當 CA 給示範)
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
-keyout ca.key -out ca.crt -subj "/CN=Demo Root CA"
openssl x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-days 365 -copy_extensions copy \
-out server.crt
# 4. 看 cert 裡的 public key 是不是還是同一把(modulus 比一比)
openssl x509 -in server.crt -noout -modulus | openssl md5
openssl req -in server.csr -noout -modulus | openssl md5
openssl rsa -in server.key -noout -modulus | openssl md5
# 三者 md5 應該完全相同
# 5. 看 cert 全貌
openssl x509 -in server.crt -noout -text
跑完會發現:cert 跟 CSR 的 modulus、cert 跟 key 的 modulus,全部一樣——public key 一路保持不變,只是 wrapper 從 CSR 變成 X.509 cert。
openssl modulus 比 md5 是業界標準寫法;不驗就等著拿到別張 cert 還寫進 nginx 起不來。openssl req man page — 產 CSR / 自簽 cert 的所有選項openssl x509 man page — 簽 CSR 變 cert / 檢查 cert / 取 modulus