Teknik Makale

PDF Revision Sınırı: İmzadan Sonra Sahte Güncellemeler

Bir sözleşme imzalanır, ardından bir güncelleme daha alır; denetim scripti de o güncellemeyi imzalı revision'a katıverir ya da yeniden yazılan sayfayı validation material diye geçiştirir. Bir revision, startxref'in /Prev zincirinin ulaştığı bir section'ı adlandırdığı yerde biter ve yeniden yazılan bir nesne önceki sınıfını korur: PDFlibPas revision haritasının arkasındaki iki kural bunlardır ve ikisi geçerliyken bu durumların hiçbiri yaşanmaz

%%EOF'un dosyayı kesmek için yanlış yer olması

Her incremental update kendi end-of-file işaretiyle son bulur (ISO 32000-1 §7.5.6); bu yüzden dosyayı her %%EOF üzerinden bölmek, revisionları bulmanın bariz yolu gibi görünür. Ama işaret beş bayttan ibarettir ve o baytların bir content stream'in, bir gömülü dosyanın ya da bir font programının içinde bitivermesine hiçbir şey engel olmaz. Linearize edilmiş dosyalar ikinci bir sorun ekler: Annex F (§F.3.4), ilk sayfa trailer'ının kendi startxref'iyle ve %%EOF işaretiyle, ana cross-reference tablosundan çok önce, dosyanın başına yakın bir yerde son bulmasına izin verir. Her işaretten bölmek, hiçbir cross-reference section'ın arkasına yaslanmayan revisionlar icat eder. Hangi işaretlerin sahte olduğunu tahmin etmekse daha kötüdür — o tahmin, uydurmacının biçim verdiği şeydir. PDFlibPas revision haritası tam tersini yapar. /Prev zincirini izler, zincirin gerçekten okuduğu her cross-reference section'ın bayt ofsetini kaydeder, ancak ondan sonra startxref N … %%EOF dizilimlerini tarar ve N, kaydedilen o ofsetlerden biriyse sınırı tutar. Bir stream'in içinde kaybolmuş bir işaretin bir şey sayılması için önünde, gerçek bir section'ı adlandıran bir startxref bulunması gerekir

Delphi'de PDFlibPas revision haritası: /Prev zincirini izleyerek ve startxref ofsetlerini eşleyerek incremental update revision sınırlarını buluyor; stream'lerin içindeki ve linearize ilk sayfa trailer'larındaki başıboş %%EOF işaretlerini yok sayıyor
Revision sonunu yalnızca /Prev zincirinin gerçekten okuduğu startxref ofsetleri işaretler; böylece stream'lerin içindeki başıboş %%EOF baytları, PDFlibPas'ın dosyayı nereden keseceğine söz geçiremez

O üç token arasındaki baytlar, §7.5.5'in ima ettiğinden daha dağınıktır. Standart, startxref'i, ofseti ve işareti ardışık satırlara çizer; oysa §7.2.2'nin (Table 1) satır sonu işaretleri ile öteki white-space karakterleri tekrar edebilir ve kaç kez edebileceğine dair bir üst sınır yoktur. Dolgulu tek bir işareti kaçırırsanız iki revision birleşir; imzalı olan, artık kimsenin imzalamadığı baytlardan sonra bitiyormuş gibi görünür. PDFlibPas revision haritası, aralarında ne kadar white-space olursa olsun ve o dolgu dosyanın neresine düşerse düşsün, keyword'den operand'a oradan marker'a uzanan dizilimi izler ve her keyword'ü bir kez sayar. Trailer keyword'ünün de benzer bir gevşek ek yeri vardır: << bir delimiter'dır (§7.2.2, Table 2) ve §7.5.5, trailer sonrasında satır sonu istemez; bu yüzden tek satırdaki trailer<< kurallara uyan bir sözdizimidir. Satır satır okuyan biri o satırı bir cross-reference alt bölüm başlığı sanır — ve zincir yolda yarım kalır

Revision haritasını PDFlibPas ile gezmek

Revision erişimcileri TPDFlib üzerinde tanımlıdır ve Small Business sürümünde uygulanır: bir TPDFlibSmallBusiness'i PDFlibSmallBusiness unit'inden oluşturun (Enterprise ondan türer ve aynı çağrıları taşır). Sıradan bir TPDFlib üzerinde bunlar başarısızlık değerlerini döndürür (0, 7 ya da -1) ve LastErrorCode değerini PDFLIB_ERROR_SMALLBUSINESS_REQUIRED yapar. Revisionlar, özgün belge olan 0'dan itibaren numaralanır. Her çağrı bir dosya adı ve bir şifre alır ve haritayı diskteki baytlardan kurar; dolayısıyla yanıtlar, dosyanın saklandığı haliyle durumunu anlatır, sizin o sırada yükleyip düzenlemiş olduğunuz belgeyi değil. GetDocumentRevisionKind bir revision'daki en kapsamlı değişikliği raporlar. Bir OCSP yanıtı ekleyip aynı zamanda bir sayfayı yeniden yazan güncelleme, sayfa yeniden yazımı olarak raporlanır:

