Teknik Makale

PDFium ile Delphi'de İmza Sonrası PDF Değişiklik Analizi

Bir PDF imzalandıktan sonra neyin değiştiğini bulmak için Delphi ve Lazarus için PDFium Component, TPdf.AnalyzeSignatureRevisions sunuyor: özgün dosya baytlarından her artımlı revizyonu yeniden kuran, sonraki her nesne değişikliğini o imzanın DocMDP ve FieldMDP kurallarına göre puanlayan ve shadow nesne tanımlarını ayrı bir risk olarak raporlayan bir imza-sonrası revizyon değişiklik analizcisi. Hedeflediği durum sözleşme uğraşan herkese tanıdıktır: sertifikalanmış bir form gider, iki artımlı kaydetme daha yapılmış hâlde döner ve her imza hâlâ doğrulanır. Bu beklenen bir şeydir, çünkü imza yalnızca kendi revizyonunun baytlarını kapsar. Gerçek soru, sonraki kaydetmelere izin verilip verilmediğidir ve imzadaki yeşil tik bunu cevaplamaz

PDFium imza API'si imzadan sonra neyin değiştiğini neden gösteremez?

PDFium imza API'si imza-sonrası değişiklikleri gösteremez, çünkü yalnızca imza sözlüğünü okur: /Contents, /ByteRange, /SubFilter ve DocMDP izin değeri. PDFium'un artımlı revizyon grafiği yoktur, FieldMDP transform parametrelerini parse etmez ve revizyonlar arasında nesne-düzeyi bir diff sunmaz; FPdfPades.pas içindeki analizci bu yüzden doğrudan ham baytlar üzerinde çalışır. Bunun tasarımınıza yansıtmanız gereken pratik bir sonucu var. TPdf.AnalyzeSignatureRevisions, belge yüklenirken tutulan baytları okur; asla SaveAs ile üretilmiş bir kopyayı okumaz, çünkü yeniden yazılmış dosya analiz edilen revizyon yapısını kaybetmiştir. Belge, indirmesi bitmemiş progresif bir kaynaktan geldiyse rapor, eksik dosyayı analiz etmek yerine SourceStatus = pvssIncomplete ve Status = prasIndeterminate döndürür

startxref, xref stream'leri ve /Prev'den revizyon sınırlarını yeniden kurmak

Analizci, ISO 32000-1 §7.5.6 ve §7.5.8'de artımlı güncellemeler için tanımlanan biçimde, her startxref'i klasik xref tabloları, çapraz-referans stream'leri, hibrit-referans /XRefStm girdileri ve /Prev zinciri boyunca geriye izleyerek revizyon sınırlarını yeniden kurar. Her imzanın kapsadığı uzunluk, ikinci ByteRange aralığının sonudur ve analizci o uzunluğu, xref bölümünün içinde düştüğü revizyona eşler. Hiçbir revizyon eşleşmezse imza prrCoveredRevisionNotFound ve Indeterminate bir durum alır. Ardından her nesnenin durumu kapsanan revizyona kadar yeniden oynatılır ve sonraki her xref girdisi o durumla karşılaştırılır. Bu, duyulduğundan daha önemli: bazı yazıcılar her artımlı kayıtta xref tablosunun tamamını yeniden yazar ve hâlâ aynı değişmemiş nesneyi gösteren bir girdi, değişiklik olarak raporlanmak yerine atlanır. O karşılaştırma olmasa tamamen yasal bir form doldurma, yüzlerce sahte değişiklikte boğulurdu

AnalyzeSignatureRevisions Delphi'de ham PDF baytlarından artımlı revizyonları nasıl yeniden kuruyor: imza ByteRange'i kapsanan revizyonun içinde biter, xref Prev zinciri sonraki her kaydetme boyunca geriye yürür, nesne durumu kapsanan revizyona kadar yeniden oynatılır ve değişmemiş yeniden yazılan girdiler değişiklik olarak raporlanmak yerine atlanır
Bir imza yalnızca kendi revizyonunun baytlarını kapsar; analizci ikinci ByteRange aralığını bir revizyona eşler ve sonraki her xref girdisini yeniden oynatılan nesne durumuna göre puanlar

