PDFlibPas 的 PLCreateSelfSignedCertificate 函式會建立自我簽署的 RSA/SHA-256 憑證,並將它連同私密金鑰直接匯出至受密碼保護的 PFX 檔案,只使用每台 Windows 電腦都已安裝的 Win32 CryptoAPI。不需要外部工具、不需要憑證授權單位,也不需要手動執行 makecert 或 OpenSSL:一次函式呼叫,就能取得足以驅動簽署測試的憑證
這個函式最常派上用場的情境,幾乎總是 CI 管線。簽署冒煙測試需要一個背後確實有私密金鑰的 PFX,而將其中一個提交至儲存庫本身就是安全性問題,因為提交一旦寫入,提交的私密金鑰就從那一刻起成為外洩的私密金鑰。從建置指令檔呼叫 makecert.exe 或 OpenSSL 也能運作,但如此一來管線就依賴一個必須安裝、必須能在 PATH 中找到,且必須在每個建置代理程式上維持版本一致的工具。在執行測試的同一個程序中,使用 Windows 已隨附的相同 Win32 CryptoAPI 呼叫來建立憑證,就能完全移除這項相依性
PLCreateSelfSignedCertificate 實際會產生什麼?
PLCreateSelfSignedCertificate 會產生一個受密碼保護的 PFX 檔案,其中包含自我簽署的 RSA 憑證及其私密金鑰,以 sha256RSA 簽署,由五個參數驅動:SubjectName、PFXFileName、PFXPassword、ValidDays 和 KeyBits,並回傳單純的 Boolean 成功旗標。SubjectName 接受完整的 X.500 字串,例如 'CN=Alice, O=Example',而不含 = 符號的純名稱會自動加上 CN= 前綴。小於 1 的 ValidDays 會退回 365,超出 1024 至 16384 範圍的 KeyBits 會退回 2048。PDFlibPas 自 v3.224.0 起便提供這個函式,不只能從 Delphi 單元使用,也能透過 DLL 和 ActiveX 介面使用,而其本身的文件註解清楚說明了實用性的界線:所有主流檢視器都會將自我簽署憑證標示為不受信任,除非有人明確安裝它,因此請將它產生的憑證視為用來運行程式碼路徑的憑證,而不是要求團隊以外任何人信賴的簽章
var
Success: Boolean;
begin
Success := PLCreateSelfSignedCertificate(
'CN=PDFlibPas CI Test, O=Example Corp',
'ci-test-signer.pfx',
'a-strong-throwaway-password',
365, // ValidDays
2048); // KeyBits
if not Success then
raise Exception.Create('Self-signed certificate generation failed');
end;
為什麼 CryptGenKey 會在 flags 參數中編碼金鑰長度?
CryptGenKey 將兩個無關的設定封裝在單一 dwFlags 參數中。低位字承載行為旗標,其中包括 CRYPT_EXPORTABLE,而對 RSA 金鑰交換金鑰而言,高位字則承載以位元為單位的要求金鑰長度。若將 2048 當成另一個旗標傳入,它會落在低位字中,在那裡不符合 CryptoAPI 定義的任何行為旗標,因此呼叫會產生提供者退回的預設長度金鑰,而不是呼叫者以為自己要求的長度。要取得實際的 2048 位元 RSA 金鑰,必須先將數字移至高位字
// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
(Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
Exit;
忘記 CRYPT_EXPORTABLE 會發生什麼事?
從同一個 flags 值中移除 CRYPT_EXPORTABLE,CryptGenKey 仍然會成功,但它會在 CSP 層級將產生的私密金鑰標記為不可匯出。後續所有步驟也會持續回報成功:CertCreateSelfSignCertificate 會回傳有效的憑證內容,而 PFXExportCertStoreEx 即使以 EXPORT_PRIVATE_KEYS 呼叫,也會成功並寫出一個能開啟、能剖析且看起來完全普通的 PFX 檔案。它沒有包含的是私密金鑰,因為 CSP 拒絕讓它離開金鑰容器,而 PFXExportCertStoreEx 從未將這項拒絕視為整個匯出作業失敗的理由
失敗只會在稍後、完全不同的地方出現:簽署呼叫開啟該 PFX,找到一個沒有附加私密金鑰的憑證,並回報你從損毀或錯誤 PFX 會得到的確切錯誤,而不是上游三層的一個遺漏旗標。只從簽署端進行偵錯的人,可能會花一整個下午在錯誤的檔案上,直到發現真正的錯誤是在產生金鑰時少了一個位元,而且位於完全不同的函式呼叫中,甚至可能位於完全不同的建置指令檔中
為什麼 CryptAcquireContextW 與憑證之間的 ProvType 必須相符?
ProvType 必須相符,因為 CertCreateSelfSignCertificate 會透過 CRYPT_KEY_PROV_INFO 記錄解析新憑證的私密金鑰,而該記錄中的一個欄位 ProvType 必須指定與開啟金鑰容器時傳給 CryptAcquireContextW 的完全相同 CSP 類型值,在 PDFlibPas 的實作中就是數值為 24 的 PROV_RSA_AES。將 ProvType 設為零,或設為任何不同於容器實際所屬提供者的提供者常數,憑證仍然可能建立成功,但它記錄的私密金鑰連結就無法再解析至存放該金鑰的容器,之後會表現為簽署或匯出失敗,而這與憑證實際的密碼編譯內容毫無關係
// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES; // same constant, both call sites
整合起來:從 GUID 容器到受密碼保護的 PFX
PLCreateSelfSignedCertificate 內部的呼叫鏈遵循一條直線:開啟一個以新產生 GUID 命名的新金鑰容器,讓並行 CI 執行不會因容器名稱而衝突;使用上述兩個旗標在其中產生 RSA 金鑰組;透過 CertStrToNameW 將 SubjectName 編碼為 X.500 名稱 blob;再以根據 ValidDays 計算並以單純 SYSTEMTIME 形式結構傳入的有效期間,呼叫 CertCreateSelfSignCertificate。產生的憑證內容會放入以 CertOpenStore 和 CERT_STORE_PROV_MEMORY 開啟的記憶體內憑證存放區,純粹是為了讓 PFXExportCertStoreEx 有可供匯出的存放區,因為該 API 針對的是存放區控制代碼,而不是單獨的憑證內容
// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_DELETEKEYSET);
PFXExportCertStoreEx 本身遵循一般的 Win32 兩階段慣例:先以零長度緩衝區呼叫一次,以得知 PFX 需要多少位元組;配置該大小的空間後,再次呼叫以填入緩衝區。位元組寫入磁碟後,PDFlibPas 會使用 CRYPT_DELETEKEYSET 刪除暫存金鑰容器,而不是將它留下,因為 PFX 已經攜帶容器所持有的每一個金鑰材料位元組。略過清理步驟,PLCreateSelfSignedCertificate 的每次呼叫都會在呼叫端使用者的設定檔中留下孤立且以 GUID 命名的金鑰容器,而這正是每次建置都執行此函式的 CI 代理程式會累積數月、直到有人注意到為止的那種洩漏
自我簽署憑證是否適合用於正式環境簽署?
不適合:自我簽署憑證適合用來運行簽署程式碼路徑,卻不適合用於預期團隊以外的人信賴的簽章,因為沒有任何鏈結能將它連回依賴方軟體已信任的根憑證。這類 PFX 的自然下一步是實際的簽署呼叫,相關內容涵蓋於在 Delphi 中使用 PDFlibPas 建立合規與簽署工作台,其中這樣建立的 PFX 會驅動管線的簽署部分,而該管線也會執行 PDF/A 預檢與 ByteRange 稽核。不過,簽署只是憑證周邊機制的一半,另一半正是自我簽署葉憑證應該失敗的地方:在 Delphi 中使用 PDFlibPas 進行 PAdES 簽署與驗證涵蓋符合性驗證器執行的信任鏈檢查,而沿鏈結回溯至受信任根憑證的驗證器,沒有理由信任這個函式五分鐘前從無到有建立的憑證
PLCreateSelfSignedCertificate 是適用於 Delphi 與 C++Builder 的 PDFlibPas PDF 函式庫中憑證與簽署 API 的其中一個函式,存在的目的正是填補本文描述的缺口:簽署測試需要一個背後確實有金鑰組,且不需要外部工具來建立它