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
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ür | Anlam | Tipik kaynak |
|---|---|---|
| 0 | Yalnızca housekeeping: okuyucunun gördüğü hiçbir şeyi değiştirmeyen nesneler | Cross-reference stream, yeni bir object stream kabı |
| 1 | Doğrulama malzemesi | Document security store, VRI, sertifikalar, CRL'ler, OCSP yanıtları |
| 2 | Belge zaman damgası | Bir /DocTimeStamp imzası ve onu taşıyan alan |
| 3 | İmza | Değerini alan mevcut bir signature field |
| 4 | Form alanı | Bir alan değeri ya da onun appearance'ı |
| 5 | Annotation | Widget dışındaki herhangi bir annotation |
| 6 | Belge içeriği | Belgenin gösterdiklerini değiştirebilen her şey |
| 7 | Belirlenemedi | Revision'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:
- 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
/Rootalanı 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
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