Teknik Makale

CryptoAPI ile Delphi'de Kendinden İmzalı Test Sertifikaları

PDFlibPas'ın PLCreateSelfSignedCertificate fonksiyonu, kendinden imzalı bir RSA/SHA-256 sertifikası kurar ve onu, her Windows makinesinde zaten kurulu olan Win32 CryptoAPI'den başka bir şey kullanmadan, özel anahtarı dahil, doğrudan şifre korumalı bir PFX dosyasına dışa aktarır. Harici bir araç yok, sertifika otoritesi yok, elle makecert veya OpenSSL adımı yok: tek bir fonksiyon çağrısı, bir imzalama testini çalıştırmaya yetecek bir sertifika

Bu fonksiyonu sahip olmaya değer kılan senaryo neredeyse her zaman bir CI boru hattıdır. Bir imzalama duman testi, arkasında gerçek bir özel anahtar olan gerçek bir PFX'e ihtiyaç duyar ve birini depoya kaydetmek kendi başına bir güvenlik sorunudur, çünkü kaydedilmiş bir özel anahtar, o commit indiği andan itibaren sızdırılmış bir özel anahtardır. Bir derleme betiğinden makecert.exe'ye veya bir OpenSSL çağrısına kabuk açmak da işe yarar, ama sonra boru hattı, kurulması, PATH'te bulunması ve her derleme ajanı genelinde sürüm tutarlı tutulması gereken bir araca bağımlı olur. Sertifikayı testi çalıştıran aynı süreç içinde, Windows'un zaten gönderdiği aynı Win32 CryptoAPI çağrılarıyla üretmek, bu bağımlılığı tamamen ortadan kaldırır

PLCreateSelfSignedCertificate gerçekte ne üretir?

PLCreateSelfSignedCertificate, kendinden imzalı bir RSA sertifikası ve özel anahtarını tutan, sha256RSA ile imzalanmış, şifre korumalı bir PFX dosyası üretir; beş parametre tarafından yönlendirilir: SubjectName, PFXFileName, PFXPassword, ValidDays ve KeyBits ve düz bir Boolean başarı bayrağı döndürür. SubjectName, 'CN=Alice, O=Example' gibi tam bir X.500 dizesini kabul eder ve içinde = işareti olmayan çıplak bir ad, otomatik olarak CN= ile önekli hale getirilir. ValidDays 1'in altındaysa 365'e düşer ve KeyBits 1024 ile 16384 aralığının dışındaysa 2048'e düşer. PDFlibPas bu fonksiyonu v3.224.0'dan beri gönderir, yalnızca Delphi biriminden değil DLL ve ActiveX yüzeylerinden de erişilebilir ve kendi belge yorumu, nerede yararlı olmaktan çıktığı konusunda açıktır: her ana akım görüntüleyici, biri açıkça kurmadıkça kendinden imzalı bir sertifikayı güvenilmez olarak işaretler; bu yüzden ürettiğini, ekibinizin dışındaki birinin güvenmesi istenecek bir imza değil, bir kod yolunu çalıştırmak için bir sertifika olarak ele alın

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 anahtar uzunluğunu neden bayraklar parametresinde kodlar?

CryptGenKey, iki ilgisiz ayarı tek bir dwFlags parametresine paketler. Alt kelime, bunların arasında CRYPT_EXPORTABLE'ın da bulunduğu davranış bayraklarını taşırken, üst kelime, bir RSA anahtar-değişimi anahtarı için, bit cinsinden istenen anahtar uzunluğunu taşır. 2048'i sanki başka bir bayrakmış gibi geçirmek, onu bunun yerine alt kelimeye indirir; burada CryptoAPI'nin tanımladığı hiçbir davranış bayrağıyla eşleşmez; bu yüzden çağrı, çağıranın istediğini sandığı uzunlukta değil, sağlayıcının varsayılan olarak düştüğü her ne uzunluktaysa onda bir anahtar üretir. Gerçek 2048 bitlik bir RSA anahtarı elde etmek, sayıyı önce üst kelimeye kaydırmak anlamına gelir

// 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'ı unutursanız ne olur?

CRYPT_EXPORTABLE'ı aynı bayraklar değerinden düşürün ve CryptGenKey yine de başarılı olur, ama üretilen özel anahtarı CSP düzeyinde dışa aktarılamaz olarak işaretler. Aşağı akıştaki her şey de başarı bildirmeye devam eder: CertCreateSelfSignCertificate geçerli bir sertifika bağlamı döndürür ve PFXExportCertStoreEx, EXPORT_PRIVATE_KEYS ile çağrılsa bile, yine de başarılı olur ve açılan, ayrıştırılan ve tamamen sıradan görünen bir PFX dosyası yazar. İçermediği şey ise özel anahtardır, çünkü CSP onun anahtar kapsayıcısından çıkmasına izin vermeyi reddetmiştir ve PFXExportCertStoreEx bu reddi hiçbir zaman tüm dışa aktarımı başarısız kılacak bir neden olarak ele almaz

Hata yalnızca daha sonra ve tamamen başka bir yerde ortaya çıkar: bir imzalama çağrısı o PFX'i açar, ekli özel anahtarı olmayan bir sertifika bulur ve tam olarak akış yukarısında üç katman önce eksik bir bayraktan değil, bozuk veya yanlış bir PFX'ten alacağınız hatayı bildirir. Yalnızca imzalama tarafından hata ayıklayan biri, gerçek hatanın tamamen farklı bir fonksiyon çağrısında, belki de tamamen farklı bir derleme betiğinde, anahtar üretimi sırasında eksik tek bir bit olduğunu fark etmeden önce bir öğleden sonrayı yanlış dosyada harcayabilir

