Teknik Makale

HotPDF Sertifika Deposundan PDF İmzalama: CNG ile CAPI Byte Sıralaması Farkı

HotPDF, Windows Sertifika Deposunda zaten duran bir sertifikaya karşı PDF imzalarken özeti doğrudan Windows'un kendisine devreder; Windows da bu isteği iki özel anahtar arka ucundan biri üzerinden tamamlar: RSA imzasını big-endian döndüren CNG, ya da little-endian döndüren eski CryptoAPI CSP. Bu ikisini birbirine karıştırırsanız HotPDF'in gömdüğü CMS imzası, hangi arka uç yanıt verdiyse ona göre byte sırası ters döner; uyumlu bir doğrulayıcı, belge byte'larına hiç dokunulmamış olsa da imzayı geçersiz olarak raporlar

Bu tek cümlenin arkasında birbiriyle ilgisiz iki sorun gizlidir ve HotPDF'in sistem sertifikası imzalayıcısı herhangi bir şey imzalamadan önce ikisini de çözmek zorundadır. Byte sıralaması uyuşmazlığı sessizdir: imzalama çağrısı yine de True döner, PDF yine açılır ve hata yalnızca bir görüntüleyici CMS yapısını gezip reddettiğinde ortaya çıkar. İkinci sorun ise gürültülü ve C++Builder'a özgüdür: crypt32'nin yarım düzine fonksiyonu bağlanmayı reddeder, çünkü RAD Studio'nun gönderdiği import kütüphanesi bunları dışa aktarmaz. Yalnızca bir PFX dosyasıyla imzaladığınızda bu sorunların hiçbiri ortaya çıkmaz; bu yüzden genellikle PFX tabanlı tek çağrılık imzalamadan, IT departmanının kullanıcı profiline zaten kurduğu bir sertifikaya geçen geliştiricileri yakalar

Depodan sertifika seçme

HotPDF bu yolu HPDFSignPDFStreamWithSystemCertificate ve HPDFSignPDFFileWithSystemCertificate olarak sunar; her ikisi de bir THPDFCertificateStoreSelector kaydı tarafından yönlendirilir: Location (cslCurrentUser veya cslLocalMachine), StoreName (varsayılan olarak kişisel depo, 'MY'), bir SHA-1 Thumbprint ve bir AllowUI bayrağı. Thumbprint dahili olarak normalize edilir; bu yüzden Sertifika Yöneticisi arayüzünden doğrudan kopyalanan tire veya boşluklar karşılaştırma yapılmadan önce temizlenir

var
  Selector: THPDFCertificateStoreSelector;
  Options: THPDFCMSSignOptions;
begin
  Selector := THPDFCertificateStoreSelector.Default;  // cslCurrentUser, store 'MY'
  Selector.Thumbprint := 'A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0';
  Selector.AllowUI := False;

  Options := HPDFCMSDefaultOptions(palBaseline_B_B);
  if not HPDFSignPDFFileWithSystemCertificate('invoice.pdf',
    'invoice-signed.pdf', Selector, Options) then
    raise Exception.Create('Certificate-store signing failed');
end;

AllowUI = False göründüğünden daha önemlidir, çünkü doğrudan CRYPT_ACQUIRE_SILENT_FLAG'e eşlenir ve Windows bunu harfiyen uygular: eşleşen sertifikanın özel anahtarı, Windows'un henüz önbelleğe almadığı bir PIN istemi gerektiren bir akıllı kart veya token üzerinde yaşıyorsa, CryptAcquireCertificatePrivateKey bir servis sürecinden gelebilecek bir iletişim kutusu açmak yerine başarısız olur. Bu hata gürültülüdür, hemen gördüğünüz bir EHPDFCMSError'dur, ancak gerçek neden kimsenin yazmayacağı bir PIN'i bekleyen bir token iken bunu "sertifika bulunamadı" diye yanlış okumak kolaydır

CNG ve CAPI byte sıralaması konusunda neden anlaşamıyor?

Hangi arka ucun yanıt verdiği bir tahmin değildir: CryptAcquireCertificatePrivateKey bunu doğrudan bir KeySpec çıkış parametresi üzerinden bildirir ve HotPDF'in imzalayıcısının dallandığı tek değer budur. Bir CNG Anahtar Depolama Sağlayıcısı anahtarı, sentinel değer CERT_NCRYPT_KEY_SPEC ($FFFFFFFF) olarak ayarlanmış KeySpec ile döner; başka her şey geleneksel bir CryptoAPI CSP anahtarıdır. Güncel bir Windows kurulumunda verilen veya içe aktarılan çoğu kişisel sertifika, uyumluluk için eski bir CSP kabuğu hâlâ mevcut olsa da CNG'ye çözümlenir; bu yüzden HotPDF, hangi değerin döndüğüne bakmadan önce CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG'i CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG ile birlikte ister

