Teknik Makale

Doğrulayıcılar PAdES İmzalarını Neden Reddeder: Delphi'de PDFium

Delphi'de PDFium Bileşeni ile ayıkladığımız neredeyse her durumda, bir doğrulayıcı bir PAdES imzasını şu üç nedenden biri yüzünden reddeder: /ByteRange dizisi hala sıfır yer tutucularını tutuyordur, /M imzalama süresi düzgün biçimlendirilmiş bir PDF tarih dizesi değildir veya artımlı bir güncelleme (incremental update) şifrelenmiş bir belgeden /Encrypt girişini düşürmüştür. Her üçü de sorunsuz açılan, sorunsuz işlenen ve kurallara uygun bir doğrulayıcı imza sözlüğünü okuduğu anda başarısız olan dosyalar üretir

Bu makaleyi motive eden senaryo acı verici derecede spesifiktir. PAdES B-B imzalama makalesindeki iş akışıyla bir sözleşme imzalarsınız, kendi denetim kodunuz imzanın mevcut ve yapısal olarak sağlam olduğunu bildirir, her yerel test yeşildir — ve ardından karşı taraf dosyayı kendi doğrulama platformuna yükler ve kırmızı bir çarpı alır. Hatanın hiçbir kısmı bir görüntüleyicide görünmez, çünkü bu üç kusurdan hiçbiri sayfa içeriğine dokunmaz. Tamamen, doğrulayıcıların baktığı ve görüntüleyicilerin çoğunlukla bakmadığı imza sözlüğünde ve dosya fragmanında (trailer) yaşarlar

PDF imzamın ByteRange değeri neden geçersiz?

Bir ByteRange reddi, neredeyse her zaman aralıkların ince bir şekilde yanlış olduğu anlamına değil, dört alanın hiçbir zaman doldurulmadığı anlamına gelir. /ByteRange [A B C D] dizisi iki aralık bildirir, A'dan A+B baytlarına kadar ve C'den C+D baytlarına kadar; ve EN 319 142-1 §6.3 (k gereksinimi), birlikte hex kodlu /Contents dizesi hariç tüm dosyayı kapsamasını talep eder. Sayılarla: A sıfırdır, C >= A+B'dir, C+D dosya uzunluğuna eşittir ve B ile C arasındaki boşluk tam olarak <...> hex dizesini tutar — iki parantez baytı artı CMS verisinin baytı başına iki hex karakteri. [0 0 0 0] okuyan bir doğrulayıcı, /Contents içindeki CMS yapısının ne kadar doğru olduğuna bakılmaksızın imzanın hiçbir şeyi kapsamadığı sonucuna varır ve reddeder

Bunun nasıl gerçekleştiğinin mekaniğini anlamak önemlidir çünkü aynı kalıp her imzalama yığınında (signing stack) mevcuttur. Bir imzalayıcı, dosya düzenlenene kadar nihai ofsetleri bilemez, bu nedenle yazar, diziyi sabit genişlikli sıfır yer tutucularıyla yayar ve düzenden sonra bunları geri doldurur. PDFium Bileşeni'nin 2.14.1'den önceki sürümlerinde, bu geri doldurma işlemi /Contents konumundan başlayarak yer tutucu kalıbını arıyordu — ancak /ByteRange sözlükte /Contents'ten önce yer alır, bu nedenle arama hiçbir şey bulamadı ve her üç değiştirme de sessizce başarısız oldu. CMS özeti (digest) doğru aralıklar üzerinden hesaplandığı için kriptografi sağlamdı; ancak bu aralıkların beyanı sıfırda kaldı, bu yüzden her kurala uygun doğrulayıcı dosyayı reddetti. Aynı sözlük düzenini kullanan PAdES B-LTA Belge Zaman Damgaları da aynı şekilde başarısız oldu — eğer uzun vadeli B-LT ve B-LTA imzaları üzerine inşa ediyorsanız bu önemlidir. Sürüm 2.14.1 imza nesnesi başlangıcından itibaren geri doldurur ve düzeltme, üretilen dosyayı ayrıştıran ve aşağıdaki aritmetiği doğrulayan bir regresyon testiyle kapsanmaktadır

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // yalnızca ilk imza
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: aralıklar /Contents hariç her şeyi kapsar
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