ProvType, CryptAcquireContextW ile sertifika arasında neden eşleşmelidir?

ProvType eşleşmelidir, çünkü CertCreateSelfSignCertificate, yeni sertifikanın özel anahtarını bir CRYPT_KEY_PROV_INFO kaydı aracılığıyla çözer ve o kayıttaki bir alan, ProvType, anahtar kapsayıcısı açıldığında CryptAcquireContextW'a geçirilen tam olarak aynı CSP türü değerini adlandırmak zorundadır; PDFlibPas'ın uygulamasında sayısal olarak 24 olan PROV_RSA_AES. ProvType'ı sıfıra veya kapsayıcının gerçekte ait olduğu sağlayıcıdan başka herhangi bir sağlayıcı sabitine ayarlayın ve sertifika yine de oluşturulabilir, ama özel anahtara geri kaydedilen bağlantısı artık onu tutan kapsayıcıya çözülmez; bu, daha sonra sertifikanın gerçek kriptografik içeriğiyle hiçbir ilgisi olmayan bir imzalama veya dışa aktarım hatası olarak yüzeye çıkar

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

Bir araya getirmek: GUID kapsayıcısından şifre korumalı PFX'e

PLCreateSelfSignedCertificate içindeki çağrı zinciri tek bir düz çizgiyi izler: eşzamanlı CI çalıştırmalarının kapsayıcı adları üzerinde asla çakışmaması için yeni üretilmiş bir GUID'in adını taşıyan taze bir anahtar kapsayıcısı açmak, RSA anahtar çiftini yukarıda ele alınan iki bayrakla onun içinde üretmek, SubjectName'i CertStrToNameW aracılığıyla bir X.500 ad blob'una kodlamak ve ValidDays'ten hesaplanan ve düz bir SYSTEMTIME şeklindeki yapı olarak devredilen bir geçerlilik penceresiyle CertCreateSelfSignCertificate'i çağırmak. Ortaya çıkan sertifika bağlamı, PFXExportCertStoreEx'in dışa aktaracağı bir deposu olsun diye, CertOpenStore ve CERT_STORE_PROV_MEMORY ile açılan bellek içi bir sertifika deposuna gider, çünkü bu API çıplak bir sertifika bağlamına değil bir depo tutamacına karşı çalışır

// 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'in kendisi sıradan Win32 iki geçişli kuralını izler: kaç baytın PFX'e ihtiyaç duyduğunu öğrenmek için önce sıfır uzunlukta bir arabellekle çağırın, bu kadarını tahsis edin, ardından arabelleği doldurmak için tekrar çağırın. Baytlar diskte olduğunda, PDFlibPas geçici anahtar kapsayıcısını arkasında bırakmak yerine CRYPT_DELETEKEYSET ile siler, çünkü PFX zaten kapsayıcının tuttuğu anahtar materyalinin her baytının kendi kopyasını taşır. Bu temizliği atlayın ve PLCreateSelfSignedCertificate'e her çağrı, çağıran kullanıcının profilinde oturan yetim, GUID adlı bir anahtar kapsayıcısı bırakır ki bu, her derlemede bu fonksiyonu çalıştıran bir CI ajanının, kimse fark etmeden önce aylar boyunca biriktireceği tam olarak bu türden bir sızıntıdır

Kendinden imzalı bir sertifika üretim imzalaması için güvenli mi?

Hayır: kendinden imzalı bir sertifika, bir imzalama kod yolunu çalıştırmak için güvenlidir ve ekibin dışındaki birinin güvenmesi beklenen bir imza için güvensizdir, çünkü hiçbir şey onu bir güvenen tarafın yazılımının zaten güvendiği bir köke bağlamaz. Bunun gibi bir PFX için doğal bir sonraki adım, Delphi'de PDFlibPas ile bir uyumluluk ve imzalama çalışma tezgahı kurma makalesinde ele alınan gerçek bir imzalama çağrısıdır; burada bu şekilde kurulan bir PFX, aynı zamanda PDF/A ön kontrolü ve ByteRange denetimleri çalıştıran bir boru hattının imzalama yarısını yönetir. Ancak imzalama, bir sertifikanın etrafında oturan şeyin yalnızca yarısıdır ve diğer yarısı, kendinden imzalı bir yaprağın başarısız olması gereken tam olarak yerdir: Delphi'de PDFlibPas ile PAdES imzalama ve doğrulama makalesi, bir uyumluluk doğrulayıcısının çalıştırdığı güven-zinciri kontrollerini ele alır ve zinciri güvenilen bir köke geri dolaşan bir doğrulayıcının, bu fonksiyonun beş dakika önce hiçlikten icat ettiği bir sertifikaya güvenmesi için hiçbir nedeni yoktur

PLCreateSelfSignedCertificate, Delphi ve C++Builder için PDFlibPas PDF kütüphanesi'ndeki sertifika ve imzalama API'leri arasındaki bir fonksiyondur ve tam olarak burada açıklanan boşluk için vardır: arkasında gerçek bir anahtar çifti gereken ve birini üretmek için harici hiçbir şeye ihtiyaç duymayan bir imzalama testi