Shadow tanımlar, en çok ilgiyi hak eden vakadır. Sonraki bir revizyonun bayt aralığında görünen ama o revizyonun xref'i tarafından referans verilmeyen bir nesne gövdesi normal bir görüntüleyiciye görünmez; shadow saldırılarının dayandığı sahneleme türü tam olarak budur: gizli içerik imzadan önce ya da sonra yerleştirilir ve sonra bir referans çevrilerek etkinleştirilir. AnalyzePadesSignatureRevisionsBytes böyle bir nesneyi IsAuthoritative = False ile yetkisiz bir değişiklik olarak kaydeder, izin düzeyine bakmaksızın prdSuspicious puanlar ve risk kümesine prrUnreferencedObjectDefinition ekler. İki ilgili risk diğer yapısal hileleri kapsar: prrDuplicateObjectDefinition, bir xref bölümü aynı nesneyi birden fazla kez listelediğinde tetiklenir; prrSignatureObjectRedefined, sonraki bir revizyon mevcut bir imza nesnesini yeniden tanımladığında tetiklenir

Sonraki bir PDF revizyon bayt aralığında shadow nesne tanımı: nesne gövdesi mevcuttur ama ona referans veren hiçbir xref girdisi yoktur, görüntüleyiciler onu asla göstermez; PDFium Component'taki AnalyzeSignatureRevisions onu yetkisiz kaydeder, prdSuspicious puanlar ve yinelenen ile yeniden tanımlanan imza risklerinin yanına prrUnreferencedObjectDefinition kaldırır
Gizli içerik imzadan önce ya da sonra yerleştirilir ve sonra bir referans çevrilerek etkinleştirilir; bu yüzden referans verilmeyen bir gövde, DocMDP izin düzeyine bakmaksızın şüpheli puanlanır
uses
  SysUtils, TypInfo, PDFium, FPdfPades;

