Техническа статия

DocMDP и FieldMDP: одит на PDF ревизии в Delphi

Подписан PDF, който се е променил след подписването, не е автоматично счупен. ISO 32000-1 позволява инкрементални обновявания върху подпис, и само някои от тях нарушават политиката, зададена от подписващия. HotPDF Component за Delphi и C++Builder отговаря на този въпрос с AnalyzeLoadedSignatureRevisions, който класифицира всяка ревизия след подписването и я оценява спрямо DocMDP и FieldMDP. Сценарият е познат на всеки, доставящ софтуер за договори: клиентът ви подписва договор за покупка, изпраща го и получава обратно версия с прикачена приложна страница. Четецът показва жълта лента, казваща, че подписът е непокътнат, но документът е бил променен след подписването, и никой в стаята не може да каже дали това е нормален процес на съвместно подписване, или някой тихо редактира подписан договор

Какво се брои за законна промяна след подписване?

Промяна е законна, когато нейната смислова категория попада в правото, декларирано от заверяващия подпис. ISO 32000-1 §12.8.2.2 дефинира трансформацията DocMDP със стойност /P 1, 2 или 3: 1 не позволява никакви промени, 2 позволява попълване на формуляр и подписване, 3 позволява попълване на формуляр, подписване и анотации. HotPDF излага тях като стойности на THPDFDocMDPPermission: dmpNoChanges, dmpFormFillAndSign и dmpFormFillSignAndAnnotate, като dmpNone е запазено за резултати от проверка, които не носят никаква трансформация DocMDP

Категориите са подредени, и тази подредба е двигателят на цялата проверка. THPDFRevisionModificationLevel протича rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, нарочно подредени така, че по-голяма поредна стойност никога не е по-малко ограничителна. Целият документ се свежда до максималното ниво, наблюдавано през всички ревизии след подписа, и сравнението с DocMDP се превръща в единствен тест с цяло число. Един нюанс има значение рано: при dmpNoChanges анализът все още приема rmlLongTermValidation. Добавянето на DSS и VRI материал за валидация или времеви печат на документ към заверен файл е поддръжка на подписа, не модификация на документа, а третирането му като нарушение би счупило всеки работен процес за дългосрочно архивиране, който съществува

Как HotPDF възстановява веригата от ревизии?

Структурно, не евристично. Според ISO 32000-1 §7.5.6 инкременталното обновяване добавя нов раздел с кръстосани референции, чийто /Prev сочи към предишния, така че HotPDF чете startxref от края, разбира раздела там, следва /Prev назад и повтаря, връщайки разделите от най-стария нататък. Два предпазни лимита седят в този цикъл и и двата си струва да се знаят при триаж на файл, който се проваля: /Prev, сочещ към офсет, вече посетен, прекратява обхождането с изричен диагностик за цикъл вместо да зацикли, а верига по-дълга от хиляда ревизии се отхвърля направо. Двата се показват в Analysis.Issue, като функцията връща False, и никой от тях не бива да се прикрива, защото цикличен /Prev е повреден или враждебен файл, а не необичаен

Четири исторически форми се срещат в реални документи и всичките четири се обработват: традиционни таблици xref, разбирани ред по ред, потоци за кръстосани референции, декомпресирани и декодирани чрез полетата им /W и /Index, хибридни файлове с референции, чийто традиционен trailer носи ключ /XRefStm, който бива разбран и слят в същата ревизия (случаят с продуцента Office, разгледан в статията за хибридни потоци за кръстосани референции), и обекти, живеещи вътре в контейнер ObjStm, които имат значение, защото модерно обновяване обикновено слага променения речник в компресиран поток, вместо да го записва директно, както е описано в материала за обектни потоци и инкрементални обновявания. Подписът закотвя разделянето: /ByteRange[2] + /ByteRange[3] се превръща в SignedRevisionLength, а всеки раздел на или след този офсет е след подписването. Дали байтовият диапазон все още хешира правилно е отделен въпрос, отговорен от VerifyLoadedSignature и разгледан в статията за проверка на цифрови подписи в PDF

Как всеки променен обект бива класифициран