TürAnlamTipik kaynak
0Yalnızca housekeeping: okuyucunun gördüğü hiçbir şeyi değiştirmeyen nesnelerCross-reference stream, yeni bir object stream kabı
1Doğrulama malzemesiDocument security store, VRI, sertifikalar, CRL'ler, OCSP yanıtları
2Belge zaman damgasıBir /DocTimeStamp imzası ve onu taşıyan alan
3İmzaDeğerini alan mevcut bir signature field
4Form alanıBir alan değeri ya da onun appearance'ı
5AnnotationWidget dışındaki herhangi bir annotation
6Belge içeriğiBelgenin gösterdiklerini değiştirebilen her şey
7BelirlenemediRevision'da bir şey okunamadı; güvensiz sayın
uses
  System.SysUtils, PDFlibSmallBusiness;

procedure ListRevisions(const FileName, Password: WideString);
const
  KindNames: array[0..7] of string = ('housekeeping', 'validation material',
    'document time stamp', 'signature', 'form field', 'annotation',
    'content', 'undetermined');
var
  Pdf: TPDFlibSmallBusiness;
  Count, I, J, Kind, Changes: Integer;
begin
  Pdf := TPDFlibSmallBusiness.Create;
  try
    Count := Pdf.GetDocumentRevisionCount(FileName, Password);
    if Count = 0 then
      raise Exception.Create('the cross-reference chain could not be followed');
    if Pdf.GetDocumentRevisionsComplete(FileName, Password) <> 1 then
      Writeln('WARNING: history incomplete, every figure below is a lower bound');
    for I := 0 to Count - 1 do
    begin
      Kind := Pdf.GetDocumentRevisionKind(FileName, Password, I);
      if (Kind < 0) or (Kind > 7) then
        Kind := 7;
      Writeln(Format('revision %d: bytes %d..%d, %s', [I,
        Pdf.GetDocumentRevisionStart(FileName, Password, I),
        Pdf.GetDocumentRevisionEnd(FileName, Password, I),
        KindNames[Kind]]));
      // nesne başına bir 'objnum,change,class,detail' satırı
      Changes := Pdf.GetDocumentRevisionChanges(FileName, Password, I);
      if Changes <> 0 then
      try
        for J := 1 to Pdf.GetStringListCount(Changes) do  // ögeler 1'den itibaren sayılır
          Writeln('    ' + Pdf.GetStringListItem(Changes, J));
      finally
        Pdf.ReleaseStringList(Changes);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

GetDocumentRevisionsComplete, ötekilerden herhangi birine güvenmeden önce okunması gereken çağrıdır. 1'i yalnızca zincir, en baştaki section'a kadar geriye kadar izlenebildiğinde döndürür; kırık ya da döngüye girmiş bir /Prev ya da 256 bağlantıdan derin bir zincir, en eski güncellemeleri görünmez bırakır ve o zaman her sayı ve indeks bir alt sınır olur. 256 derinlik, gerçek dosyaların kaydet-ve-imzala geçmişine yer bırakır ve kendini geriye işaret eden bir zincir de neredeyse anında sona erer. Döngüdeki indekslemeye dikkat edin. GetStringListItem 1'den sayarken revision'lar 0'dan sayar; iç döngüyü 0'dan başlatmak, her revision'ın ilk değişen nesnesini hiçbir hata vermeden düşürür

Sahte bir güncelleme kendini validation material olarak nasıl gösterir

PAdES uzun vadeli doğrulama, son imzadan sonra güncellemeleri meşru biçimde ekler: bir document security store (/Type /DSS), imza başına /VRI sözlükleri, sertifika ve iptal stream'leri, belge zaman damgaları. Certification seviyeleri bunlara geçer izni verir; bu da validation material'i, bir güncellemenin taşıyabileceği en değerli etiket yapar. Sahtesi de en ucuz olandır. /Type, güncellemenin yazmayı seçtiği her neyse odur. Sayfa çizen bir viewer hiçbir zaman bir content stream'in, bir resource sözlüğünün ya da bir fontun /Type alanına bakmaz; bu yüzden bir güncelleme 3. sayfanın content stream'ini yeniden yazıp sözlüğüne /Type /DSS ekleyebilir ve /Type'a inanan bir sınıflandırıcı housekeeping görürken her renderer yeni metni boyamaya devam eder. /VRI, /Sig ve /DocTimeStamp da aynı şekilde çalışır