İki arka uç yalnızca farklı fonksiyonlar çağırmakla kalmaz — bir CNG anahtarına karşı NCryptSignHash, bir CSP anahtarına karşı CryptSignHashA — ham RSA imzasını da ters byte sırasıyla geri verirler. CNG'nin çıktısı zaten PKCS#1'in beklediğiyle eşleşir: en anlamlı byte önce gelen big-endian bir oktet dizisi, tam olarak RFC 8017'nin I2OSP dönüşümünün ürettiği ve ISO 32000-1 §12.8.3 altında bir CMS SignerInfo'nun (RFC 5652) imza alanında ihtiyaç duyduğu şey. CryptoAPI'nin CryptSignHash'i ise buna karşılık imzayı little-endian döndürür; bu, klasik CSP'lerin büyük sayıları dahili olarak nasıl temsil ettiğine kadar uzanan belgelenmiş bir tuhaflıktır. CAPI yolunda ters çevirmeyi atlarsanız imzadaki her byte yanlış yerde durur; RSA matematiği yine de doğrudur, ancak bir doğrulayıcının okuduğu oktet dizisi PKCS#1'in tanımladığı dizi değildir

// CryptSignHashA returns the RSA signature least-significant byte first;
// CMS/PKCS#7 (ISO 32000-1 Section 12.8.3) needs it most-significant byte first.
for I := 0 to (Length(Signature) div 2) - 1 do
begin
  Temp := Signature[I];
  Signature[I] := Signature[High(Signature) - I];
  Signature[High(Signature) - I] := Temp;
end;

Özel bir imzalayıcı callback'i ne olacak?

HotPDF'in yerleşik sertifika deposu imzalayıcısını atlayan herkes aynı byte sıralaması kuralını devralır. HPDFCMSSignPDFStreamWithExternalSigner, bir HSM, bir akıllı kart ara katman yığını veya Windows deposunun size bir anahtar tanıtıcısı veremeyeceği herhangi başka bir şey üzerinden imzalamak için bir THPDFCMSSignDigestCallback alır — reference to function(const SignedAttributesSHA256: TBytes): TBytes tipinde bir closure. Bu callback'in arkasında hangi arka uç dururken dursun, döndürdüğü byte'lar HotPDF onları CMS yapısına katlamadan önce big-endian sırada olmalıdır

Signer :=
  function(const SignedAttributesSHA256: TBytes): TBytes
  begin
    if UsesCngKeyStorageProvider then
      Result := SignWithMyCngKey(SignedAttributesSHA256)       // already big-endian
    else
      Result := ReverseBytes(SignWithMyLegacyToken(SignedAttributesSHA256));
  end;
HPDFCMSSignPDFStreamWithExternalSigner(InputStream, OutputStream,
  CertificateDER, Signer, Options);

Burada bir sınırı açıkça belirtmekte fayda var: HotPDF'in iki yerleşik imzalama yolu — PKCS#1 dolgusuyla NCryptSignHash üzerinden CNG ve CryptSignHashA üzerinden CAPI — her ikisi de 32 byte'lık bir SHA-256 özetini imzalayan RSA anahtarlarını hedefler. Hiçbiri bir ECDSA imza biçimini müzakere etmez. Özel anahtarı EC tabanlı olan bir sertifika, sabit uzunluklu bir RSA byte dizisi varsaymak yerine ECDSA imzasını CMS'in beklediği şekilde kodlayan, HPDFCMSSignPDFStreamWithExternalSigner'a karşı kendinizin yazdığı bir imzalayıcıya ihtiyaç duyar; bu yüzden EC sertifikasıyla donatılmış bir token için yerleşik sertifika deposu imzalayıcısının doğru şeyi yapmasını beklemeyin

C++Builder CertOpenStore'u neden bağlayamıyor?

Çünkü RAD Studio'nun varsayılan C++Builder import kütüphanesi import32.lib, CertOpenStore'u ya da komşularından beşini dışa aktarmaz: CertEnumCertificatesInStore, CertGetCertificateContextProperty, CertFreeCertificateContext, CertCloseStore ve CryptAcquireCertificatePrivateKey. Delphi derlemeleri bunu asla görmez, çünkü dcc32/dcc64 statik bir external 'crypt32.dll' import'unu doğrudan PE import tablosuna çözer. C++Builder farklıdır: Delphi derleyicisi paket derlemesi için bir OMF .obj üretir, ilink32 bunu bağlar ve o noktada aynı external bildirimi yalnızca komut satırında bir import kütüphanesi bekleyen çözülmemiş bir semboldür. Bağlayıcıyı, tam crypt32.lib'in altı sembolü de dışa aktardığı Windows SDK'nın psdk dizinine yönlendirmek de bunu düzeltmez: ilink32 yalnızca komut satırında gerçekten adı geçen import kütüphanelerini bağlar — varsayılan olarak import32.lib cp32mt.lib — ve bir arama yolu eklemek o yoldan fazladan bir şey çekmesini sağlamaz. import32.lib üzerinde tdump çalıştırmak açığı doğrudan doğrular: CertOpenStore için sıfır isabet, SDK'nın crypt32.lib'inde ise altı temiz isabete karşı

