Bài viết kỹ thuật

Chứng chỉ test tự ký trong Delphi với CryptoAPI

Hàm PLCreateSelfSignedCertificate của PDFlibPas dựng một chứng chỉ RSA/SHA-256 tự ký và xuất nó, gồm cả khóa riêng, thẳng vào một file PFX được bảo vệ bằng mật khẩu, chỉ dùng Win32 CryptoAPI đã cài sẵn trên mọi máy Windows. Không công cụ ngoài, không cơ quan cấp chứng chỉ, không bước makecert hay OpenSSL thủ công: một lệnh gọi hàm, một chứng chỉ đủ tốt để chạy một test ký

Kịch bản khiến hàm này đáng có gần như luôn là một pipeline CI. Một smoke test ký cần một PFX thật với một khóa riêng thật đứng sau nó, và commit một cái vào repository là vấn đề bảo mật riêng của nó, vì một khóa riêng đã commit là một khóa riêng đã rò rỉ ngay từ khoảnh khắc commit đó đáp xuống. Shell ra makecert.exe hay một lệnh gọi OpenSSL từ một script build cũng hoạt động, nhưng khi đó pipeline phụ thuộc vào một công cụ phải được cài đặt, tìm thấy trên PATH, và giữ nhất quán phiên bản trên mọi build agent. Việc tạo chứng chỉ bên trong chính tiến trình chạy test, với chính các lệnh gọi Win32 CryptoAPI mà Windows đã gửi kèm sẵn, hoàn toàn loại bỏ phụ thuộc đó

PLCreateSelfSignedCertificate thực sự tạo ra gì?

PLCreateSelfSignedCertificate tạo ra một file PFX được bảo vệ bằng mật khẩu mang một chứng chỉ RSA tự ký và khóa riêng của nó, được ký bằng sha256RSA, điều khiển bởi năm tham số: SubjectName, PFXFileName, PFXPassword, ValidDays, và KeyBits, và nó trả về một cờ thành công Boolean thuần túy. SubjectName chấp nhận một chuỗi X.500 đầy đủ như 'CN=Alice, O=Example', và một tên trần trụi không có dấu = nào tự động được thêm tiền tố CN=. ValidDays dưới 1 rơi về mặc định 365, và KeyBits nằm ngoài phạm vi 1024 đến 16384 rơi về mặc định 2048. PDFlibPas đã phát hành hàm này kể từ v3.224.0, có thể tiếp cận không chỉ từ unit Delphi mà còn qua các bề mặt DLL và ActiveX, và comment tài liệu riêng của nó thẳng thắn về nơi nó ngừng hữu ích: mọi trình xem chính thống đánh dấu một chứng chỉ tự ký là không đáng tin trừ khi ai đó cài đặt nó tường minh, nên hãy coi những gì nó tạo ra như một chứng chỉ để thực hiện một đường code, không phải một chữ ký mà bất kỳ ai bên ngoài đội của bạn nên được yêu cầu dựa vào

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;

Vì sao CryptGenKey mã hóa độ dài khóa trong tham số flags?

CryptGenKey đóng gói hai thiết lập không liên quan vào một tham số dwFlags duy nhất. Word thấp mang các cờ hành vi, CRYPT_EXPORTABLE trong số đó, trong khi word cao, cho một khóa trao đổi khóa RSA, mang độ dài khóa được yêu cầu tính bằng bit. Truyền 2048 như thể nó chỉ là một cờ khác đưa nó vào word thấp thay vào đó, nơi nó không khớp với bất kỳ cờ hành vi nào CryptoAPI định nghĩa, nên lệnh gọi sinh ra một khóa ở bất cứ độ dài mặc định nào mà provider rơi về thay vì độ dài caller tưởng mình đã yêu cầu. Việc lấy được một khóa RSA 2048-bit thật có nghĩa là dịch số đó vào word cao trước

// 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;

Điều gì xảy ra nếu bạn quên CRYPT_EXPORTABLE?

Bỏ CRYPT_EXPORTABLE khỏi cùng giá trị flags đó và CryptGenKey vẫn thành công, nhưng nó đánh dấu khóa riêng đã sinh không thể xuất ở cấp CSP. Mọi thứ ở hạ nguồn cũng tiếp tục báo cáo thành công: CertCreateSelfSignCertificate trả về một context chứng chỉ hợp lệ, và PFXExportCertStoreEx, ngay cả khi được gọi với EXPORT_PRIVATE_KEYS, vẫn thành công và viết ra một file PFX mở được, phân tích được, và trông hoàn toàn bình thường. Thứ nó không chứa là khóa riêng, vì CSP từ chối để nó thoát khỏi container khóa, và PFXExportCertStoreEx không bao giờ coi sự từ chối đó là lý do để làm thất bại toàn bộ lượt xuất