PDFlibPas revision haritası, bir güncellemenin yeniden yazdığı bir nesnenin üzerine yazdığı etiketi yok sayar. Yeniden yazılmış bir nesne, yalnızca aynı nesnenin önceki sürümü zaten validation material, bir zaman damgası ya da bir imzaysa, o sınıftan sayılır. Sertifika, CRL ve OCSP yanıt stream'leri kendi türlerini taşımadığı için onları, işaret eden document security store üzerinden tanır — ve yalnızca yargılanmakta olan revision'da ya da daha eskisinde yazılmış bir store sayılır. Sonradan eklenen bir store, geriye uzanıp imzalı revision'ın yazdığı bir nesneye kefil olamaz. Belgenin yapısına dokunan değişiklikler, okuyucunun çözeceği şeyi değiştirdikleri anda content sayılır:

PDFlibPas GetDocumentRevisionKind, bir sayfa content stream'ini yeniden yazan sahte /Type /DSS güncellemesini belge içeriği olarak raporluyor; çünkü revision haritası yeniden yazılmış bir nesneyi önceki sürümüne göre sınıflandırıyor
Yeniden yazılmış bir sayfa content stream'ini /Type /DSS ile etiketlemek renderer için hiçbir şeyi değiştirmez; PDFlibPas güncellemeyi yine de belge içeriği olarak raporlar, çünkü o nesne daha önce sayfa içeriğiydi
  • mevcut bir object stream'in yerine yenisini koymak; çünkü içindeki her sıkıştırılmış nesne artık yenisiyle çözülür (yepyeni bir kabın housekeeping kalması bundandır: her üye kendi girdisini alır ve orada yargılanır)
  • bir catalog ya da bir sayfa eklemek
  • /Root alanı başka bir catalog adlandıran bir trailer; imzanın kapsadığı hiçbir şeyi yeniden tanımlamasa bile, çünkü root değişimi tek bir imzalı nesneye dokunmadan bütün belgeyi değiştirir
  • sözlüğü eskisiyle bayt bayt aynı olan yeniden yazılmış bir stream: dolgulanıp aynı boyuta getirilmiş bir gövde ya da indirekt bir /Length, çizim değişirken sözlüğü değişmemiş tutar; bu yüzden bu kanıt üzerinde yalnızca stream gövdesi olmayan nesneler değişmemiş sayılır

Object stream üyelerinin soyulacak bir başlığı yoktur

PDF 1.5'ten itibaren bir güncelleme, yeniden yazdığı nesneleri bir object stream'in içinde saklayabilir ve §7.5.7 format konusunda nettir: yalnızca nesne değerleri saklanır, obj ve endobj keyword'leri kullanılmamalıdır. Bir nesneyi bulunduğu yerden okuyup başlığı, obj string'ini arayarak silen kod, bu yüzden bir üyenin önünde bir şey bulamaz ve aramaya gövdesinin içinden sürdürür. İmzadan sonra yeniden yazılmış bir sayfa sözlüğüne (obj<</Type/DSS>>) gibi masum görünümlü bir string verin; o arama string'in içinde durur. Çözücüye <</Type/DSS>> ulaşır — revision'ın hiç yazmadığı bir store sözlüğü. PDFlibPas revision haritası, sıkıştırılmamış bir nesnenin önünden N G obj dizilimini parse eder, sayıların beklediği nesneyi adlandırdığını doğrular ve eksik ya da uymayan bir başlığı daha ileri aramak yerine okunamaz sayar

Cross-reference stream'lerin kendi alan düzeyinde belirsizlikleri vardır. §7.5.8.3 (Table 18) uyarınca yalnızca type 1 girdi bir bayt ofseti taşır. Type 2 girdinin ikinci alanı onu barındıran object stream'in nesne numarası, üçüncüsü stream'in içindeki indekstir; type 0 girdinin ikinci alanı ise bir sonraki boş nesne numarasıdır. O nesne numarasını ofset sanırsanız, sıkıştırılmış bir girdi o küçük bayt konumlarını kapsayan hangi revision varsa oraya düşer — neredeyse her zaman özgün belgeye; böylece yeniden yazım imzalı baytların içine gömülür. Revision haritası her girdiyi, onu bildiren cross-reference section'a yerleştirir. Aynı madde, 0, 1 ya da 2 dışındaki bir girdi türünün null nesneye referans olduğunu söyler; dolayısıyla bu girdiler bir şey eklemez, boşaltmaz ve haritanın dışında kalır

Bir imza denetimi ne zaman cevap vermeyi reddetmeli?

