Вирішити, що підпис PDF є кваліфікованим за eIDAS, означає відповісти на питання, що не має нічого спільного з криптографією: чи видав сертифікат сервіс довіри, якого держава-член занесла до списку як кваліфікований, на момент, коли підпис було зроблено. Відповідь живе в довіреному списку — XML-документі, опублікованому на територію, — і вся цінність того документа залежить від його справжності. Тож компонент PDFium відмовляється зазирати всередину, доки хтось не поручився за нього. TPdfEuropeanTrustedList.ParseAuthenticated передає повні сирі байти постаченому викликачем IPdfTrustedListAuthenticator до того, як розбирає хоч один сервіс, і творить знімок лише тоді, коли той автентикатор явно проходить
Той порядок — це конструкція. Все інше в цій можливості випливає з нього, включно з частинами, що виглядають незручними
Розібране не є довіреним
Довірений список, що розібрався чисто, каже вам, що XML добре сформований. Він не каже нічого про те, хто його написав. Оскільки список — те, на чому тримається все ваше рішення про кваліфікований статус, прийняття його за те, що він розібрався, зробило б рішення беззмістовним: зловмисник, що може підмінити список, може оголосити кваліфікованим власний центр сертифікації
Та сама логіка стосується кешування, і це пастка, варта назви. Кеш знімків зберігає оригінальний XML разом із дайджестом SHA-256, і було б легко трактувати збіжний дайджест при завантаженні як доказ, що список справжній. Це не так. Дайджест, обчислений тим самим процесом, що зберіг файл, без жодного ключа, перевіряє лише те, що байти не змінилися з моменту запису; якщо список був шахрайським у момент кешування, дайджест підтверджує, що це той самий шахрайський список. Тож завантаження кешованого знімка йде через той самий автентикатор, що й розбір свіжого. Цілісність і справжність — різні властивості, і лише одній з них потрібен ключ
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// Ваша політика живе тут: перевірте enveloped-підпис XMLDSIG
// проти сертифіката підписування списку, який ви закріпили
// незалежним каналом, і опишіть, що ви перевірили, для сліду аудиту
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// Знімок існує лише тому, що автентикатор сказав так
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
Валідатор не володіє мережевою політикою
Валідатору PAdES немає діла вирішувати, як дістатися до списку списків довіри, чи йти через проксі, як часто повторювати чи що робити, коли територія недосяжна. Це рішення застосунку та розгортання, і в регульованих середовищах їх часто аудитують. Тож оновлення приходять через IPdfTrustedListSource, якому передають URI та стелю байтів і який повертає байти
Те, що компонент справді примушує, — інваріанти, що роблять оновлення оновленням, а не підміною. Update вимагає, щоб територія не змінювалася, щоб порядковий номер строго зростав і щоб час випуску не рухався назад. Ті три перевірки розбивають найочевидніші атаки зниження: повторення старішого списку, що досі вносить сервіс, відкликаний відтоді, чи підміну списком іншої території, чиї сервіси ви й не мали на увазі довіряти
Межі парсера і жодного DTD взагалі
TPdfTrustedListOptions обмежує розмір XML, кількість токенів, глибину вкладення, кількість сервісів, кількість сертифікатів та розмір окремого сертифіката, а функція класу Default дає придатні значення. Довірені списки — опубліковані документи передбачуваного розміру, тож межі дешево встановити, і немає законного списку, якому треба їх перевищувати
Окремо й безумовно парсер відкидає оголошення DTD та сутностей. Це закриває і відмову в обслуговуванні розширенням сутностей, і маршрут розкриття зовнішніх сутностей однією відмовою, і нічого не коштує, бо довірені списки не вживають сутностей. Будь-який XML-парсер, досяжний з недовіреного входу, варто налаштовувати так; різниця тут у тому, що відмова не налаштовувана, тож її не можна вимкнути доброзичливою зміною опції
Кваліфікований статус записується поруч із довірою ланцюга, а не зливається з нею
Сторона оцінювання свідомо окрема. TPadesTrustValidationOptions.QualifiedTrustEvaluator приймає IPdfQualifiedTrustEvaluator, який реалізує знімок довіреного списку. Під час валідації оцінювач отримує листовий сертифікат, ланцюг і час валідації, зіставляє сертифікати сервісів точним порівнянням DER проти підписувача та ланцюга, поєднує статус сервісу, ідентифікатор типу сервісу та URI кваліфікаторів на той момент часу і повертає запис оцінювання
Результат сідає в два місця на кожному підписі: QualifiedTrustStatus як грубий статус і QualifiedTrust як повне оцінювання з територією, назвою постачальника, назвою сервісу, ідентифікатором типу, статусом і часом початку статусу. Чого воно не робить — не змінює CertificateTrustStatus. Системна довіра ланцюга та кваліфікований статус відповідають на різні питання, і звіт, що зливає їх, не може розрізнити «довірений, але не кваліфікований» від «кваліфікований, але ланцюг не валідується», — обидва реальні, і обидва потребують різної обробки
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // автентифікований знімок
Options.QualifiedValidationTime := SigningTime; // не поточний момент
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// Немає збіжного сервісу, або знімок не може відповісти за цей час
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
Чому час валідації — не зараз
Бо кваліфікація — властивість моменту. Сервіс довіри може отримати кваліфікований статус, пізніше втратити його, а ще пізніше бути поновленим, і кожен із тих переходів несе час початку в списку. Підпис, зроблений поки сервіс був кваліфікованим, лишається кваліфікованим і після; підпис, зроблений до надання, не стає кваліфікованим ретроактивно. Оцінювання проти поточного часу тому дає неправильну відповідь в обидва боки
Список несе те, що для цього потрібно: кожен запис сервісу має час початку статусу та прапорець, що розрізняє історичні записи від поточних, а оцінювач поєднує їх проти часу, який ви постачаєте. На практиці той час приходить із довіреного часового штампа на підписі, а не з заявленого в CMS часу підписування, — тому матеріал довгострокової валідації важить навіть для питання, що виглядає як пошук політики; сторону часового штампа та DSS охоплено в статті про довгострокові підписи
Що вам досі треба побудувати
Три речі, і жодне з них не належить PDF-бібліотеці. Автентикатор, тобто справжня перевірка XMLDSIG проти сертифіката підписування списку, який ви отримали каналом, якому довіряєте. Політика отримання, тобто як і як часто ви поновлюєте і що робить ваш застосунок, коли поновлення зазнає невдачі. І територіальне охоплення, тобто які списки ви взагалі носите, — ділове рішення про те, у яких державах-членах підписують ваші контрагенти
Що ви отримуєте від компонента — та частина, яку легко зробити тонко неправильною: порядок автентикації перед розбором, обмежений і вільний від сутностей розбір XML, монотонні інваріанти оновлень, точне DER-зіставлення сервісів, історичне оцінювання статусу і результат, що лишається окремим від звичайної довіри ланцюга. Якщо ваша безпосередня проблема простіша — валідатор відкидає підпис, який ви вважаєте чинним, — звичайні причини занесені до каталогу в чому валідатори відкидають підписи PAdES, а поверхня огляду підписів описана в огляді підписів і рівнів PAdES. Можливості компонента наведені на сторінці продукту PDFium Delphi component