const
  ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
  Pdf: TPdf;
  Report: TPadesRevisionAnalysisReport;
  i, j: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-returned.pdf';
    Pdf.Active := True;
    Report := Pdf.AnalyzeSignatureRevisions;
    Writeln('Revisions: ', Report.RevisionCount,
      '  Signatures: ', Report.SignatureCount,
      '  Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
        Ord(Report.Status)));
    for i := 0 to High(Report.Signatures) do
      with Report.Signatures[i] do
      begin
        Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
          [SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
           DocMdpPermission,
           GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
        for j := 0 to High(Changes) do
          Writeln(Format('  rev %d  obj %d  %s -> %s%s',
            [Changes[j].RevisionIndex, Changes[j].ObjectNumber,
             GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
             GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
             ShadowTag[not Changes[j].IsAuthoritative]]));
      end;
  finally
    Pdf.Free;
  end;
end;

DocMDP ve FieldMDP her imza için nasıl uygulanır?

DocMDP ve FieldMDP her imza için ayrı ayrı, o imzanın kendi kapsadığı revizyonda uygulanır; aynı dosyadaki bir sertifikalandırma imzası ile daha sonraki bir onay imzası, aynı düzenleme hakkında farklı hükme varabilir. Sonraki her nesne önce /Type, /Subtype ve /FT girdilerinden ve sayfa, form, annotation ile DSS grafiklerindeki rolünden bir TPadesRevisionChangeKind içine sınıflanır. /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia ya da /EmbeddedFile taşıyan her şey prckActiveContent olur. Karar sonra ISO 32000-1 §12.8.2.2'yi izler: P=1 ile çapraz-referans verisi ve doğrulama malzemesi dışındaki her şey yasaktır; P=2 form doldurmaya ve sonraki imzalara izin verir ama annotation değişikliklerini reddeder; P=3 annotation'lara da izin verir. Sayfa içeriği, belge yapısı, metadata, aktif içerik ve silinmiş nesneler herhangi bir DocMDP düzeyi altında yasaktır ve imza hiç DocMDP taşımıyorsa prdSuspicious puanlanır; bir onay imzası biçimsel olarak hiçbir şeyi yasaklamaz ama okuyucu artık imzalananı görmemektedir

ISO 32000-1 §12.8.2.4'ten gelen FieldMDP, form-alanı kararını bir daraltır daha. pfmaAll her alanı kilitler, pfmaInclude yalnızca listelenen alanları kilitler, pfmaExclude listelenenler dışındaki her şeyi kilitler. Include ya da Exclude'i uygulamak için analizci, değişen her alanı /Parent zinciri üzerinden tam nitelikli adına çözer ve kilit listesiyle tam eşleşmeyle karşılaştırır; bir ebeveyn adının çocuklarını kapsamasını bekleme, uç alan adlarını listeleyin. Bir ad çözümlenemediğinde ya da transform, parser'ın tanımadığı bir eylem kullandığında değişiklik prdIndeterminate olur ve prrFieldMdpUnresolved kaldırılır. Değişiklik-başı kararlar sonra en kötü öncelikli birleşir: Suspicious, Disallowed'ın üzerinde; Disallowed, Indeterminate'ın üzerinde; Indeterminate, Allowed'ın üzerinde sıralanır, böylece tek bir shadow nesnesi her sayıda meşru alan güncellemesini ağır basar

AnalyzeSignatureRevisions'ın Delphi'de her imza-sonrası değişikliğe uyguladığı puanlama hattı: Type ve Subtype girdilerinden bir TPadesRevisionChangeKind, kapsanan revizyonda P=1'den P=3'e bir DocMDP kararı, tam nitelikli alan adları üzerinde bir FieldMDP kilidi kontrolü ve prdSuspicious'tan prdAllowed'a en kötü öncelikli bir birleştirme
Tek bir shadow nesnesi her sayıda meşru alan güncellemesini ağır basar, çünkü Suspicious, Disallowed, Indeterminate ve Allowed'ın üzerinde sıralanır; bazı riskler ise durumu düşürmeden yanına kaydedilir

Bazı değişiklikler güvenli yerine neden Indeterminate dönüyor?

Analizci bir değişikliğe izin verildiğini kanıtlayamadığında değişiklikler Indeterminate döner; bir imza kontrolünde bilinmeyen asla izin verilmiş olarak raporlanmamalıdır. Yaygın bir vaka bunun yerine hassas biçimde ele alınır: uzun süreli doğrulama bir /DSS ekler ve kataloğu yeniden yazar; bu aksi hâlde P=1 altında yapısal bir değişiklik sayılırdı. Analizci eski ve yeni katalog sözlüklerinden /DSS ile /Extensions'ı soyar ve kalanı karşılaştırır; başka hiçbir şey farklı değilse yeniden yazım bir doğrulama-malzemesi güncellemesi sayılır ve izin verilir, böylece B-LT ve B-LTA genişletmesi sertifikalandırma imzasını bozmaz. Diğer boşluklar bilerek açık bırakılır. Çapraz-referans stream'indeki Type-2 girdiler sıkıştırılmış object stream'lere işaret eder ve analizci bu güvenlik sınırının içinde object stream'leri açmaz; o değişiklikler prrCompressedObjectUnresolved ile birlikte prckCompressedObject olarak belirir, P=1 altında yasak ve aksi hâlde Indeterminate'tir. 1024 revizyon, 1.000.000 nesne numarası ve 2.000.000 raporlanmış değişiklik sert bütçeleri prrResourceLimitExceeded üretir, kırık bir xref zinciri prrMalformedRevisionChain; ikisi de Indeterminate olarak biter, asla geçiş olarak değil

const
  StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrResourceLimitExceeded];

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Bazı riskler Status'u değiştirmeden kaydedilir, önce onları test edin
  if R.Risks * StructuralRisks <> [] then
    Exit('review: structural risk in the revision chain');
  case R.Status of
    prasNoLaterChanges: Result := 'accept: nothing was added after signing';
    prasAllowed:        Result := 'accept: every later change is permitted';
    prasDisallowed:     Result := 'reject: a change violates DocMDP or FieldMDP';
    prasSuspicious:     Result := 'reject: shadow or unconstrained content change';
    prasIndeterminate:  Result := 'review: the analyzer could not decide';
  else
    Result := 'not checked: no signatures or no original bytes';
  end;
end;

