Başka bir yerde imzalanmış AES ile şifrelenmiş bir sözleşme, şifre çözme sonucu rastgele baytlara düşen bir imza değeriyle ya da AES-GCM altında hiç şifresi çözülmeyen bir imza sözlüğüyle açılabilir. Trailer içindeki /ID ile encryption sözlüğünün dışında ISO 32000-2 §7.6.2 yalnızca imzanın /Contents alanını şifrelenmemiş bırakır; PDFlibPas bunu okumada da yazımda da açık tutarken /Reason ve /M şifreli kalır
ISO 32000-2'nin şifreleme kurallarına eklediği istisna
ISO 32000-2 §7.6.2, şifrelemenin dosyadaki her stringi ve streami kapsadığını söyler; dört istisna vardır:
- trailer içindeki
/IDgirdisinin değerleri /Encryptsözlüğünün içindeki stringler- streamlerin içindeki stringler; bunlar bir bütün olarak şifrelenir
- bir imza sözlüğünde
/Contentsdeğerini oluşturan herhangi bir hexadecimal string
İlk üçü eskidir. ISO 32000-1 §7.6.1 yalnızca o üçünü sayar; bu yüzden 2008 metnine göre yazılmış bir şifre çözücünün, bir imza sözlüğündeki hiçbir şeyi atlaması gerektiğine dair bir talimatı yoktur. İmzalama araçları değeri yine de açık yazmıştır. Gerekçe mekaniktir: imzalayan bir hex yer tutucusu ayırır, dosyanın tamamını yazar, yer tutucu dışındaki her şeyi hash eder, CMS'i inşa eder ve hexini boşluğa yamalar. O özet, diskte durdukları haliyle baytları kapsar; ciphertext dahil. İmza değerini şifrelemek hiçbir şeyi korumaz; üstelik yamalama adımının dosya anahtarını bilmesini ve IV ile padding'i, hash henüz var olmadan önce boyutlanmış bir yer tutucuya sığdırmasını zorunlu kılar. Sözlüğün geri kalanı olağan kuralları izler:
| İmza sözlüğü girdisi | PDF türü | Şifreli saklanır mı? |
|---|---|---|
/Contents | hexadecimal string | Hayır, ISO 32000-2 §7.6.2 uyarınca muaf |
/Reason, /Location, /ContactInfo, /Name | text string | Evet |
/M | date string | Evet |
/ByteRange | integer dizisi | Hayır, integerlar hiçbir zaman şifrelenmez |
/Type, /Filter, /SubFilter | name | Hayır, name'ler hiçbir zaman şifrelenmez |
İstisnayı gözden kaçıran bir okuyucu, şifreyi düz DER üzerinden çalıştırır. Ortaya çıkan şey algoritmaya bağlıdır. RC4 bir stream cipher'dır; çıktı uzunluğunu korur ve gürültüye dönüşür, CMS parser artık 0x30 olmayan bir ilk bayt görür ve bozuk bir imza bildirir. AES-CBC ilk 16 baytı initialization vector, gerisini ciphertext sayar; sonuç bir padding hatası ya da daha kısa bir çöp kümesidir. PDF için ISO/TS 32003 ile tanımlanan AES-GCM en acımasız durumdur. Her şifreli string kendi nonce'una ve authentication tag'ine sahiptir ve açık bir değer tag denetimini geçemez. Tek bir başarısız tag'i bütün sözlük için ölümcül sayan bir şifre çözücü, imza değeriyle birlikte /Reason ve /M'i de kaybeder
Bir imza /Contents alanını öteki tüm /Contents'lerden nasıl ayırt edersiniz?
Anahtar adıyla eşlemek bariz ilk hamledir ve iki yönde de başarısız olur. Annotation sözlüklerinin de bir /Contents girdisi vardır — gözden geçirenin yazdığı yorumu tutan bir text string'dir bu — ve o string şifreli olmak zorundadır. Atlanırsa gizli bir dosyadaki her yapışkan not şifresiz okunabilir hale gelir. /Type /Sig ile eşlemek ise öbür yönde kırılır. ISO 32000-2 Table 255, imza sözlüğünde /Type alanını isteğe bağlı kılar, varsayılanı Sig sayar ve yalnızca /DocTimeStamp için zorunlu tutar; dolayısıyla kurallara uygun bir üretici onu yazmayabilir. PDFlibPas bu yüzden bir /Contents string'ini, aynı sözlük bir /ByteRange dizisiyle birlikte ya da Sig veya DocTimeStamp değerinde bir /Type taşıdığında ya da /Type hiç yokken yanında bir /Filter adı bulunduğunda imza değeri sayar. /Filter her imza sözlüğünde, /ByteRange ise bir imza alanına ait her imzada zorunludur; bir annotation'da ikisi de yoktur. Kütüphane aynı testi kaydederken şifrelemek için de yüklerken şifre çözmek için de koşturur; böylece /Contents, yüklemenin ardından üreticinin yazdığı baytları tutar
Önce şifreleyin, sonra PDFlibPas ile imzalayın
İmzalamayı en sona bırakın. İmza özeti saklanan baytları kapsar; bu yüzden imzadan sonra dosyayı yeniden yazan her işlem, içindeki her imzayı kırar. İmzalı bir belgeye şifreleme uygulamak her string'i ve stream'i yeniden şifreler; şifrelemeyi kaldırmak da onları aynı derecede kapsamlı biçimde yeniden yazar. Çalışan sıralama şudur: şifrele, kaydet, sonra şifreli dosyayı imzala. İmzalama işlemi bir incremental update ekler; bu güncellemenin yeni string'leri mevcut dosya anahtarıyla şifrelenir ve /Contents yer tutucusu açıkta kalır. Dosyayı imzalama işleminde owner password ile açın; user password ancak izinler, imza alanlarını açıkça adlandıran ISO 32000-2 Table 22'nin 9. bitini içerdiğinde yeterlidir. Encrypt ve imzalama işlemi her PDFlibPas sürümünde mevcuttur
uses
SysUtils, PDFlibrary;
const
OwnerPwd = 'owner-secret';
UserPwd = 'reader-secret';
procedure EncryptCopy(const Source, Target: string);
var
Lib: TPDFlib;
Perms: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Source, '') <> 1 then
raise Exception.CreateFmt('Load failed (%d)', [Lib.LastErrorCode]);
// yazdırma, alan doldurma (9. bit imza alanlarını kapsar), erişilebilirlik kopyası, yüksek çözünürlüklü yazdırma
Perms := Lib.EncodePermissions(1, 0, 0, 0, 1, 1, 0, 1);
// Strength 4 = AES-256 revision 6: dosya anahtarı doğrudan kullanılır, /ID içinde yer almaz
if Lib.Encrypt(OwnerPwd, UserPwd, 4, Perms) <> 1 then
raise Exception.CreateFmt('Encrypt failed (%d)', [Lib.LastErrorCode]);
if Lib.SaveToFile(Target) <> 1 then
raise Exception.CreateFmt('Save failed (%d)', [Lib.LastErrorCode]);
finally
Lib.Free;
end;
end;
procedure SignField(Lib: TPDFlib; const Source, Target, FieldName,
PFXFile, PFXPassword: string);
var
SP, R: Integer;
begin
SP := Lib.NewSignProcessFromFile(Source, OwnerPwd);
if SP = 0 then
raise Exception.CreateFmt('Input not found: %s', [Source]);
try
Lib.SetSignProcessField(SP, FieldName); // yoksa görünmez biçimde oluşturulur
Lib.SetSignProcessPFXFromFile(SP, PFXFile, PFXPassword);
Lib.SetSignProcessInfo(SP, 'Approved for release', 'Head office', '');
// yer tutucuda, sonradan eklenecek imza zaman damgası için pay
Lib.SetSignProcessReserveContentsBytes(SP, 16384);
Lib.EndSignProcessToFile(SP, Target); // her zaman 0 döndürür
R := Lib.GetSignProcessResult(SP);
if R <> 1 then
raise Exception.CreateFmt('Signing %s failed (result %d)', [FieldName, R]);
finally
Lib.ReleaseSignProcess(SP);
end;
end;
Strength 4, AES-256 revision 6'yı seçer ve bu seçim iki başarısızlık modunu ortadan kaldırır. RC4 ve AES-128 altında Algorithm 1 (ISO 32000-1 §7.6.2), her nesne için nesne numarası ile generation numarasından yola çıkarak ayrı bir anahtar türetir. Algorithm 2 (ISO 32000-1 §7.6.3.3, e adımı) ilk /ID ögesini de dosya anahtarına katar. Bir imzalama güncellemesi AcroForm sözlüğünü ve sayfanın /Annots dizisini yeniden yazar; bu yüzden yeniden yazılan her nesne özgün generation numarasını korumak, güncellemenin trailer'i de aynı ilk /ID ögesini taşımak zorundadır. Generation numarasını yanlış tutarsanız o nesne artık çözülmez; /ID'yi yanlış tutarsanız hiçbiri çözülmez. Revision 6 dosya anahtarını doğrudan kullanır (ISO 32000-2 §7.6.3.3, Algorithm 1.A) ve ona /ID'ye bakmadan ulaşır (§7.6.4.3.3, Algorithm 2.A). PDFlibPas her güç seviyesinde yeniden yazılan nesnelerde generation numaralarını korur ve anahtarı /ID'den saklandığı haliyle türetir; bozuk bir dosya, ISO 32000-2 Table 15'in istediği doğrudan dizi yerine /ID 7 0 R taşıyorsa bile. Boş bir /ID [] şifre denetimini geçemez. Şifreden anahtara uzanan yol, ham dosya anahtarıyla şifreli PDF'leri açmak yazısında daha derinlemesine izlenir
Sonucu görmek için bir text editor yeter. İmza sözlüğü, onu ekleyen güncelleme içinde sıkıştırılmamış durur; çünkü imzalayan, /ByteRange ile /Contents'i dosya ofsetine göre yamalar. /ByteRange'i aratın, doğrudan üstüne düşersiniz. /Contents hex'i tipik olarak 3082 ile açılır — CMS SignedData SEQUENCE'unun DER başlığıdır bu — ve imzadan daha geniş kalan yer tutucunun bulunduğu yerde uzun bir sıfır dizisiyle biter. /Reason, metin değişmese bile bir imzalama koşusundan öbürüne değişen ciphertext'tir; çünkü AES her string için taze bir IV üretir. Güncel bir qpdf ile beklenen: qpdf --password=owner-secret --show-object=14 signed.pdf, çözülmüş /Reason'u değişmemiş bir /Contents yanında yazdırır. O denetim için strength 4 dosya kullanın. Ham dosyada beklenen şekil, ofsetler ve baytlar değişkenlik göstermek kaydıyla şöyle:
14 0 obj
<</Type /Sig /Filter /Adobe.PPKLite
/ByteRange [0 48211 81093 2304]
/Contents <308240A206092A864886F70D010702A0...000000000000>
/Reason <7F21C9E04B...>
/M <D35A0F98...>
...>>
endobj
Bir zaman damgası yamalandığında hangi imzalar bozulur?
Damgalanan imzadan sonra eklenen her imza bozulur. Bir PAdES imza zaman damgası, imzalayanın unsigned attribute'ü olarak saklanan bir RFC 3161 token'idir (RFC 5652 §5.3, unsignedAttrs); dolayısıyla CMS'in içinde, /Contents alanında yaşar. Biri eklemek, o hex'i yerinde yeniden yazmak demektir. Şifreli bir dosyada bu yeniden yazım tam da /Contents açık metin olduğu için çalışır: yeniden şifreleme yok, yeni nesne yok — token'in yalnızca imza anında ayrılan boşluğa sığması yeterlidir. Sahip imza, /ByteRange'i o baytları atladığı için ayakta kalır. Sonraki imzalar böyle bir muafiyet almaz. ISO 32000-2 §12.8.1 uyarınca her imzanın byte range'i %PDF- başlığından, onu ekleyen incremental update'in sonuna dek uzanır; bu yüzden ikinci bir imza ya da bir document time stamp, ilk imzanın /Contents alanını geri kalan her şeyle birlikte hash'e sokar. O boşluk yalnızca tek bir imzaya aittir. İkinci imzalayan imzasını attıktan sonra yamalarsanız, ikinci imzanın özeti artık tutmaz. Token isteğinin kendisi Delphi'den PAdES imzalama ve doğrulama yazısının konusudur; buradaki bölüm yalnızca hangi baytların yer değiştirdiğiyle ilgilenir
PDFlibPas, hiçbir şeyi hash'e sokmadan önce bu çakışmayı denetler. İmza zaman damgası işlemi, belgedeki öteki her imzanın /ByteRange'ini okur ve her span çiftini hedefin /Contents boşluğuyla karşılaştırır. Herhangi bir çakışma, hazırlığı process result 17 ile durdurur ve hiçbir çıktı dosyası yazılmaz. İşlem, TPDFlibEnterprise'i PDFlibEnterprise unit'inden örneklediğiniz Enterprise sürümüne aittir; öteki sürümler metotları tutmaya devam eder, bunlar 0 döndürür ve LastErrorCode değerini 900 yapar. Log'larınızda 17 sayısını dikkatle izleyin. GetPAdESSignatureTimeStampProcessResult'tan gelen 17, sonraki bir imzanın bunu kapladığını; GetSignProcessResult'tan gelen 17 ise bir certification seviyesinin ya da alan kilidinin imzalamayı yasakladığını anlatır. İkisini ayrı ayrı eşleyin. Aşağıdaki yardımcı, önceki listing'den SignField ile OwnerPwd'yi kullanır
uses
SysUtils, PDFlibrary, PDFlibEnterprise;
const
STS_COVERED_BY_LATER_SIGNATURE = 17;
function StampSignature(Lib: TPDFlibEnterprise; const Source, Target,
FieldName: string): Integer;
var
STS: Integer;
begin
Result := 0;
STS := Lib.NewPAdESSignatureTimeStampProcessFromFile(Source, OwnerPwd);
if STS = 0 then
Exit;
try
Lib.SetPAdESSignatureTimeStampField(STS, FieldName);
if Lib.ApplyPAdESSignatureTimeStamp(STS, 0) <> 1 then
begin
Result := Lib.GetPAdESSignatureTimeStampProcessResult(STS);
if Result = 1 then
Result := -1; // değer hash edildi, TSA alışverişi başarısız: bkz. GetTimeStampError
Exit;
end;
Result := Lib.EndPAdESSignatureTimeStampProcessToFile(STS, Target);
finally
Lib.ReleasePAdESSignatureTimeStampProcess(STS);
end;
end;
procedure SignAndStampInOrder(const EncryptedFile: string);
var
Lib: TPDFlibEnterprise;
R: Integer;
begin
Lib := TPDFlibEnterprise.Create;
try
Lib.SetTimeStampServer('https://tsa.example.com/tsa', '', '');
SignField(Lib, EncryptedFile, 'step1.pdf', 'Author', 'author.pfx', 'pfx-1');
R := StampSignature(Lib, 'step1.pdf', 'step1-ts.pdf', 'Author');
if R <> 1 then
raise Exception.CreateFmt('Author time stamp failed (%d)', [R]);
SignField(Lib, 'step1-ts.pdf', 'step2.pdf', 'Approver', 'approver.pfx', 'pfx-2');
R := StampSignature(Lib, 'step2.pdf', 'final.pdf', 'Approver');
if R <> 1 then
raise Exception.CreateFmt('Approver time stamp failed (%d)', [R]);
// Beklenen: 17. Approver'ın byte range'i, step2.pdf içinde Author'ın
// /Contents alanını kaplar; bu yüzden işlem reddeder ve restamped.pdf hiç yazılmaz
R := StampSignature(Lib, 'step2.pdf', 'restamped.pdf', 'Author');
if R <> STS_COVERED_BY_LATER_SIGNATURE then
raise Exception.CreateFmt('Unexpected result %d', [R]);
finally
Lib.Free;
end;
end;
Her imzayı, sıradaki imzalayan dosyaya dokunmadan önce damgalayın. Bu, imzalayan başına bir TSA turu maliyeti taşır ve her imzaya kendi güvenilir zamanını verir. Dosyanın birkaç imzayla hazır halde gelmesi farklı bir durumdur: öncekiler artık bir imza zaman damgası alamaz. Bunun yerine bir document time stamp ekleyin — yeni bir revision içinde /Type /DocTimeStamp imzası ve /SubFilter /ETSI.RFC3161; bu imzanın byte range'i tüm önceki imzaları kapsar ve o anda var olduklarını kanıtlar. ApplyPAdESLongTermArchiveValidation, doğrulama malzemesini ekledikten sonra bir tane ekler. İmzalarken, ilk listing'deki gibi SetSignProcessReserveContentsBytes üzerinden bir imza zaman damgası için yer ayırın. TSA sertifika zincirini gömen bir token kolayca birkaç kilobayta ulaşır ve o payı olmayan bir yer tutucu, sonraki güçlendirmenin result 13 ile başarısız olmasına yol açar
İmzalı bir dosyanın şifresini çözmek ve doğrulayıcının neyi hash ettiği
Şifreli bir dosyanın imzalı aralıkları ciphertext'tir. Doğrulayıcı, saklanan ciphertext'in özetini CMS'in içindeki özetle karşılaştırır: /ByteRange integer'dır, /Contents açık metindir ve bütünlük denetimi anahtara ihtiyaç duymaz — anahtara ihtiyaç duyan yalnızca alan adı ile /Reason'dur. Aynı gerçek, imzalı bir dosyanın şifresini çözüp düz olarak kaydetmenin içindeki her imzayı geçersiz kılmasını da açıklar; görünürde tek bir piksel bile değişmemiştir. Şifreli, imzalı özgünü kayıt kopyası olarak saklayın. Cross-reference stream'ler de hiçbir zaman şifrelenmez (ISO 32000-2 §7.5.8.2); bu yüzden bir xref stream'le yazılan güncelleme, herhangi bir anahtar türetilmeden önce ofsetlerini okunabilir tutar. Yerleşim, Delphi'de object stream'ler ve cross-reference stream'ler yazısında anlatılır. İmzalı bir aralık, karşılaştırma için belleğe alınmayacak kadar büyüdüğünde PDFlibPas bütünlük denetimini geçersiz değil bilinmiyor olarak raporlar; çünkü hiç karşılaştırılmamış baytların farklı olduğu gösterilememiştir
Şifreli bir dosyanın imza değeri, imzalamadan sonra başka bir imza ya da bir document time stamp onu kaplayana dek yazılabilir kalır. Zaman damgası adımını o pencerenin içine koyun; ilk listing'deki şifrele-sonra-imzala sıralaması, Delphi için PDF Library tarafındaki çok imzalayıcılı dosyalara değişmeden taşınır