Teknik Makale

Delphi'de FDF Differences: İmzalı Form Revizyonları Eklemek

HotPDF Component, bir FDF /Differences akışını Delphi tarafından THPDFHeadlessDocument.SaveFDFDifferencesToFile ile uygular: metot, yüklü PDF'in tam bayt uzunluğu ve SHA-256 ile tanımladığınız bir taban revizyonla hâlâ eşleştiğini kontrol eder, birikimli revizyon baytlarını tam olarak bir kez ekler, sonucu şifreleme izni ile DocMDP/FieldMDP kabulünden geçirir ve atomik olarak yayımlar. Aynı gönderiyi tekrarlamak bayt bayt bir no-op'tur

Varlık sebebi olan iş akışı gösterişsiz ve yaygındır. Bir sözleşmeyi ya da denetim formunu PDF olarak gönderirsiniz; biri kendi görüntüleyicisinde doldurur ve imzalar; geri dönen şey PDF değil bir FDF dosyasıdır. Alan değerleri /Fields içinde seyreder ama imzalı revizyonun kendisi — CMS imzasının gerçekten kapsadığı baytlar — /Differences içinde seyreder. Birleştirmeyi yanlış yaparsanız ya imzayı bozar ya da daha kötüsü, hiçbir imzanın kapsamadığı alan değerlerini kabul edersiniz

Bir FDF /Differences akışı gerçekte ne taşır?

Bir FDF /Differences akışı, hedef PDF için ham artımlı güncelleme baytları taşır; FDF düzeyinde bir geçmiş taşımaz. ISO 32000-1 §12.7.7.3, Tablo 243, FDF sözlüğünün Differences girdisini, alttaki PDF'in açıldığından bu yana yapılmış artımlı değişiklikleri olarak tanımlar; biçim, §7.5.6 altındaki bir PDF artımlı güncellemesiyle aynıdır: yeni ve değiştirilmiş nesneler, bir çapraz referans bölümü ve /Prev'i taban dosyanın içine işaret eden bir trailer

FDF kapsayıcısının kendisi tam tersidir. §12.7.7.1, FDF dosyalarına artımlı güncellemeleri yasaklar; bir FDF tek trailer, yalnızca generation 0 nesneler ve özgün nesne numaraları taşır. HotPDF Component bunu FDF ayrıştırıcısında aynen zorunlu kılar ve /Differences'i kolaylaştırmak için asla gevşetmez. Artımlı veri, kesinlikle artımsız bir kapsayıcının tek bir akışının içinde yaşar; o iki katmanı ayrı tutmak işin yarısıdır

Delphi için HotPDF FDF /Differences anatomisi şeması: tek trailer ve generation 0 nesneleriyle kesinlikle artımsız bir FDF kapsayıcısı, ham artımlı güncelleme baytlarından oluşan tek bir birikimli /Differences akışı taşır; /Fields ile /Annots imzasız ve birleştirilmemiş kalır
İmzalı revizyon /Differences akışında seyreder, /Fields içinde değil: kapsayıcının kendisi artımlı güncellemeleri yasaklar ve CMS imzasının kapsadığı bayt yükü güvenilir tek veridir

Akışın iki özelliği, sonrasındaki her tasarım kararını biçimlendirir:

  • Akış birikimlidir: hedef PDF açıldığından beri eklenen her baytı tutar; aynı oturumdan yapılan ikinci gönderi, hâlihazırda commit ettiğiniz revizyonu yineler ve yeniyi arkasına ekler
  • Aynı FDF'nin dış /Fields ve /Annots girdilerinin /Differences ile uyuşacağı garanti edilmez; kolayca okuyabildiğiniz alan değerleri, imzanın kapsadığı veri değildir

İkinci nokta açık sözlülüğü hak eder. İçe aktarma kodunuz /Fields'ten /V'yi okuyup sonucu imzalı sayıyorsa — çünkü imza aynı dosyada gelmiştir — bir sahtecilik kanalı kurmuşsunuz demektir. HotPDF Component yolları bilerek ayrı tutar: FDF açıklama içe aktarıcısı /Differences'i tümüyle reddeder ve ona yalnızca özelleşmiş Differences taşıyıcısı dokunur

Eklemeyi açık bir taban revizyona bağlamak

