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

Инспектиране на PDF подписи и нива на PAdES в Delphi

Получили сте подписан PDF файл и трябва да покажете във вашия браузър кой го е подписал, кога е бил подписан, дали подписът обхваща целия файл и доколко отговаря на изискванията за дългосрочно съхранение. Компонентът PDFium Component за Delphi и Lazarus отговаря на тези четири въпроса с извиквания само за четене: FPDF_GetSignatureCount и фамилията функции FPDFSignatureObj_* разкриват речника на подписа, а TPdf.ValidatePades класифицира базовото ниво на PAdES. Това е първата от три статии за PDF подписи с PDFium; следващите две покриват създаването на B-B подпис и добавянето на дългосрочни времеви отпечатъци (timestamps). Важно разграничение обаче трябва да се направи от самото начало: всичко описано тук е само инспектиране (проверка), а четенето на твърденията на даден подпис е много различно от верифицирането на неговата криптография или вземането на решение дали имате доверие на подписалия

Защо речникът на PDF подписа не е просто набор от байтове

PDF подписът е речник, а не непрозрачен прикачен файл, и неговите два най-важни записа ви казват колко точно от файла е защитен. ISO 32000-1 §12.8 дефинира речника на подписа със записите /ByteRange и /Contents. /Contents съдържа шестнадесетично кодирана CMS SignedData структура (RFC 5652) — криптографският плик, който носи сертификата на подписалия, подписаните атрибути и самата стойност на подписа. /ByteRange е частта, която разработчиците често подценяват: това е масив от два диапазона (отместване и дължина), които заедно обхващат целия файл с изключение на шестнадесетичния низ на /Contents. Този празен интервал е точно мястото, където стоят байтовете на подписа, а двата диапазона от двете му страни са именно онова, което подписът валидира

Дизайнът на ByteRange е това, което прави инкременталния запис проверяем. Тъй като подписващият не може да хешира байтове от подпис, които все още не съществуват, файлът се разделя около контейнера-заместител /Contents и всичко останало се хешира в подписа. Подпис, чийто ByteRange не достига до края на файла, е предупредителен сигнал: съдържание, добавено след обхванатия диапазон чрез последващо инкрементално обновяване, няма да наруши подписа, въпреки че променя онова, което потребителят вижда. Така че първото нещо, което сериозният инспектор проверява, не е кой е подписал, а дали подписът покрива всички байтове, които твърди, че подкрепя

Как се чете речникът на подписа с API-то на PDFium само за четене

Компонентът PDFium показва речника на подписа чрез два члена само за четене: SignatureCount и записа Signature[Index]. Под капака те извикват FPDF_GetSignatureCount, FPDF_GetSignatureObject и методите за достъп FPDFSignatureObj_* за /SubFilter, /ByteRange, /Contents, /Reason и времето на подписване. „Само за четене“ е важно уточнение: PDFium може да изброява и чете подписи, но няма API за създаване или записване на такива, поради което подписващата страна в тази поредица от статии се имплементира от самата библиотека, а не от PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // низ с дата от подписалия, напр. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // необработени DER SignedData, взети от /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = няма сертификация, 1..3 = MDP ниво
    end;
  finally
    Pdf.Free;
  end;
end;

Всеки запис TPdfSignature се съпоставя директно с речника. Encoding е стойността на /SubFilter — най-важното диагностично поле, тъй като описва обработчика на подписа и веднага разграничава модерен ETSI.CAdES.detached подпис от остарял или забранен такъв. Time е декларираното от автора време на подписване като PDF низ за дата, което представлява твърдение на подписалия, а не доверено време. Content съдържа суровите CMS SignedData данни, а Permission показва нивото на сертификация DocMDP (0 за обикновен одобрителен подпис, от 1 до 3 за сертификационен подпис, който заключва последващи промени). Единственото поле, което записът не излага, е анализираният ByteRange, и този пропуск е съзнателен, тъй като ValidatePades извършва изчислението на обхвата на ByteRange вместо вас, спестявайки ви ръчни сметки

Каква е разликата между PAdES B-B, B-T, B-LT и B-LTA?

Четирите базови нива на PAdES формират стълбица от минимално валиден подпис до такъв, създаден да издържи десетилетия архивиране, като всяко ниво строго включва нивата под него. ETSI EN 319 142-1 ги дефинира като B-B, B-T, B-LT и B-LTA. B-B (Basic) е самият подпис плюс задължителните подписани атрибути и нищо повече. B-T (Timestamp) добавя доверен времеви отпечатък по RFC 3161 над подписа, така че моментът на подписване се удостоверява от орган за времеви отпечатъци (TSA), а не просто се твърди от часовника на подписалия. B-LT (Long-Term) вгражда данните за валидиране — веригата от сертификати и незадължително OCSP или CRL отговори — вътре във файла, така че подписът да може да бъде валидиран години по-късно, когато издаващата инфраструктура вече не съществува. B-LTA (Long-Term с архивен времеви отпечатък) обвива времевия отпечатък на документа около тези данни, защитавайки самите дългосрочни данни и ви давайки възможност за повторно подпечатване, преди базовата криптография да е остаряла