Dosya kendi geçmişinin bir kısmını saklıyorsa. Revision haritası bunu üç şekilde bildirir: GetDocumentRevisionsComplete 0 döndürür, GetDocumentRevisionKind imzadan sonraki bir revision için 7 döndürür ya da GetSignatureRevisionIndex -1 döndürür. Son değer, bir revision sınırında bitmeyen bir ByteRange'i kapsar — imzanın kapladığını iddia ettiği aralığın içine baytlar yerleşmiştir — aynı derecede var olmayan, imza değeri taşımayan ya da okunamayan bir dosyada duran bir alanı da kapsar. Üçünü de, revision indeksi ile GetDocumentRevisionChanges satırları ekli olarak elle incelemeye gönderin; dosyayı açan kişi nesne numaralarından yola çıksın

// Bir imzadan sonraki güncellemeleri DocMDP tarzı bir politikaya karşı denetler.
// Türler şiddet sırasına göre dizilidir; her politika seviyesi bir tavan demektir:
// P 1 housekeeping, validation material ve zaman damgalarını (0..2) tolere eder,
// P 2 imzalama ile form doldurmayı (..4), P 3 annotation'ları (..5) ekler.
function UpdatesWithinPolicy(Pdf: TPDFlibSmallBusiness;
  const FileName, Password, FieldName: WideString; Level: Integer): Boolean;
const
  Ceiling: array[1..3] of Integer = (2, 4, 5);
var
  Signed, Count, I, Kind: Integer;
begin
  Result := False;
  if (Level < 1) or (Level > 3) then
    Exit;
  if Pdf.GetDocumentRevisionsComplete(FileName, Password) <> 1 then
    Exit;                        // geçmişin bir kısmı hiç görünmedi
  Signed := Pdf.GetSignatureRevisionIndex(FileName, Password, FieldName);
  if Signed < 0 then
    Exit;                        // revision sınırı dışında, o adda imzalı
                                 // bir alan yok ya da dosya okunamadı
  Count := Pdf.GetDocumentRevisionCount(FileName, Password);
  for I := Signed + 1 to Count - 1 do
  begin
    Kind := Pdf.GetDocumentRevisionKind(FileName, Password, I);
    if (Kind = 7) or (Kind > Ceiling[Level]) then
      Exit;                      // 7 her tavanın üstündedir; okuyucu için yazılmıştır
  end;
  Result := True;
end;

Tavanlar ISO 32000-2 §12.8.2.2'yi (Table 257) izler: P 1 hiçbir değişikliğe izin vermez, 2 form doldurma ile imzalamaya izin verir, 3 annotation'ları ekler ve yalnızca DSS verisi ya da belge zaman damgası ekleyen incremental update'ler hiçbir seviyede değişiklik sayılmaz. ISO 32000-1 §12.8.2.2 aynı üç değeri o muafiyet olmadan tanımlar. Politika sertifikayı yazanın değil sizin olduğunda döngüyü kendiniz yazın; mesela dosyanın hangi P'yi taşıdığına bakmadan, annotation eklenmiş sözleşmeleri reddeden bir kabul kuralı gibi. Politika dosyadan gelmeliyorsa, aynı sınıftaki GetSignatureModificationVerdict certification seviyesini okur, FieldMDP kilitli form alanlarındaki düzenlemeleri yakalamak yazısında anlatılan alan bazlı kilitleri uygular ve sınır dışı durum için 5 döndürür. İkisi de, sakladığınız belge kopyasına karşı değişiklikleri sınıflandırmak yazısındaki soruya cevap verenden farklı bir soruya cevap verir: revision haritası, önündeki dosyadan başka hiçbir şeye ihtiyaç duymaz

Delphi'de PDFlibPas DocMDP tarzı imza denetimi: P 1, P 2 ve P 3 için GetDocumentRevisionKind tavanları kullanılıyor; önce GetDocumentRevisionsComplete ile GetSignatureRevisionIndex denetleniyor
Her DocMDP seviyesi revision türü üzerinde bir tavana dönüşür: P 1, 0 ile 2 arasındaki türleri tolere eder; P 2 dörde, P 3 beşe kadar kabul eder ve belirlenemeyen bir revision her seviyede düşer

Ekleme de aynı zinciri izler. Bir incremental update'in trailer'ı, önceki cross-reference section'ın konumunu veren bir /Prev girdisi taşımak zorundadır (§7.5.6); tam tarama ile cross-reference tablosunu yeniden kurmak yoluyla açılabilen bir belgenin verecek böyle bir konumu yoktur. AppendToFile bunun için 0 döndürür ve hedef dosyaya dokunmaz. Onu tam kaydedin ve temiz kopyayı imzalayın — tam yeniden yazım, mevcut imzaları devralmaz

İmzalı sözleşmelerden oluşan bir arşiv için, yeni bir şey eklenmeden önce her signature field için bir kez UpdatesWithinPolicy koşturun. Sonraki revision'ı tür 7 okunan bir dosya önce bir insana ulaşmalıdır; onu bir başka güncellemenin altına gömecek PDFlibPas imzalama ya da uzun vadeli doğrulama adımının önünde