Валидатор отхвърля PAdES подпис по една от три основни причини в почти всеки случай, който сме дебъгвали с PDFium Component в Delphi: масивът /ByteRange все още съдържа своите подразбиращи се нули, времето за подписване /M не е коректно дефиниран низ за дата в PDF, или инкрементна актуализация е премахнала записа /Encrypt от криптиран документ. И трите проблема водят до генериране на файлове, които се отварят и изглеждат напълно нормално, но се отхвърлят в момента, в който съвместим валидатор прочете речника на подписите
Сценарият, който илюстрира тази статия, е съвсем конкретен. Подписвате договор по начина, описан в статията за PAdES B-B подписване, вашият собствен тестов код показва, че подписът е наличен и структурно коректен, всички локални тестове преминават успешно – а след това отсрещната страна качва файла в своята платформа за валидиране и получава червен статус за грешка. Нищо от този проблем не се вижда в четец на файлове, тъй като никой от тези три дефекта не засяга визуалното съдържание на страницата. Те съществуват изцяло в речника на подписите и в края на файла, където валидаторите проверяват, а четците на документи обикновено не го правят
Защо стойността ByteRange на моя PDF подпис е невалидна?
Отхвърлянето на ByteRange почти винаги означава, че четирите полета никога не са били попълнени, а не че диапазонът е леко неточен. Масивът /ByteRange [A B C D] декларира два сегмента – от байт A до A+B и от байт C до C+D, и стандартът EN 319 142-1 §6.3 (изискване k) изисква те заедно да обхващат целия файл, с изключение на шестнадесетичния низ в /Contents. Изразено в числа: A е 0, C >= A+B, C+D е равно на дължината на файла, а празнината между B и C съдържа точно низа <...> – два байта за скобите плюс по два шестнадесетични символа за всеки байт CMS данни. Валидатор, който прочете [0 0 0 0], заключава, че подписът не обхваща нищо и го отхвърля, независимо колко коректна е структурата на CMS вътре в /Contents
Механиката на този процес си струва да бъде разбрана, тъй като същият модел се среща във всяко решение за подписване. Програмата не може да знае финалните отмествания, докато файлът не бъде структуриран, така че тя записва масива с подразбиращи се нули и ги попълва впоследствие. Във версиите на PDFium Component преди 2.14.1 това попълване търсеше нулите от позицията на /Contents нататък – но /ByteRange се намира преди /Contents в речника, така че търсенето не откриваше нищо и замяната приключваше неуспешно без грешка. CMS хешът се изчисляваше върху правилните диапазони, така че криптографията беше коректна, но декларирането на диапазоните оставаше на нула, поради което всеки съвместим валидатор отхвърляше файла. Времевите маркери на документи по PAdES B-LTA, които използват същия модел, се проваляха по идентичен начин – важен факт, ако работите с дългосрочни подписи B-LT and B-LTA. Версия 2.14.1 извършва попълването от началото на обекта на подписа, а корекцията е тествана с регресионен тест, който анализира генерирания файл и потвърждава аритметиката по-долу
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Двадесет реда чист код, без външни библиотеки, и той засича целия клас грешки в момента на подписване. Ако решите да запазите само една проверка от тази статия, използвайте тази: анализирайте резултатите и проверете тези три равенства преди запис
Защо валидаторите докладват времето за подписване /M като некоректно?
Строгите валидатори отхвърлят време за подписване, което не е пълен низ за дата в PDF, като двата елемента, които най-често липсват, са префиксът D: и маркерът за UTC отместване. Стандартът ISO 32000-1 §7.9.4 дефинира формата за дата като D:YYYYMMDDHHmmSS, последвано от отместване – Z за UTC или стойност +HH'mm'. Обикновен низ 20260709143000 се разчита като дата от толерантни четци, но валидатор, следващ граматиката буквално, вижда некоректен низ в задължително поле и маркира подписа като невалиден. Версиите на PDFium Component преди 2.14.4 записваха стойността /M в методите TPdf.SignPades и SignPadesBytes в тази съкратена форма; от версия 2.14.4 насам записът съдържа префикса D: и маркера Z, така че декларираното време изглежда като D:20260709143000Z
Две важни бележки важат за това поле. Първо, записвайте времето в UTC и го указвайте изрично: времеви маркер без индикатор за отместване принуждава валидатора да отгатва часовата зона, а §7.9.4 третира отместването като част от формата, а не като незадължителна настройка. Второ, помнете какво представлява /M – декларираното от подписващия време, което е просто твърдение, а не доказателство. Валидаторът проверява само неговия формат, а не неговата достоверност; достоверно време се осигурява от времеви маркер по RFC 3161 при PAdES B-T и по-високи нива. Правилното форматиране на полето и доверието в него са различни понятия, и валидаторите проверяват само първото
Защо методът SignPades генерира изключение EPadesCrypto при криптиран PDF?
Компонентът PDFium Component отказва да подпише криптиран документ, тъй като алтернативата е генериране на файл, който съвместимите четци ще повредят при отваряне. PAdES подписът се добавя като инкрементна актуализация, а стандартът ISO 32000-1 §7.5.6 изисква новият трейлър (trailer) на актуализираната секция да съдържа всеки запис от предходния трейлър с изключение на /Prev – което в криптиран документ включва и /Encrypt. Ако го пропуснете, новият трейлър ще декларира, че файлът е декриптиран, и тогава съвместим четец ще анализира криптираното тяло като чист текст, получавайки некоректни данни. Нещо повече – добавените от подписващия потоци и низове трябва също да бъдат криптирани с ключа на документа, което обикновен инжектор на подписи не може да направи. Тъй като няма начин да се добави валиден подпис в чист текст към криптиран файл, от версия 2.14.2 методите TPdf.SignPades, SignPadesBytes и InjectPadesDssMarkers генерират изключение EPadesCrypto, вместо да запишат повреден резултат
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
Генерирането на изключение е правилният резултат, затова изграждайте логиката спрямо него, вместо да го пренебрегвате. Първо декриптирайте файла, подпишете го в декриптиран вид, и ако защитата изисква шифриране, имайте предвид, че криптирането преди и след подписване генерира различни файлове. Подпис, генериран върху декриптирани данни, не може да съществува при последващо шифриране на файла, така че решенията са две – подписване на чист текст или изрично документиране на избора в коментарите на кода
Как две грешки могат да се маскират една друга
Дефектът с ByteRange остана скрит дълго време, тъй като нашият собствен валидатор съдържаше съответстваща грешка, което е ценен урок. Проверката за обхват в ValidatePadesCompliance изискваше втория сегмент да започва точно от A+B – нулев интервал – което погрешно класифицира стандартното оформление, при което интервалът съдържа шестнадесетичния низ /Contents. По този начин вътрешният валидатор отхвърляше именно коректното оформление, а вътрешният генератор никога не го произвеждаше, и автоматизираните тестове оставаха зелени. Всяка от грешките лишаваше тестовия пакет от примери, които биха разкрили другата. Версия 2.14.1 коригира и двете страни: генераторът попълва и четирите полета, а валидаторът приема C >= A+B, като C+D съответства на общата дължина на файла
Методологичното решение е извършване на кръстосана проверка (cross-validation) спрямо външна имплементация. Генератор и валидатор, които споделят обща кодова база, автор или логика на работа, могат да поддържат едно и също грешно разбиране неограничено време; независим валидатор прекъсва тази зависимост. Тествайте генерираните файлове с поне един външен инструмент за съответствие преди пускането им в експлоатация и поддържайте бърза структурна проверка в проекта – методът TPdf.ValidatePades, описан в статията за инспектиране на подписи, докладва проблемите с обхвата като конкретно събитие
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
Списък за проверка на подписани файлове
Когато външна платформа отхвърли вашия PAdES подпис, първо проверете структурните причини, преди да търсите проблеми в сертификатите или веригите на доверие. Прочетете масива /ByteRange и проверете трите равенства: първо поле нула, вторият сегмент започва при или след края на първия и вторият сегмент завършва точно в края на файла. Проверете записа /M и се уверете, че е пълен низ за дата съгласно §7.9.4 с префикс D: и маркер за отместване. Уверете се, че изходният документ не е бил криптиран при добавяне на подписа – и ако системата ви го е подписала без предупреждение, разглеждайте това поведение като грешка. И трите проверки се извършват върху чисти байтове за милисекунди и според нашия опит обясняват отхвърлянето много по-често от коя да е криптографска причина
Методите за подписване, инспектиране и валидиране, показани тук – SignPades, ValidatePades и проверките за съответствие с техните корекции за ByteRange, дати и обработка на шифриране – се доставят с PDFium Component за Delphi, C++Builder и Lazarus