O kapıdaki sıralama bilinçlidir. prrDuplicateObjectDefinition, kendisi Status'u düşürmeden risk kümesine eklenir ve parse edilemeyen bir FieldMDP transform, bir form alanı gerçekten değişene dek durumu etkilemez; yalnızca Status'e bakan bir kapı, raporun zaten içerdiği kanıtı gözden kaçırabilir. Raporun iddia etmediğini de akılda tutun. TPadesRevisionAnalysisReport, CMS imzasının kriptografik olarak geçerli olup olmadığı ya da imzalayıcı sertifikasının güvendiğiniz bir köke zincirlenip zincirlenmediği hakkında hiçbir şey söylemez. Revizyon analizi imzadan sonra ne olduğu sorusunu cevaplar ve yapısal ile güven doğrulamasının yanına gelir; onların yerine değil

İmza anında seed değerleri ve MDP kilitleri yazmak

Aynı kurallar, imzalarken TPadesSignatureFieldOptions üzerinden yazılabilir; bu, hem TPadesSignOptions'un hem TPadesRemoteSignOptions'un FieldOptions üyesidir. PDFium bir widget oluşturabilir ama /SV, /Lock, bir FieldMDP ya da DocMDP transform'u ya da katalog /Perms sözlüğünü yazamaz; component'in kendi artımlı PAdES yazıcısı bu nesneleri, imzayla aynı xref güncellemesinin içinde üretir. FieldName kök alan adını belirler, RequiredSeedValues, ISO 32000-1 §12.7.4.5'te tanımlanan seed-value sözlüğünün /Ff bitleri olur; Reasons, LegalAttestations ve AcceptableCertificates sonraki bir imzalayıcının ne seçebileceğini kısıtlar, LockAction ile LockFields dolaylı bir /SigFieldLock yazar ve 1'den 3'e CertificationPermission imzayı bir sertifikalandırma imzasına çevirir. DocMDP ve FieldMDP transform'larının ikisi de imza değeri üzerindeki tek bir /Reference dizisine girer; her biri kataloğu gösteren bir /Data taşır

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // yalnızca form doldurma ve imzalama
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // yalnızca bu alanları kilitle
  SetLength(Options.FieldOptions.LockFields, 2);
  Options.FieldOptions.LockFields[0] := 'Total';
  Options.FieldOptions.LockFields[1] := 'IBAN';
  if not Pdf.SignPades('contract-certified.pdf', Options) then
    Writeln('Signing failed');
end;

Bunu elle yazıyorsanız birkaç ayrıntıda hata etmek kolaydır. Katalog /Perms /DocMDP'si widget annotation'ını değil imza değeri sözlüğünü referans almalıdır ve yazıcı, imza değerini o yüzden kendi dolaylı nesnesi olarak tutar. Mevcut bir /Perms sözlüğü hâlihazırda /UR3 kullanım hakları taşıyor olabilir; yazıcı onu kopyalar ve yerine koymak yerine /DocMDP'yi ekler — ISO 32000-1 §12.8.4'teki permissions sözlüğüne uyarak. Hâlihazırda /DocMDP taşıyan bir belge, ikinci bir sertifikalandırma imzasını EPadesCrypto ile reddeder ve tutarsız seçenekler de öyle: alan adı olmayan bir Include ya da Exclude kilidi, alan listesiyle gelen bir All kilidi, sertifikalandırma olmayan bir imzadaki yasal attestation ya da kök alan adındaki nokta. Uzaktan imzalama bir kural daha ekler, çünkü PreparePadesRemoteSignature çalıştığında imzalama sertifikası bilinmez: orada CertificateRequired set etmek açık bir AcceptableCertificates listesi ister; yerel imzalama ise çözümlenmiş imzalayıcı sertifikasına düşebilir

Revizyon analizi, imza araç kutusunun herhangi bir parçasını değiştirmek yerine onu tamamlar. Sözlüğü ve taban PAdES düzeyini okumak için PDFium Component ile PDF imzalarını ve PAdES düzeylerini incelemeye başlayın, revizyon sorusundan önce gelen yapısal başarısızlıklar için doğrulayıcıların PAdES imzalarını neden reddettiğine bakın ve hükmü, JavaScript ile gömülü-dosya kontrollerinin yanında daha geniş bir PDF güvenlik riski denetimine katın. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions ve burada gösterilen artımlı PAdES yazıcısı, Delphi, C++Builder ve Lazarus için PDFium Component ile gelir