Практическото разбиране се отнася до времевия хоризонт. Подпис B-B отговаря на въпроса „подписал ли е някой това“. B-T добавя отговор на въпроса „и кога, доказуемо“. B-LT отговаря на „и мога ли все още да го проверя, след като сертификатите изтекат“. B-LTA отговаря на „и дали тази проверка ще бъде валидна след двадесет години“. Регулаторните профили избират съответното ниво: много контексти на електронно фактуриране и eIDAS изискват поне B-T, а изискванията за архивиране се насочват към B-LT или B-LTA. Знанието кое ниво реално достига даден документ, преди да го приемете или отхвърлите, е основната цел на стъпката по инспектиране

Откриване на базовото ниво с TPdf.ValidatePades

Компонентът PDFium свежда целия въпрос за нивото до едно извикване. TPdf.ValidatePades връща запис TPadesValidationResult, чието поле Level е TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT или plB_LTA — наред с набор от проблеми (issues), брой подписи и брой времеви отпечатъци на документа. Нивото се определя последователно: валидаторът първо установява B-B, след това повишава до B-T, ако присъства времеви отпечатък на подписа или на документа, до B-LT, ако каталогът съдържа /DSS с сертификати и маркера /Extensions /ESIC Level 1, и до B-LTA, ако времеви отпечатък на документа и маркерът ESIC Level 2 присъстват едновременно. Два помощни метода правят резултата използваем: IsCompliant е True само когато нивото достигне поне B-B и списъкът с проблеми е празен, а IsCompliantAt ви позволява да наложите изискване за минимално ниво, като например plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Защо adbe.pkcs7.sha1 е забранен SubFilter?

Тъй като SHA-1 е компрометиран и обработчикът adbe.pkcs7.sha1 го използва твърдо кодиран. Този SubFilter предварително хешира документа с SHA-1, преди да го обвие в PKCS#7, а SHA-1 е уязвим за колизии от години, поради което EN 319 142-1 клауза 6.3 го забранява изцяло за базови подписи. ValidatePades задейства проблема ppeiForbiddenSubFilter, когато открие adbe.pkcs7.sha1 или adbe.x509.rsa_sha1, и ppeiBadDigestAlgorithm, когато самата CMS система използва MD5 или SHA-1 за извличане на съобщението (клауза 6.2.1). Това са две отделни проверки, улавящи един и същ клас слабост на две различни нива

Наборът от проблеми съдържа общо 26 елемента, а тези, с които ще се срещате най-често, са свързани със структурата и обхвата на покритие. ppeiByteRangeNotCoveringFile е проверката за покритие, описана по-горе. ppeiForbiddenCertKey се задейства, когато речникът на подписа съдържа запис /Cert, което PAdES забранява, тъй като веригата трябва да се съхранява в CMS структурата SignedData.certificates. ppeiMissingSigningCertificate, ppeiMissingContentType и ppeiMissingMessageDigest сигнализират за липса на задължителни подписани атрибути, а ppeiDetachedContentViolation улавя подпис, който погрешно вгражда подписаното съдържание, вместо да го отдели. Изброяването на набора превръща обикновеното отхвърляне в диагноза, която можете да запишете в лог файл

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    // цикъл за обхождане на проблемите:
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Какво ValidatePades не проверява

ValidatePades проверява структурата, а не доверието, и смесването на двете е опасна грешка. Резултат plB_LTA означава, че документът съдържа правилно оформен B-LTA подпис с всички задължителни атрибути, материали и времеви отпечатъци на правилните места — това не означава, че подписът е криптографски валиден, че веригата от сертификати достига до доверен корен или че никой сертификат във веригата не е оттеглен. Валидаторът умишлено не извършва криптографска проверка: той не изчислява отново подписа над диапазона ByteRange, не изгражда и не оценява веригата на доверие и не проверява статуса на оттегляне чрез OCSP или CRL. Това разделение е съзнателно и полезно, тъй като структурната проверка е бърза, напълно детерминирана и не изисква ключове, мрежова връзка или системни криптографски услуги. Поради това ValidatePades работи идентично на Windows, Linux и macOS като чист Pascal код над байтовете на файла. Верифицирането на веригата на доверие, напротив, е тясно свързано с конкретната политика — кои корени считате за доверени, как изтегляте данните за оттегляне, какъв е толерансът за времеви отпечатъци — и принадлежи на по-късен етап, зависещ от системното хранилище за сертификати. Затова приемете успешното преминаване на ValidatePades като задължително условие, че подписът е оформен правилно, и след това предайте структурно изправния подпис за реална криптографска проверка, преди да му се доверите

Този структурен пас е правилното място за начало и се съчетава естествено с по-широките проверки само за четене, описани в статията за одит на рисковете за сигурността на PDF с компонента PDFium и с дейности по съответствие на формата като валидиране на готови за печат PDF/X документи. След като можете да четете и класифицирате даден подпис, следващата стъпка е създаването на такъв: втората статия в тази поредица описва подписването на PDF файлове с PAdES B-B, а третата го разширява до доверени времеви отпечатъци и дългосрочни подписи B-LT и B-LTA. Инспектирането на подписи само за четене и класификаторът ValidatePades, показани тук, са част от компонента PDFium Component за Delphi, C++Builder и Lazarus