Share Notes

chundev

View the Project on GitHub latteouka/share-notes

CSR 是什麼?換憑證為什麼一定要先產 CSR?

日期:2026-05-23


TL;DR

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 裡到底有什麼?

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:...

三個欄位最重要:

  1. Subject — 你想要憑證上印的名字(CN + 可選 O/OU/C/L/ST)
  2. Subject Public Key Info — 你的 public key(最終會原樣搬進 cert)
  3. Signature Value — 用對應 private key 對前面所有欄位算出的簽名

Requested Extensions(例如 SAN)是給 CA 參考的「我希望你也加上這些」,但 CA 完全有權決定要不要照搬。

Signature Value 的關鍵作用

這個簽名是用 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 在簽之前一定會跑這個檢查。沒過 = 拒簽。


CA 怎麼簽 CSR 變成 cert?

openssl x509 -req \
  -in server.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -days 365 \
  -copy_extensions copy \
  -out server.crt

CA 做了五件事:

  1. 驗 CSR 自簽名 — 證明對方有 private key
  2. 驗對方身份out-of-band,不在密碼學機制裡)——可能是 DNS challenge(Let’s Encrypt)、email 確認(傳統 CA)、紙本申請(企業內部 CA)
  3. 挑出要保留的欄位 — Subject DN、Public Key Info、選定的 extensions 從 CSR 搬進新 cert
  4. 加 CA 自己決定的欄位 — Issuer DN(CA 自己)、Serial Number(CA 內部編號)、NotBefore/NotAfter(有效期)、AuthorityKeyIdentifier、CRLDistributionPoints 等
  5. 用 CA’s private key 對整張 cert 簽名 — 這個簽名是「信任傳遞」的核心:browser 認 CA、CA 簽 cert、所以 browser 認 cert

注意 CSR 完成任務後就丟掉了——cert 跟 CSR 是兩個獨立的 X.509 結構,cert 不會夾帶 CSR。


為什麼安裝 cert 時必須驗「cert 跟 host 私鑰配對」?

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)

完整 openssl 流程(從零到一)

可以自己跑一遍體驗 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。


學到的事


參考資料