HotPDF bunu, kütüphane içinde başka yerde sertifika numaralandırmasını zaten ele aldığı şekilde çözer: bağlayıcıdan bu sembolleri istemek yerine, çalışma zamanında yükler. Dahili bir THPDFCryptoProcs kaydı bir crypt32.dll tanıtıcısı, bir advapi32.dll tanıtıcısı ve on bir fonksiyon işaretçisi alanı taşır; LoadCryptoProcs her iki DLL'i de yükler ve her giriş noktasını GetProcAddress ile tam olarak bir kez, HPDFSignPDFStreamWithSystemCertificate'in başında çözer; imzalama akışının derinliklerinde bir access violation ile daha sonra başarısız olmak yerine eksik bir şey varsa hemen EHPDFCMSError fırlatır

type
  TCertOpenStoreFn = function(lpszStoreProvider: Pointer; dwEncodingType: DWORD;
    hCryptProv: NativeUInt; dwFlags: DWORD; pvPara: Pointer): HCERTSTORE; stdcall;
var
  Crypt32Handle: HMODULE;
  CertOpenStore: TCertOpenStoreFn;
begin
  Crypt32Handle := LoadLibrary('crypt32.dll');
  if Crypt32Handle = 0 then
    raise Exception.Create('crypt32.dll could not be loaded');
  @CertOpenStore := GetProcAddress(Crypt32Handle, 'CertOpenStore');
  // ... use CertOpenStore, then FreeLibrary(Crypt32Handle) when signing returns
end;

Yükleme her yardımcı fonksiyon içinde tembelce değil, çağrı başına bir kez gerçekleşir; çünkü CNG ile CAPI arasında seçim yapan closure, yüklenen fonksiyon tablosunu değer olarak yakalar ve HPDFCMSSignPDFStreamWithExternalSigner'a olan callback dahil, tüm imzalama akışı boyunca canlı kalmak zorundadır; her iki DLL tanıtıcısı da imzalama bittiğinde veya bir hata fırlattığında en dıştaki finally bloğunda serbest bırakılır. Bunların hiçbiri genel yüzeye dokunmaz: HPDFSignPDFStreamWithSystemCertificate, HPDFSignPDFFileWithSystemCertificate ve THPDFCertificateStoreSelector daha önce sahip oldukları tam imzaları korur; bu yüzden düzeltmeyi almak mevcut çağıranlar için bir kod değişikliği değil, yalnızca bir yeniden derlemedir

Bu neyi kapsamıyor?

Byte sıralamasını ve C++Builder bağlantısını doğru yapmak, bir doğrulayıcının ayrıştırabileceği ve aritmetik olarak kontrol edebileceği bir CMS SignerInfo üretir; ancak bu doğrulayıcının arkasındaki sertifikaya güvenip güvenmemesi gerektiği konusunda bir şey söylemez, çünkü zincir oluşturma, iptal kontrolü ve zaman damgası politikası, byte sıralaması doğruluğunun bedavaya kazandırdığı bir şey değil, CMS seçenekleri üzerinden katmanlanan ayrı konulardır. İki bakım detayı kriptografi kadar önemlidir: sertifika aramasının döndürdüğü PCCERT_CONTEXT, depo kapanmadan önce CertFreeCertificateContext ile serbest bırakılmalıdır ve API çağıranın sahibi olduğunu bildirdiğinde elde edilen bir CNG veya CSP anahtar tanıtıcısı, asla diğerininki değil, yalnızca eşleşen arka ucun kendi çağrısı üzerinden serbest bırakılmalıdır. Tüm bunlardan sonra elde ettiğiniz svValid sonucu beklediğinizden daha dar çıkarsa, PDF imzalarını doğrulama üzerine olan makale bu bayrağın tam olarak neyi vaat edip neyi etmediğini ortaya koyar. Sertifika burada tüm süreç boyunca Windows'un gözetiminde kaldığı için, sertifika deposu imzalama tüm bir saldırı yüzeyini atlar: ayrıştırılacak bir PKCS#12 dosyası ve kendinizin gezmesi gereken bir ASN.1 yoktur; bu da HotPDF'in PKCS#12 ve ASN.1 sertleştirmesinin PFX dosya imzalama yolu için ele aldığı sorundur

Sertifika deposu imzalama, PFX imzalama ve harici imzalayıcı callback'leri, Delphi ve C++Builder için HotPDF PDF bileşeni içindeki aynı CMS/PKCS#7 boru hattına açılan üç kapıdır; doğru olanı seçmek büyük ölçüde özel anahtarı kimin tutmasına izin verildiğine bağlıdır: sizin süreciniz mi, bir PFX dosyası mı, yoksa Windows'un kendisi mi