Lỗi chỉ lộ ra sau đó, và ở một nơi hoàn toàn khác: một lệnh gọi ký mở PFX đó, tìm thấy một chứng chỉ không có khóa riêng đính kèm, và báo cáo chính xác lỗi mà bạn sẽ nhận được từ một PFX hỏng hay sai, không phải từ một cờ bị thiếu ba lớp ở thượng nguồn. Bất kỳ ai debug chỉ từ phía ký có thể đốt cả buổi chiều trên sai file trước khi nhận ra lỗi thật là một bit duy nhất bị thiếu tại thời điểm sinh khóa, trong một lệnh gọi hàm hoàn toàn khác, có thể trong một script build hoàn toàn khác

Vì sao ProvType phải khớp giữa CryptAcquireContextW và chứng chỉ?

ProvType phải khớp vì CertCreateSelfSignCertificate giải quyết khóa riêng của chứng chỉ mới thông qua một bản ghi CRYPT_KEY_PROV_INFO, và một trường trong bản ghi đó, ProvType, phải nêu tên đúng cùng giá trị kiểu CSP được truyền cho CryptAcquireContextW khi container khóa được mở, PROV_RSA_AES, về mặt số là 24, trong triển khai của PDFlibPas. Đặt ProvType thành không, hoặc thành bất kỳ hằng số provider nào khác ngoài cái container thực sự thuộc về, và chứng chỉ vẫn có thể được tạo ra, nhưng liên kết đã ghi lại của nó trở lại khóa riêng không còn giải quyết được đến container giữ nó, điều này lộ ra sau này như một lỗi ký hay xuất không liên quan gì đến nội dung mật mã thực sự của chứng chỉ

// 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

Ghép lại: từ container GUID đến PFX được bảo vệ bằng mật khẩu

Chuỗi lệnh gọi bên trong PLCreateSelfSignedCertificate theo một đường thẳng, mở một container khóa mới được đặt tên theo một GUID vừa sinh để các lượt chạy CI đồng thời không bao giờ va chạm về tên container, sinh cặp khóa RSA bên trong nó với hai cờ được nói đến ở trên, mã hóa SubjectName thành một blob tên X.500 thông qua CertStrToNameW, và gọi CertCreateSelfSignCertificate với một khung hiệu lực tính từ ValidDays và đưa qua như một cấu trúc dạng SYSTEMTIME thuần túy. Context chứng chỉ kết quả đi vào một kho chứng chỉ trong bộ nhớ được mở bằng CertOpenStoreCERT_STORE_PROV_MEMORY, thuần túy để PFXExportCertStoreEx có một kho để xuất từ đó, vì API đó hoạt động trên một handle kho thay vì một context chứng chỉ trần trụi

// 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 tự thân theo quy ước hai-lượt Win32 thông thường: gọi nó một lần với một bộ đệm độ dài không để biết PFX cần bao nhiêu byte, cấp phát ngần đó, rồi gọi lại lần nữa để điền bộ đệm. Một khi các byte nằm trên đĩa, PDFlibPas xóa container khóa dùng-một-lần bằng CRYPT_DELETEKEYSET thay vì để nó lại, vì PFX đã mang theo bản sao riêng của nó của mọi byte tài liệu khóa mà container giữ. Bỏ qua lượt dọn dẹp đó và mỗi lệnh gọi đến PLCreateSelfSignedCertificate để lại một container khóa mồ côi, tên GUID nằm trong profile của người dùng gọi, chính xác là loại rò rỉ mà một agent CI chạy hàm này trên mỗi build sẽ tích lũy qua nhiều tháng trước khi ai đó nhận ra

Một chứng chỉ tự ký có an toàn để dùng cho việc ký sản xuất không?

Không: một chứng chỉ tự ký an toàn để thực hiện một đường code ký và không an toàn cho một chữ ký mà bất kỳ ai bên ngoài đội được kỳ vọng tin tưởng, vì không gì nối nó trở lại một root mà phần mềm của một bên tin cậy đã tin tưởng sẵn. Bước tiếp theo tự nhiên cho một PFX như thế này là một lệnh gọi ký thực sự, được nói đến trong xây dựng một xưởng tuân thủ chuẩn và ký trong Delphi với PDFlibPas, nơi một PFX được xây dựng theo cách này điều khiển nửa ký của một pipeline cũng chạy preflight PDF/A và kiểm toán ByteRange. Tuy nhiên, việc ký chỉ là một nửa những gì đứng quanh một chứng chỉ, và nửa còn lại chính xác là nơi một lá tự ký được cho là sẽ thất bại: ký và xác thực PAdES trong Delphi với PDFlibPas nói đến các kiểm tra chuỗi tin cậy mà một bộ xác thực tuân thủ chuẩn chạy, và một bộ xác thực duyệt chuỗi trở lại một root đáng tin cậy không có lý do gì để tin tưởng một chứng chỉ mà hàm này vừa phát minh năm phút trước từ hư vô

PLCreateSelfSignedCertificate là một hàm trong số các API chứng chỉ và ký trong thư viện PDF PDFlibPas dành cho Delphi và C++Builder, và nó tồn tại chính xác cho khoảng trống được mô tả ở đây: một test ký cần một cặp khóa thật đứng sau nó và không cần gì bên ngoài để tạo ra một cặp