Yirmi satırlık düz RTL kodu, kütüphane çağrısı yok ve tüm başarısızlık sınıfını üretim noktasında yakalar. Bu makaleden tek bir savı (assertion) aklınızda tutacaksanız, bunu tutun: Kendi imzalı çıktınızı ayrıştırın ve dosya sürecinizden ayrılmadan önce üç eşitliği kontrol edin

Doğrulayıcılar /M imzalama zamanını neden hatalı biçimlendirilmiş olarak işaretler?

Katı doğrulayıcılar, tam bir PDF tarih dizesi olmayan bir imzalama zamanını reddeder ve en sık eksik olan iki parça, D: öneki ve UTC ofset işaretçisidir. ISO 32000-1 §7.9.4, tarih biçimini D:YYYYMMDDHHmmSS ve ardından gelen bir ofset — UTC için Z veya onunla imzalı +HH'mm' ilişkisi — olarak tanımlar. Yalın bir 20260709143000, esnek bir okuyucuya bir tarih olarak ayrışır, ancak dilbilgisini kelimenin tam anlamıyla uygulayan bir doğrulayıcı, zorunlu biçim alanında hatalı biçimlendirilmiş bir dize görür ve imzayı işaretler. PDFium Bileşeni'nin 2.14.4'ten önceki sürümleri, TPdf.SignPades and SignPadesBytes'ın /M girişini tam olarak bu yalın biçimde yazıyordu; 2.14.4'ten beri giriş D: önekini ve Z işaretçisini taşır, böylece beyan edilen imzalama süresi D:20260709143000Z olarak okunur

Bu alana iki pratik not eklenmiştir. İlk olarak, UTC yazın ve öyle olduğunu belirtin: Bir ofset işaretçisi olmayan zaman damgası, doğrulayıcıyı saat dilimi ilişkisini tahmin etmeye zorlar ve §7.9.4, ofseti isteğe bağlı bir incelikten ziyade biçimin bir parçası olarak ele alır. İkinci olarak, /M'in ne olduğunu unutmayın — kanıttan ziyade bir iddia olan, imzalayan tarafından beyan edilen zaman. Bir doğrulayıcı doğruluğunu değil, buradaki biçimini kontrol eder; kanıtlanabilir zaman, PAdES B-T ve üzerinde bir RFC 3161 zaman damgasından gelir. Bir alanı doğru biçimlendirmek ve ona güvenmek ayrı kararlardır ve doğrulayıcılar yalnızca ilkini zorunlu kılar

SignPades neden şifreli bir PDF üzerinde EPadesCrypto hatası verir?

PDFium Bileşeni, şifrelenmiş bir belgeyi imzalamayı reddeder çünkü alternatif, kurallara uygun okuyucuların açılışta yok edeceği bir dosya üretmektir. Bir PAdES imzası artımlı bir güncelleme olarak eklenir ve ISO 32000-1 §7.5.6, bir güncelleme bölümünün yeni fragmanının (trailer), /Prev hariç önceki fragmanın her girişini taşımasını gerektirir — şifrelenmiş bir belgede bu /Encrypt'i içerir. Bunu düşürün; en son fragman dosyanın şifrelenmemiş olduğunu beyan eder, böylece kurallara uygun bir okuyucu şifrelenmiş gövdeyi düz metin olarak ayrıştırır ve çöp elde eder. Daha da kötüsü, imzalayanın eklediği akışlar ve dizeler yasal olabilmesi için belge anahtarıyla şifrelenmiş olmalıdır, bunu da düz metin imza enjektörü yapamaz. Şifrelenmiş bir dosyaya geçerli bir düz metin imzası eklemenin hiçbir yolu yoktur; bu nedenle sürüm 2.14.2'den beri TPdf.SignPades, SignPadesBytes ve InjectPadesDssMarkers, bozuk veya doğrulanamayan bir sonuç yaymak yerine EPadesCrypto hatası vermektedir

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // örn. 'SignPadesBytes: kaynak belge şifrelenmiştir;
    //       imzalamadan önce şifrelemeyi kaldırın'
    Writeln('İmzalanamıyor: ', E.Message);
  end;
