Teknik Makale

Şifreli PDF'in Açıkta Bıraktığı İmza Değeri

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 /ID girdisinin değerleri
  • /Encrypt sözlüğünün içindeki stringler
  • streamlerin içindeki stringler; bunlar bir bütün olarak şifrelenir
  • bir imza sözlüğünde /Contents değ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üğü girdisiPDF türüŞifreli saklanır mı?
/Contentshexadecimal stringHayır, ISO 32000-2 §7.6.2 uyarınca muaf
/Reason, /Location, /ContactInfo, /Nametext stringEvet
/Mdate stringEvet
/ByteRangeinteger dizisiHayır, integerlar hiçbir zaman şifrelenmez
/Type, /Filter, /SubFilternameHayı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

Delphi'de PDFlibPas şifreli PDF imza sözlüğü: ISO 32000-2 §7.6.2 altında hexadecimal /Contents açık kalırken /Reason ve /M şifreli kalıyor; RC4, AES-CBC ve AES-GCM başarısızlık modlarıyla birlikte
Şifreli PDF'te imzanın /Contents alanı okunabilir DER olarak kalırken /Reason ve /M ciphertext olur; o DER'i yine de çözmek her cipher altında farklı biçimde başarısız olur

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

Delphi'de PDFlibPas şifreleme testi: string'i şifrelemeden ya da şifresini çözmekten önce /ByteRange, /Type Sig veya DocTimeStamp ile /Filter üzerinden imza /Contents alanını bir annotation /Contents alanından ayırt ediyor
Annotation yorumları şifreli kalır; çünkü PDFlibPas /Contents alanını yalnızca yanında /ByteRange ve eşleşen bir /Type varken, /Type yoksa da bir /Filter adı varken imza değeri sayar

Ö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

Delphi'de PDFlibPas PAdES imza zaman damgası sıralaması: sonraki imzanın /ByteRange'i önceki bir /Contents boşluğunu kapladığında GetPAdESSignatureTimeStampProcessResult 17 döndürür ve hiçbir dosya yazılmaz
Approver imzasını attıktan sonra byte range'i Author /Contents boşluğunu kaplar; PDFlibPas geç kalan Author zaman damgasını, sonraki imzayı bozmak yerine process result 17 ile reddeder
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