SaveFDFDifferencesToFile, FDF'nin hangi revizyona ait olduğunu tahmin etmeyi reddeder. Çağıran BaseBytes — formun açıldığı eksiksiz PDF revizyonunun tam uzunluğu — ile BaseSHA256, yani 64 haneli onaltılık SHA-256 değerini verir. Metot yüklü belgenin o önekini yeniden özetler, uyuşmazlığı "The loaded PDF does not match the explicitly selected base" iletisiyle reddeder ve önekin, startxref'i gerçek çapraz referans verisini gösteren gerçek bir revizyon ucunda bittiğini kontrol eder

Pratik kural, iki sayıyı da formu gönderdiğinizde kaydetmektir; FDF geri döndüğünde değil. HPDFCrypt içindeki SHA256Init / SHA256Update / SHA256Final rutinlerinin üzerine küçük bir yardımcı yeter:

uses
  SysUtils, Classes, HPDFCrypt;

function PrefixSHA256Hex(const FileName: string; PrefixBytes: Int64): AnsiString;
var
  Stream: TFileStream;
  Ctx: TSHA256Context;
  Digest: TSHA256Digest;
  Buffer: array[0..65535] of Byte;
  Remaining: Int64;
  Count, I: Integer;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if (PrefixBytes <= 0) or (PrefixBytes > Stream.Size) then
      raise EArgumentException.Create('Base length is outside the file');
    SHA256Init(Ctx);
    Remaining := PrefixBytes;
    while Remaining > 0 do
    begin
      Count := SizeOf(Buffer);
      if Remaining < Count then
        Count := Integer(Remaining);
      Stream.ReadBuffer(Buffer, Count);
      SHA256Update(Ctx, Buffer, Count);
      Dec(Remaining, Count);
    end;
    SHA256Final(Ctx, Digest);
  finally
    Stream.Free;
  end;
  Result := '';
  for I := 0 to High(Digest) do
    Result := Result + AnsiString(LowerCase(IntToHex(Digest[I], 2)));
end;

// Gönderim sırasında: giden dosyanın tamamı taban revizyondur
// BaseLength := 'out\contract-0042.pdf' boyutunun bayt cinsinden değeri
// BaseHash   := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);

Yüklü belge tam olarak taban olmak zorunda değildir. Taban artı daha önceki gönderilerden hâlihazırda commit ettiğiniz revizyonlar olabilir; birikimli akışın yönetilebilir kalması da böyle sağlanır: BaseBytes özgün öneki adlandırmaya devam eder ve sonrasındaki her şey önceki ekleme baytları sayılır

Delphi için HotPDF FDF Differences bayt düzeni: yüklü PDF, BaseBytes uzunluğu ve BaseSHA256 ile doğrulanan taban revizyon artı hâlihazırda commit edilmiş önceki ekleme baytları ile, yeni bir sonek eklemeden önce o baytları yinelemek zorunda olan birikimli /Differences yükü olarak ayrışır
Taban öneki uzunluk ve SHA-256 ile sabitlenir; commit edilen baytlar yalnızca ekleme yapan bir sözleşme gibi davranır ve yalnızca sondaki sonek yenidir — tekrarlanan bir gönderi böylece bayt bayt bir no-op olarak düşer

Differences'ı SaveFDFDifferencesToFile ile uygulamak

THPDFHeadlessDocument.SaveFDFDifferencesToFile, çıktı dosyası adını, FDF baytlarını, taban uzunluğu ile özetini, isteğe bağlı bir gömülü FDF seçimini ve bir THPDFFDFPackageOptions sınır kaydını alır, bir THPDFHeadlessFDFDifferencesResult döndürür. İçeride FDF'yi bir THPDFFDFPackage olarak açar ve DecodeDifferences'ı kendisi çağırır; yani siz ön çözülmüş bir akış değil FDF dosyasını verirsiniz. Boş bir Selection üst düzey FDF demektir; boş olmayan biri önce gömülü bir alt FDF'ye girer

uses
  SysUtils, IOUtils, HPDFHeadlessDocument, HPDFHeadlessCMS, HPDFFDFPackage;

procedure CommitReturnedForm(const CurrentPdf, NextPdf, FdfFile: string;
  BaseLength: Int64; const BaseHash: AnsiString);