Класификацията протича по обект, после се разпространява по референциите. За всеки номер на обект, докосван от раздел след подписването, HotPDF чете новото тяло и тялото, каквото е стояло в подписаната снимка; идентично тяло е rmlNone, защото продуцентите наистина презаписват обекти, без да ги променят. Разпознавачите са нарочно тесни. Обект /Type /DocTimeStamp, или такъв, чийто /SubFilter е ETSI.RFC3161, е rmlLongTermValidation, както и всичко достижимо от дървото /DSS на каталога; речник /Type /Sig е rmlFormFillAndSign. За контейнерите тестът е кои ключове са се преместили, не какъв е обектът: каталогът може само да добие или промени /DSS, /Extensions или /AcroForm; речникът AcroForm — само /Fields, /SigFlags, /NeedAppearances, /DR, /DA или /Q; страница — само /Annots; поле или уиджет — само /V, /AP, /AS или /M. Всичко извън тези множества пада до rmlOther, което е точно как приложната страница бива уловена: добавянето на страница пренарежда дървото на страниците по начини, които никой списък с разрешени не покрива, и никакво количество законно попълване на формуляр не му прилича

После нивата се разпространяват, като всеки контейнер наследява максималното ниво на променените деца, към които сочи, повтаряно докато присвояването се стабилизира. Точно това кара потоците за визуален изглед да работят. Попълнено текстово поле презаписва /V и сочи към нов поток /AP, а този поток сам по себе си е анонимен блоб от оператори за съдържание без тип за разпознаване; защото полето, което го притежава, е rmlFormFillAndSign, потокът наследява същото ниво, вместо да пада до rmlOther. Същото разпространение носи контекста на DSS върху потоци със сертификати и анулиране, които иначе биха били некласифицируеми

Защо нечетим обект се брои за нарушение?

Защото алтернативата е валидатор, победен чрез записване на нещо, което не разбира. Три ситуации завършват на rmlOther без обжалване в HotPDF: обект, чието тяло не е могло да бъде прочетено от ревизията, обект, който ревизията маркира като освободен, и обект, несъответстващ на нито един от разпознавачите по-горе. Всяка записва конкретен диагностик в полето Issue на ревизията, така че оператор може да види кой номер на обект е произвел присъдата

Освобождаването е най-острото от трите. Ревизия след подписването, маркираща предварително дефиниран обект като свободен, е изтрила съдържание от подписан документ, и никое ниво на права по §12.8.2.2 не позволява това; номерата на обектите се озовават в FreedObjectNumbers и ревизията се повдига до rmlOther. Нечетимите обекти следват същата логика по различна причина. Валидатор, който не може да разбере обект, няма основание да го нарече безобиден, а честният отговор на това не е мълчание. Отчитането на необичайна, но безобидна конструкция като нарушение струва човешки преглед; обратната грешка доставя подписан договор с незабелязана редакция вътре

Четене на присъдата в Delphi

Извикването е кратко. Заредете документа, изберете индекс на подпис, прочетете записа; безпараметровата претоварена версия отваря отново файла, от който документът е зареден, а претоварената версия с TStream взема байтове, подадени от извикващия, и възстановява позицията в потока преди да върне резултат. PolicyCompliant е единственият булев резултат, който повечето извикващи искат, комбиниращ три независими решения: структурната валидност на речниците с права, DocMDPCompliant и FieldMDPCompliant. Дръжте компонентите видими в потребителския си интерфейс, вместо да ги свивате, и отбележете, че документ без трансформация DocMDP оставя DocMDPCompliant на True, тъй като обикновен одобряващ подпис не декларира политика, която да бъде нарушена, и обобщеното ModificationLevel тогава е описателно, а не присъда

var
  Pdf: THotPDF;
  Analysis: THPDFSignatureRevisionAnalysis;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
    begin
      if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
      begin
        if Analysis.PolicyCompliant then
          Writeln('Post-signature changes stay inside the signing policy')
        else
          Writeln('Policy violation: ', string(Analysis.Issue));
      end
      else
        Writeln('Analysis could not run: ', string(Analysis.Issue));
    end;
  finally
    Pdf.Free;
  end;
end;

