Teknik Makale

Delphi'de macOS Keychain Kimliğiyle PAdES İmzalama

PDFium VCL, PAdES belgelerini macOS Keychain'de tutulan bir özel anahtarla, her Security ve CoreFoundation sembolünü çalışma zamanında dlopen ile dlsym aracılığıyla çözümleyen bir backend üzerinden imzalar. Hiçbir şey link zamanında bağlanmaz; bu, yanlış yazılmış bir sembol adının linker hatası ya da çökme olarak değil, KeychainAvailable'ın False döndürmesi ve KeychainMissingSymbols'ın suçluyu adıyla bildirmesi olarak ortaya çıkarır

O seçim, rahatsız edici bir kısıt tarafından dayatıldı ve idare ediş biçimi geneller. Unit, macOS SDK'sı olmayan bir makinede yazıldı; her framework sembol adı ve her sabit belgelerden geldi ve hiçbiri bir header'a karşı denetlenemedi. O durumda yanlış cevap, kodu dikkatlice yazıp ummaktır. Doğru cevap, kaçınılmaz hataların kendilerini mümkün olan en kolay bulunur biçimde ilan etmelerini ayarlamaktır

Dinamik bağlamanın hedef platformda bile doğru karar olması

Çünkü programı durduran bir hata sınıfını, kendini bildiren bir hata sınıfına çevirir. Yanlış bir statik bağlanmış framework referansı, hedefte link zamanında kalır ve başka hiçbir yerde bağlanmaz. Yanlış bir dinamik bağlanmış olan ise kullanılamayan bir backend ile çözümlenmemiş adlar listesi üretir ve Mac'teki ilk çalıştırma, soruyu bu neden kullanılamıyor durumundan bir yazım hatasını adlandıran tek satıra indirger

Bir kez değil her gün karşılığını ödeyen ikinci bir kazanç daha var. Unit hiçbir framework bağlamadığı için her platformda derlenir; sıradan Windows derlemesi böylece sözdizimini, türlerini ve uses cümlesini denetlemeye devam eder. Yalnızca ekipten kimsenin sahip olmadığı bir platformda derlenen unit, ona bakan hiçbir derleyicisi olmayan unit'tir ve paylaşılan her tür refactor'unda sessizce çürür

uses
  FPdfCrypto, FPdfCryptoMac;

var
  Options: TPadesSignerOptions;
begin
  if not KeychainAvailable then
    raise Exception.Create('Keychain backend unavailable, unresolved: ' +
      KeychainMissingSymbols);

  ConfigureKeychainSignerProvider;   // PAdES signer backend olarak kurulur
  ConfigureKeychainCmsVerifier;      // ve doğrulama backendi olarak

  Writeln('signer backend  : ', PadesCryptoBackendName);
  Writeln('verify backend  : ', PadesCmsVerificationBackendName);

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1, harf durumu fark etmez
  Options.PaddingScheme := psRsaPss;
end;

İki tür export sembolü, onları okumanın iki yolu

Bu, bütün bağlamadaki tek en kafa karıştırıcı ayrıntıdır ve tersini kurmak temiz derlenir, çalışma zamanında kalır. CoreFoundation ile Security, aynı dlsym çağrısı üzerinden kategorik olarak farklı iki şey export eder ve kodun hangisinin ne olduğunu bilmesi gerekir

Keychain öğe sınıfı anahtarları ile CoreFoundation boolean singletonları gibi adlandırılmış sabitler, içerikleri istediğiniz CFStringRef ya da CFBooleanRef olan export edilmiş değişkenlerdir. dlsym o değişkenin adresini döndürür; değeri elde etmek için bir kez dereference etmelisiniz. Dictionary key ile value callback'leri gibi callback tablosu yapıları ise export edilmiş yapılardır ve dlsym yapının adresini döndürür; dictionary oluşturma işlevinin beklediği pointer da tam olarak budur. Onu dereference ederseniz yapının ilk makine kelimesini bir pointer varmış gibi geçirirsiniz

Hataların hiçbiri derleme hatası üretmez, hiçbiri net bir çalışma zamanı hatası da üretmez. Elinize, aşağı akışta bir yerde kalacak çöp bir pointer geçer. Ayrımı yanlış yapmayı imkansız kılmanın yolu, hatırlamaya güvenmeyi bırakmaktır: biri bağlayıp dereference eden, öteki bağlayıp etmeyen iki yardımcı işlev; çağrı noktası hangi tür sembolü istediğini bildirir, yardımcı gerisini dayatır