var
  Doc: THPDFHeadlessDocument;
  Policy: THPDFHeadlessCMSOptions;
  Selection: THPDFFDFChildSelections;
  Applied: THPDFHeadlessFDFDifferencesResult;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Policy := THPDFHeadlessCMSOptions.Default;
    Policy.TrustedCAFile := 'trust\issuing-ca.pem';
    Doc.SetSignaturePolicyOptions(Policy);
    Doc.LoadFromFile(CurrentPdf);

    Selection := nil;  // üst düzey FDF, gömülü alt yok
    Applied := Doc.SaveFDFDifferencesToFile(NextPdf,
      TFile.ReadAllBytes(FdfFile), BaseLength, BaseHash,
      Selection, THPDFFDFPackageOptions.Default);

    Writeln(Format('base %d, prior %d, total %d, added %d, no-op %s',
      [Applied.BaseBytes, Applied.PriorAppendBytes, Applied.TotalAppendBytes,
       Applied.AddedBytes, BoolToStr(Applied.NoOp, True)]));
  finally
    Doc.Free;
  end;
end;

Kontrollerin sırası çağrının kendisinden daha önemlidir. Taban özeti geçtikten sonra metot, hâlihazırda commit edilmiş ekleme baytlarını — BaseBytes ile yüklü dosyanın sonu arasındaki her şeyi — çözülmüş yükün başıyla karşılaştırır. Commit ettiğinizden daha kısa bir gönderi "A cumulative submission cannot remove committed append bytes" ile başarısız olur; onunla çelişen biri "The cumulative Differences conflict with the committed revision" ile. Yalnızca kalan sonek yenidir ve AddedBytes'a yalnızca o sonek düşer

Delphi'de HotPDF SaveFDFDifferencesToFile kontrol hattı: taban uzunluğu ve SHA-256; birikimli önek karşılaştırması; geçici aday doğrulaması; /ID ve imza kimliği; izin bitleri artı DocMDP ve FieldMDP kabulü; ardından eklenen revizyonun atomik yayımı
Eklenen revizyonun herhangi bir baytı yayımlanmadan önce altı fail-closed kapı sabit bir sırada çalışır; bozuk bir FDF ya da izin verilmeyen bir değişiklik, yüklü belgeyi ve mevcut çıktıyı olduğu gibi bırakır

Kurulan aday, hedef dizinde özel (exclusive) bir geçici dosyaya yazılır, diske boşaltılır, bağımsız bir revizyon kaynağı olarak yeniden açılır ve çıktının yerini almadan önce doğrulanır. Ayrıştırma, iptal, politika, bütçe ya da yayım başarısızlıkları hem yüklü belgeyi hem var olan çıktı dosyasını olduğu gibi bırakır. İki önkoşul insanları erkenden yakalar: çıktı yüklü kaynağın adı olamaz ve belgenin kaydedilmemiş düzenlemesi olamaz; çünkü opak bir ekleme onları sessizce atardı ("Publish pending document edits before applying Differences")

Tekrarlanan bir uygulama neden bayt bayt bir no-op'tur?

Aynı birikimli FDF'nin tekrarlanan uygulaması no-op'tur, çünkü yükünün her baytı zaten dosyadadır. Yük, önceki ekleme baytlarına tam olarak eşit olduğunda SaveFDFDifferencesToFile NoOp = True ile AddedBytes = 0 raporlar ve yayımladığı çıktı geçerli PDF ile özdeştir. Bu, "yalnızca ekleme"den daha güçlü bir garantisidir ve ağ zaman aşımı ya da çöken bir işçiden sonraki yeniden denemeleri güvenli kılan da budur

// Rev 1 zaten contract-0042-r1.pdf içine commit edildi; aynı FDF iki kez geliyor
// (Doc, Again, Fdf, Selection, BaseLength ve BaseHash yukarıdaki gibi bildirili)
Doc := THPDFHeadlessDocument.Create;
try
  Doc.LoadFromFile('store\contract-0042-r1.pdf');
  Again := Doc.SaveFDFDifferencesToFile('store\contract-0042-r1-retry.pdf',
    Fdf, BaseLength, BaseHash, Selection, THPDFFDFPackageOptions.Default);
  // Again.NoOp = True, Again.AddedBytes = 0,
  // ve yeniden deneme dosyası contract-0042-r1.pdf ile bayt bayt özdeş