end;

İstisna (exception) doğru sonuçtur, bu yüzden iş akışını yakalayıp yeniden denemek yerine bunun etrafında tasarlayın. Önce sahip (owner) yetkisiyle şifreyi çözün, düz metin kopyayı imzalayın ve dağıtım kanalı şifreleme gerektiriyorsa, şifrele-ve-imzala ile imzala-ve-şifrele'nin farklı doğrulama hikayelerine sahip farklı yapılar ürettiğini kabul edin. Düz metin baytları üzerinden hesaplanan bir imza, o baytların yeniden şifrelenmesinden sağ çıkamaz, bu nedenle dürüst seçenekler imzalı bir düz metin dosyası veya kodun yanında belgelenmiş bir politika kararıdır

İki hata birbirini nasıl onaylayabilir?

ByteRange hatası, kendi doğrulayıcımızın da tamamlayıcı bir şekilde yanlış olması nedeniyle gizlenebildiği kadar gizlendi ve bu, bu makaledeki en aktarılabilir derstir. ValidatePadesCompliance içindeki kapsam kontrolü, ikinci aralığın tam olarak A+B'de — sıfır boşlukta — başlamasını gerektiriyordu; bu da boşluğun /Contents hex dizesini tuttuğu standart düzeni yanlış sınıflandırır. Böylece şirket içi doğrulayıcı tam olarak kurallara uygun düzeni reddetti ve şirket içi oluşturucu asla bir tane üretmedi ve uçtan uca imzala-ve-doğrula boru hatları tesadüfen yeşil kaldı. Her bir hata, test paketini diğerini ortaya çıkaracak karşı örnekten mahrum bıraktı

Metodolojik düzeltme, yazmadığınız bir uygulamaya karşı çapraz doğrulamadır (cross-validation). Bir kod tabanını, bir yazarı veya hatta sadece formatın zihinsel bir modelini paylaşan oluşturucu ve doğrulayıcı, süresiz olarak ortak bir yanlış anlama üzerinde anlaşabilirler; bağımsız bir doğrulayıcı bu simetriyi bozar. İmzalı çıktınızı bir sürümden önce en az bir harici uygunluk denetleyicisinden geçirin ve hızlı ilk satır olarak yapı içinde yapısal bir kendi kendine kontrol bulundurun — imza inceleme makalesinde açıklanan TPdf.ValidatePades, kapsama hatasını adlandırılmış bir sorun olarak bildirir

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange dosyayı kapsamıyor');
  if not R.IsCompliant then
    Writeln('Yapısal sorunlar mevcut: Bu dosyayı sevk etmeyin');
end;

İmzalı çıktı için bir reddedilme kontrol listesi

Bir platform PAdES imzanızı reddettiğinde, sertifikalardan veya güven zincirlerinden şüphelenmeden önce ucuz yapısal nedenleri kontrol edin. /ByteRange dizisini doğrulayın: İlk alan sıfır, ikinci aralık birincinin bitiminde veya sonrasında başlıyor, ikinci aralık tam olarak dosya sonunda bitiyor. /M girişini doğrulayın: D: öneki ve bir ofset işaretçisi ile tam bir §7.9.4 tarih dizesidir. İmza eklendiğinde kaynak belgenin şifrelenmediğinden emin olun — ve yığınınız şikayet etmeden yine de imzaladıysa, bu sessizliği yığındaki bir hata olarak ele alın. Her üç kontrol de ham baytlar üzerinde milisaniyeler içinde çalışır ve deneyimlerimize göre reddedilmeyi herhangi bir kriptografik nedenden çok daha sık açıklarlar

Burada kullanılan imzalama, inceleme ve doğrulama çağrıları — düzeltilmiş ByteRange aritmetiği, tarih biçimlendirmesi ve şifreleme geçidi ile SignPades, ValidatePades ve PAdES uygunluk kontrolleri — Delphi, C++Builder ve Lazarus için PDFium Bileşeni ile birlikte gönderilir