PDFium VCL macOS Keychain backendinin Security ve CoreFoundation sembollerini dlsym üzerinden çözümleme şeması: kSecClass, BindConstantın CFStringRef değerini elde etmek için bir kez dereference ettiği export değişkendir; kCFTypeDictionaryKeyCallBacks ise BindStructın adrese göre geçirdiği export yapıdır ve iki kuralın karıştırılması aşağı akışta çöp pointerlar verir
Tek dlsym çağrısı, kategorik olarak farklı iki şey döndürür: CFTypeRef tutan bir değişkenin adresi ile bir callback yapısının adresi. İki yardımcı, dereference edilip edilmeme kararını bellekte değil bağlama noktasında verir
// Export değişkeni: dlsym, CFTypeRef'i tutan değişkenin adresini verir;
// bir kez dereference edin
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Export yapı: dlsym, yapının KENDİSİNİN adresini verir; API'nin
// istediği de budur. Dereference etmeyin
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Bir RSA-PSS imzası neden iki ayrı yedeğe ihtiyaç duyar?

Çünkü algoritma iki bağımsız şekilde yok olabilir ve bunlardan yalnızca biri sürüm sorusudur. PSS digest-signing algoritma sabiti macOS 10.13'te belirdi; daha eski bir sistemde sembol sadece orada değildir ve bağlama nil alır. Bu sürüm kontrolüdür. Ayrı olarak, sabitin var olduğu bir sistemde belirli bir anahtar yine de onu reddedebilir ve framework o soruyu o anahtar için SecKeyIsAlgorithmSupported üzerinden cevaplar. Donanımla desteklenen bir anahtar ya da kısıtlayıcı nitelikli bir anahtar, aynı makinedeki bir yazılım anahtarı PSS'i kabul ederken onu reddedebilir

Her iki yol da aynı yedeğe varmak zorundadır: PKCS#1 v1.5'e geç. Ve kritik kısım şudur: yedek, yalnızca imzalama çağrısını değil CMS yapısına yazılan algoritma tanımlayıcısını da değiştirmek zorundadır. Gerçekte v1.5 imza üretirken PSS algoritma tanımlayıcısı yazmak, her doğrulayıcının haklı olarak reddettiği bir belge verir; bu, PSS desteklenmiyor bildirmekten kat kat kötüdür. Bir düşüş kabul edilebilir; bildirdiğiniz ile yaptığınız arasındaki uyuşmazlık değildir; bu macOS cilvesi değil, imza kodu için genel bir kuraldır. İmza düzeyindeki sonuçlar PAdES B-B ile PDF imzalama makalesinde serimlenir

PDFium VCL Keychain backendinde RSA-PSS imzalamanın neden iki bağımsız yedeğe ihtiyaç duyduğunu gösteren karar zinciri: dlsym, 10.13 öncesi macOS sürümlerinde digest-signing sabiti için nil döndürür, SecKeyIsAlgorithmSupported donanım destekli bir anahtarı reddedebilir ve her iki kapı da CMS algoritma tanımlayıcısının da değişmek zorunda olduğu aynı PKCS#1 v1.5 düşüşüne akar
PSS iki kez yok olabilir: macOS sürümü başına bir ve anahtar başına bir; sürüm kapısının sorusu sistemle ilgilidir, yalnızca onunki. İki kapı da aynı v1.5 düşüşüne akar ve CMS tanımlayıcısı onu izler

ECDSA imza kodlaması ve kayda değer bir terslik

Eliptik eğri yolu macOS'ta hiçbir dönüşüm gerektirmez ve bu, bir PKCS#11 bağlamasının gerektirdiğinin tersidir. Security framework'ünün ECDSA için digest-signing algoritması imzayı zaten X9.62 DER biçiminde döndürür; CMS'in istediği de tam olarak budur. Bir PKCS#11 token ise ham sabit genişlikte P1363 çiftini döndürür; bir imza yapısına girmeden önce yeniden kodlanması gerekir