finally
  Doc.Free;
end;

Kabul testlerinizi buna göre yazın. Yeniden deneme çıktısı ile geçerli revizyon arasında dosyanın küçülmediğini değil tam bayt eşitliğini öne sürün. Yalnızca yalnızca-ekleme davranışını kontrol eden bir test, aynı revizyonu ikinci kez ekleyen bir uygulamayı memnuniyetle geçer; bu da geçerli görünümlü ama yinelenmiş bir güncelleme bölümü taşıyan ve sonraki bir DocMDP denetiminin açıklamak zorunda kalacağı bir PDF üretir

Revizyon yayımlanmadan önce hangi kabul kontrolleri çalışır?

Yayımdan önce aday PDF kimlik, imza ve izin kontrollerinden geçmek zorundadır ve hiçbiri dış FDF alanlarına güvenmez. Trailer'ın /ID'sinin ilk elemanı değişmeden kalmalı ve yüklü belgede hâlihazırda bulunan her imza alanı, özdeş imza sözlüğüyle ve aynı imza değeri kimliğiyle varlığını sürdürmelidir. Var olan bir imzayı düşüren, değiştiren ya da yeniden yazan bir Differences yükü hdSignatureModificationUnsupported ile başarısız olur

Sıra yetkilendirme katmanlarında. Şifreli bir belge üzerinde boş olmayan bir eklemede değişen nesneler role göre sınıflanır (katalog ve sayfalar, açıklamalar, AcroForm alanları, appearance stream'ler) ve kimliği doğrulanmış kullanıcı izin bitleriyle karşılaştırılır; açıklama düzenleyen bir revizyon açıklama iznine ihtiyaç duyar, gerisi aynı. Sonra aday, HotPDF Component'in başka yerlerde kullandığı sertifika ve alan kilidi uygulamasından geçer: bir DocMDP sertifika imzası (ISO 32000-1 §12.8.2.2), sonraki revizyonların ne tür değişiklik yapabileceğini sınırlar; FieldMDP kilitleri (§12.8.2.4) adlandırılmış alanları korur. HotPDF'te DocMDP ve FieldMDP revizyon analizi, o izinlerin revizyon başına nasıl sınıflandığını anlatır

Hata işlemesi "bu FDF bozuk" ile "bu revizyona izin yok"u ayırmalıdır; çünkü ikinci durum çoğu zaman bir insana ihtiyaç duyar:

try
  Applied := Doc.SaveFDFDifferencesToFile(NextPdf, Fdf, BaseLength, BaseHash,
    Selection, THPDFFDFPackageOptions.Default);
except
  on E: EHPDFFDFPackageError do
    QuarantineFdf(FdfFile, 'No usable Differences stream: ' + E.Message);
  on E: EHPDFHeadlessDocumentError do
    case E.Code of
      hdPermissionDenied, hdSignatureModificationUnsupported:
        EscalateForReview(FdfFile, E.Message);
      hdLimitExceeded:
        QuarantineFdf(FdfFile, 'Over budget: ' + E.Message);
    else
      QuarantineFdf(FdfFile, E.Message);  // taban uyuşmazlığı, çelişki, bozuk uç nokta
    end;
end;

İşlemin asla yapmadıkları, kontrol ettikleri kadar önemlidir. SaveFDFDifferencesToFile, FDF'nin /F dosya belirtimini asla getirmez; FDF içinde adı geçen PDF, birleştirmeyi diskteki başka bir dosyaya ya da ağa yönlendiremez ve işlem JavaScript ya da eylem asla çalıştırmaz. Her girdi açıkça gelir: yüklü taban, FDF baytları ve yapılandırdığınız güven malzemesi

Windows işlemleri ve C ABI fdf.differences.apply

Aynı taşıyıcı, headless kodu doğrudan yazmadan da kullanılabilir. THPDFWindowsFormDataTransaction.ApplyFDFDifferences, FDF baytlarını, taban uzunluğunu, taban SHA-256'sını, seçimi, paket seçeneklerini ve bir parolayı alır, işi SaveFDFDifferencesToFile'a devreder ve işlemin bayt muhasebesini, rollback, iptal ve aynı tanıtıcıyla yeniden deneme davranışlarını ekler. Delphi dışı çağıranlar için C ABI, base'in uzunluğu ile özeti taşıdığı JSON işlemini fdf.differences.apply olarak ortaya koyar:

{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
  "base": { "bytes": 673, "sha256": "..." },
  "signaturePolicy": { "trustedCAPEM": "..." } }

JSON yanıtı baseBytes, priorAppendBytes, totalAppendBytes, addedBytes ve noop bildirir; THPDFHeadlessFDFDifferencesResult ile aynı alanlardır. Bu taşıyıcının yayımlanan kabul kapıları Linux FPC 3.3.1, Windows Delphi 32 bit ve 64 bit ve Windows FPC 32 bit ve 64 bit üzerinde çalışır; her güvenlik hedefi, 1,244 yerel kontrol ve 382 callback komutuyla 21 durumu kapsar

Bilmeye değer sınırlar ve test verisi tuzakları

Differences taşıyıcısı bilinçli olarak dardır ve birkaç kenarı gerçek test setlerini ısırdı:

  • Taban, eksiksiz bir PDF revizyonu olmalıdır. Dosyanın ortasına düşen bir uzunluk ya da startxref'i gerçek çapraz referans verisini göstermeyen bir uzunluk, tek bir bayt kopyalanmadan reddedilir
  • İmza test verilerinde dolaylı bir /Lock gerekir. Windows yüklü belge izin okuyucusu, /Lock'u satır içi sözlük olan bir imza alanını "Signature field /Lock must be an indirect reference" ile reddeder; bazı imzalama yolları satır içi biçimi tolere etse de kilit sözlüğünü kendi nesnesi olarak yazın
  • Çıplak openssl req -x509 ile yapılan bir test CA'sında CA:TRUE ile keyCertSign yoktur; zincir doğrulaması, kütüphane hatasına benzeyen ama olmayan bir sertifika doğrulama hatasıyla başarısız olur
  • Dış /Fields değerleri bu çağrıyla birleştirilmez. Onları belgede de istiyorsanız bilgilendirici olup olmadıklarına açıkça karar verin ve asla imzalı olarak sunmayın
  • FDF'nin kendisi artımsız kalır. Birden çok trailer, sıfırdan farklı generation ya da yinelenen nesne numaraları taşıyan bir FDF dosyası daha zengin değil bozuk bir FDF'dir

İmzalı revizyonların devrede olmadığı, FDF ve XFDF'nin sade değer tarafı için HotPDF'te çok seçimli FDF ve XFDF gidiş dönüşü ile Delphi'de XFDF açıklama içe ve dışa aktarımı daha iyi başlangıç noktalarıdır

SSS

FDF dosyası yerine çözülmüş Differences akışını verebilir miyim?

Hayır. SaveFDFDifferencesToFile, verdiğiniz FDF'yi bir THPDFFDFPackage olarak ayrıştırır, isteğe bağlı gömülü seçimi uygular ve DecodeDifferences'ı dahilen çağırır. Ham revizyon baytlarını incelemek ya da arşivlemek istediğinizde THPDFFDFPackage.DecodeDifferences'ı tek başına kullanın ama FDF dosyasını apply çağrısına verin

İstemci ikinci, daha büyük bir birikimli FDF gönderdiğinde ne olur?

İlk commit edilen revizyonu içeren PDF'i yükleyin ve aynı BaseBytes ile BaseSHA256'yı koruyun. Metot, yeni yükün tam olarak commit edilmiş baytlarla başladığını doğrular, yalnızca yeni soneki ekler ve onu AddedBytes'ta bildirir, sonra sonuç üzerinde tam izin ile DocMDP/FieldMDP kabulünü çalıştırır

Başarılı bir uygulama, /Fields içindeki alan değerlerinin imzalı olduğu anlamına gelir mi?

Hayır. ISO 32000-1, tek bir FDF'deki /Fields, /Annots ve /Differences'in uyuşmasını gerektirmez ve HotPDF Component onları uzlaştırmaz. İmzalı olan, eklenen revizyondaki CMS imzalarının kapsadığı her neyse odur; imzalı değerleri FDF sarmalayıcısından değil birleştirilmiş PDF'den okuyun

FDF Differences taşıyıcısı, headless imza politika motoru ve Windows form verisi işlemlerinin tamamı HotPDF Component içinde, Delphi, C++Builder ve Free Pascal ile gelir