За триаж обикновено искате разбивката по ревизия, вместо обобщението, защото тя казва кога в историята на документа нещата са тръгнали накриво. Всеки запис в Analysis.Revisions носи своя индекс във веригата, офсета на кръстосаните референции, на който е записан, собственото си ниво на модификация и засегнатите номера на обекти

const
  LevelNames: array[THPDFRevisionModificationLevel] of string =
    ('none', 'long-term validation', 'form fill and sign',
     'annotations', 'other');
var
  I: Integer;
begin
  Writeln(Format('%d revisions in chain, signature sits at index %d',
    [Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
  for I := 0 to High(Analysis.Revisions) do
    Writeln(Format('  rev %d at offset %d: %s (%d changed, %d freed) %s',
      [Analysis.Revisions[I].RevisionIndex,
       Analysis.Revisions[I].XRefOffset,
       LevelNames[Analysis.Revisions[I].ModificationLevel],
       Length(Analysis.Revisions[I].ChangedObjectNumbers),
       Length(Analysis.Revisions[I].FreedObjectNumbers),
       string(Analysis.Revisions[I].Issue)]));
end;

FieldMDP се съди отделно, и това е умишлено

Документ може да удовлетворява DocMDP и пак да е незаконен, затова FieldMDPCompliant е отделен булев резултат, вместо да е сгънат в сравнението по ниво. ISO 32000-1 §12.8.2.4 дефинира трансформацията FieldMDP, а §12.7.5.5 — свързания запис /SigFieldLock, за да замразят именувани полета на формуляр в момента на подписването, дори когато документът като цяло все още позволява попълване на формуляр. Попълването на поле е действие от ниво 2; попълването на поле, заключено от подписващия, е нарушение независимо от нивото. HotPDF чете обхвата в THPDFFieldLockAction като flaAll, flaInclude или flaExclude, като flaNone е за резултати без политика за заключване, а имената — в Permissions.FieldNames: flaAll заключва всичко, flaInclude заключва изброените имена, flaExclude заключва всичко освен тях. Една подробност има значение при четене на резултатите, тъй като само полета, вече присъстващи в подписаната снимка, се отчитат в ChangedFieldNames, защото поле, създадено изцяло след подписването, няма подписано състояние, на което да противоречи, и бива уловено вместо това от пътя на DocMDP

var
  Source: TFileStream;
  Analysis: THPDFSignatureRevisionAnalysis;
  I: Integer;
begin
  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
      if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
        for I := 0 to High(Analysis.ChangedFieldNames) do
          Writeln('modified after locking: ',
            string(Analysis.ChangedFieldNames[I]));
  finally
    Source.Free;  // stream position was restored before the call returned
  end;
end;

Какво този анализ няма да ви каже

Той не верифицира подпис. AnalyzeLoadedSignatureRevisions разсъждава за структура и права; дали подписаният байтов диапазон все още хешира до стойността в CMS blob-а, и дали сертификатът на подписващия се верижи до нещо, на което се доверявате, се отговарят от VerifyLoadedSignature и VerifyLoadedSignatureWithTrust. Файл може да бъде напълно съответстващ на политиката и криптографски безполезен, така че двете проверки принадлежат една до друга във всяка реална врата за приемане. Той също не чете намерение вътре в потоците със съдържание: страница, чийто поток със съдържание е бил заменен изцяло, се улавя като промяна извън списъка с разрешени, но анализът няма да ви каже, че замяната е разменила сума за плащане. Присъда rmlOther означава, че човек трябва да погледне, не че е извършена измама, а съответстваща присъда означава, че промяната пасва на разрешена категория, не че промяната е била желана. Когато всичко, което ви трябва, е това, което подписващият е декларирал, без обхождането на ревизиите, GetLoadedSignaturePermissions връща речниците с политика самостоятелно

Всичко описано тук работи нативно в Delphi и C++Builder без външна услуга за подписване в цикъла, което го прави практично да се изпълнява при всеки входящ документ, а не само при онези, които някой вече е заподозрял. Пълното API за подписи и ревизии, включително методите за права и верификация, с които се съчетава, е част от HotPDF Component за Delphi и C++Builder