Aynı interface'i uygulayan iki backend, aynı algoritma için zıt muamele ister ve hiçbiri yanlış değildir. Bir soyutlamanın sergilemek yerine yutmak zorunda olduğu fark tam olarak budur: PAdES katmanı bir provider'dan imzalamasını ister ve kodlama konvansiyonları provider'ın içinde kalır. Yukarı sızarlarsa her çağıran, backend başına koşullu taşımakla biter. Aynı şekil, HSM'ye karşı uzak PAdES imzalama oturumları makalesinde anlatılan uzak imzalama hikayesinde de görünür

PDFium VCL PAdES imzalayıcının iki backendi arasındaki ECDSA imza kodlaması karşılaştırması: macOS Keychain Security frameworkü, CMSin dönüşümsüz kabul ettiği X9.62 DERi döndürür; bir PKCS#11 token ise yeniden kodlanması gereken ham sabit genişlikte P1363 çiftini döndürür, böylece ResolvePadesSigner kodlama konvansiyonlarını provider içinde tutar
Aynı ECDSA interface'i backend başına zıt muamele ister: Security bitmiş DER'i teslim ederken bir PKCS#11 token ham P1363 çiftini verir; dönüşüm provider'ın içinde yaşar ve çağıranlar hiçbir zaman backend başına koşullu görmez
// Provider interface'i her platformda aynıdır; seçim çağrı başına değil
// başlangıçta verilen bir karardır
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // Windows CNG provider'ı platform unit'i tarafından kurulur
{$ENDIF}

if not PadesCryptoAvailable then
  raise Exception.Create('no signing backend on this platform');

// Buradan itibaren imzalama kodu platform-nötrdür
Signer := ResolvePadesSigner(Options);

Üç satır arayla duran referans sayımı kuralları

Core Foundation bellek yönetimi adlandırma konvansiyonlarını izler ve buradaki tuzak, farklı konvansiyonlu işlevlerin aynı kısa blokta yan yana görünmesidir. Bir trust nesnesinden sertifika alan (get) işlev, serbest bırakılmaması gereken ödünç bir referans döndürür. İmzalayan sertifikasını kopyalayan (copy) ya da verisini kopyalayan işlevler ise serbest bırakılması gereken sahipli referanslar döndürür. Art arda üç çağrı, iki sahiplik kuralı ve ödünç olanı serbest bırakmak o satırda başarısız olmaz; bir retain sayısını bozar ve alakasız bir şeyi daha sonra yok eder

Panzehir, temizlik kodunu yazmadan önce her seferinde, istisnasız her framework işlev adındaki fiili okumaktır. Bu, bir API'nin kopya mı görünüm mü döndürdüğünü kontrol etmenin CoreFoundation karşılığıdır ve yanılmak hataya değil aralıklı bir çökmeye mal olur

Bu backend'in iddia etmedikleri

Yazıldığı anda macOS'ta hiç çalışmadı ve bunu açıkça söylemek, ima yollu bir güvenceden daha faydalıdır. Kanıtlanabilir olan daha dardır ve hâlâ değerlidir: unit, günlük derlemenin parçası olarak Windows'ta derlenir; her framework sembolü, başarısızlıklar sayılarak çalışma zamanında ada göre bağlanır; iki PSS yedeği dahil algoritma seçim mantığı, incelenebilir ve üzerinde akıl yürütülebilir sıradan Pascal'dır. Mac'teki ilk çalıştırma ya işler ya da düzeltilecek adlar listesi üretir

CMS yapısını elle kurmak yerine daha yüksek düzeyli CMS çözücüsünü kullanan doğrulama tarafı, macOS'ta SecTrust ile PDF imzalarını doğrulama makalesinde ele alınır ve aynı bağlama altyapısını, aynı teşhis yaklaşımını paylaşır

Buradan taşınabilir fikir, macOS'tan çok risk yerleşimiyle ilgilidir. Doğrulayamadığınız bir interface'e karşı kod yazmak zorunda kaldığınızda, hataların en ucuz bulunabildiği kurguyu seçin. Çözümlenmemiş adların açık bir listesiyle dinamik bağlama, doğrulanamayan yirmi varsayımı tek bir teşhis satırına çevirir. İki backend de PDFium Delphi bileşeni ile kaynak olarak gelir; bir sembol adının düzeltilmesi gerekirse bu, destek talebi değil kendi ağacınızda tek satırlık bir değişikliktir