Технічна стаття

Цифрові підписи PAdES у Delphi: підписання та валідація через PDF Library for Delphi

Перевірити один підпис PAdES означає перевірити три незалежні речі, а зелена галочка у переглядачі говорить лише про третю з них. По-перше, масив /ByteRange має покривати правильні байти: названі в ньому проміжки мусять відтворювати саме той вхід, з якого було взято дайджест CMS, і жодного підписаного байта не має лишитися поза ними. По-друге, сертифікат усередині CMS має вибудовувати ланцюжок до кореня, якому ви довіряєте, і нести підписаний атрибут signing-certificate, якого вимагає PAdES. По-третє, якщо профіль заявляє штамп часу, токен RFC 3161 має прив'язати значення підпису до моменту в часі, що передує завершенню строку дії сертифіката. Acrobat згортає всі три в одну піктограму; перевіряч відповідності тримає їх окремо, і так само має робити код, який виробляє ці файли. losLab PDF Library (PDF Library for Delphi) дає вам бік підписання, повторне вбудовування штампа часу і виклики аудиту, щоб оглянути ByteRange, перш ніж йому довіритися

Одна відмінність збиває з пантелику майже кожну першу реалізацію PAdES, тож її варто назвати ще до будь-якого коду. Підпис, записаний із /SubFilter /adbe.pkcs7.detached, є цілком справним підписом за ISO 32000-1 §12.8, який Acrobat покаже як дійсний. Він також не є підписом PAdES, бо ETSI EN 319 142-1 вимагає ETSI.CAdES.detached на кожному базовому рівні. Перевіряч відповідності eIDAS відхиляє перший і приймає другий, хоча криптографія в них однакова. Профіль — це твердження, яке документ робить про самого себе, і правильно висловити це твердження в PDF Library for Delphi коштує одного виклику

Що перетворює підпис PDF на підпис PAdES

ETSI EN 319 142-1 визначає чотири базові рівні, надбудовані над форматом CMS. PAdES-B-B — це точка входу: підпис CAdES у полі підпису PDF із SubFilter ETSI.CAdES.detached та підписаним атрибутом signing-certificate. PAdES-B-T додає штамп часу RFC 3161 над значенням підпису, доводячи, що підпис існував до моменту, який ніхто не може датувати заднім числом. PAdES-B-LT вбудовує сертифікати, CRL та відповіді OCSP, потрібні для перевірки, у Document Security Store, тож файл лишається придатним до перевірки й після того, як центр сертифікації згорне свою інфраструктуру. PAdES-B-LTA вінчає цю піраміду штампом часу документа, який заново захищає накопичені докази в міру ослаблення алгоритмів

PDF Library for Delphi відображає ці поняття на свій API процесу підписання. Маркером профілю є SetSignProcessCustomSubFilter. Якщо ваша політика потребує зазначення типу зобов'язання (підтвердження походження, підтвердження схвалення або один з інших ідентифікаторів ETSI під номерами від 1 до 6), це йде через SetSignProcessCommitmentType. Явна політика підпису приєднується через SetSignProcessSignaturePolicy, який приймає OID політики та її дайджест. Один типовий параметр заслуговує на увагу: якщо алгоритм дайджесту лишити в автоматичному режимі, бібліотека обирає SHA-256 для підписів ETSI та adbe.pkcs7.detached і відкочується до SHA-1 лише на застарілому шляху adbe.pkcs7.sha1. Однаково задайте його явно. Аудитори питають, який геш ви використали, і явне значення в коді захистити легше, ніж типове значення, яке доводиться пояснювати, попередньо зазирнувши до документації

Драбина рівнів базової лінії PAdES — B-B, B-T, B-LT і B-LTA, побудована з PDF Library for Delphi: кожен рівень додає мітки часу, докази DSS або поновлювану мітку часу документа поверх ядра ETSI.CAdES.detached
Кожен базовий рівень ETSI додає ще одну гарантію поверх того самого ядра CAdES — від підписаних атрибутів до оновлюваної мітки часу документа

Створення базового підпису

Плаский API веде підписання як одноразовий кінцевий автомат: відкрийте процес на вихідному файлі, налаштуйте його, завершіть у файл результату, прочитайте код результату. Послідовність нижче створює підпис PAdES-B-B із SHA-256. Найважливіший рядок не має до самого підпису жодного стосунку. Це навмисно завищене резервування /Contents, бо це єдине, чого ви не зможете змінити пізніше, якщо до цього підпису колись доведеться додати штамп часу

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // місце для майбутнього штампа часу
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile повертає 0, коли вихідний файл узагалі не вдається відкрити. Далі GetSignProcessResult розрізняє ті види збоїв, які справді трапляються на бойових системах: 4 означає неправильний пароль PDF, 7 — неправильний пароль PFX, 9 — файл сертифіката без закритого ключа, 10 — недоступний для запису шлях результату, 11 — збій під час накладання байтів підпису. Запис числового коду поруч із іменем вхідного файлу перетворює туманне звернення до підтримки на однохвилинну діагностику

Додавання штампа часу RFC 3161, який бібліотека не отримає за вас

PDF Library for Delphi не постачає клієнта TSA, і це свідомо проведена межа, а не прогалина. Бібліотека обчислює геш, який має контрпідписати служба штампів часу, і згодом повторно вбудовує доповнений CMS; обмін HTTP і хірургія над CMS між цими кроками належать викликачу. Для такого поділу є вагома технічна причина. Елемент керування Windows CryptoAPI, який номінально додає непідписані атрибути, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, падає з CRYPT_E_INVALID_INDEX на відокремленій розкладці SignedData, яку використовує PAdES. Тому розширений CMS має надійти від кодувальника CMS під вашим власним контролем. Жодна бібліотека не може тихо вкласти токен одним системним викликом, а та, що це стверджує, робить хірургію там, де ви її не бачите

Конвеєр додавання мітки часу RFC 3161 до підпису PAdES у Delphi: хешування та вбудовування PDF Library for Delphi відділені від запиту до TSA та перекодування CMS викликача всередині зарезервованого простору /Contents
Бібліотека хешує та повторно вбудовує, тоді як ваш код отримує токен і проводить CMS-хірургію, і результат має вміститися в 8192-байтовий резерв /Contents
var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // обидва виклики нижче є кодом застосунку: HTTP POST до вашої TSA
    // і перекодування CMS, що долучає токен як непідписаний атрибут
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Стежте тут за кодами результату: 12 означає, що названого поля підпису не існує, 11 — що наявний CMS не вдалося розібрати, а 13 — що доповнений CMS більше не вміщується в зарезервований заповнювач /Contents. Код 13 болить найдужче, бо єдиний спосіб це виправити — підписати заново: типовий токен штампа часу разом із ланцюжком сертифікатів займає від 4 до 6 КБ, і резервування у 8192 байти, зроблене на кроці B-B, існує саме для того, щоб цьому крокові було куди приземлитися

Валідація починається з ByteRange, а не з ланцюжка сертифікатів

Зелена галочка у переглядачі — це рішення про довіру щодо сховища сертифікатів конкретної машини, а не структурний вирок щодо файлу. Програмна валідація має починатися нижче, з питання, яке інкрементні оновлення роблять неочевидним: які саме байти покриває кожен підпис? Кожне вдосконалення, обговорене тут, — чи то другий підпис, чи то словник DSS, чи то штамп часу документа — надходить через інкрементне оновлення, і кожне таке оновлення дописує байти поза межами /ByteRange попереднього підпису. Ці дописані байти є законними. Валідатор усе одно має класифікувати їх за політикою змін документа, а рівень DocMDP для кожного поля, у якому ця політика живе, читається через GetSignatureDocMDPLevelByName

Аудит байтової розкладки підписаного PDF у Delphi: покриті діапазони ByteRange, виключені байти /Contents, додані поза діапазоном інкрементні оновлення та вердикт покриття проти розміру файлу
Два покриті відрізки, з яких виключені власні байти підпису, розповідають правдиву історію покриття, а додані оновлення класифікуються за політикою DocMDP, а не викликають страх
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // до Open: SignDoc тримає блокування спільного доступу
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 означає, що поле справді підписано
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

У цьому шляху аудиту живуть дві пастки. TPDFlibSignDoc.Open тримає файл із винятковим блокуванням спільного доступу, тож валідатор, який також хоче обчислити геш сирих байтів файлу для перевірки CMS, мусить прочитати файл у пам'ять до того, як відкриє його для аудиту. Поміняйте цей порядок місцями — і читання впаде на блокуванні, яке ви самі й поставили. Друга пастка радше тиха, ніж гучна: відповідник із плаского API, GetSignProcessByteRange, повертає Integer, тоді як самі зсуви мають тип Int64, тож за межею 2 ГБ плаский виклик мовчки обрізає їх, і саме тому цей приклад дістає зсуви через клас аудиту. Варто назвати й одну відсутність. У пласкому шарі немає жодної обгортки VerifySignature. Криптографічні вироки надходять від TPDFlibSignatureVerifier рівня класів, який повертає vsValid, vsInvalid або vsUnknown, або від зовнішнього валідатора, якому ваша політика відповідності вже довіряє

Довготривала валідація: DSS, VRI та штамп часу документа

PAdES-B-LT існує тому, що інфраструктура відкликання смертна. ETSI EN 319 142-1 §5.4.2.2 описує Document Security Store: словник рівня документа, що несе сертифікати, CRL та відповіді OCSP, за потреби індексовані по кожному підпису через записи VRI із ключем за гешем /Contents відповідного підпису. Потік у PDF Library for Delphi дзеркалить конструкцію зі штампом часу. NewPAdESDSSProcessFromFile відкриває процес; AddPAdESDSSCertificate, AddPAdESDSSCRL та AddPAdESDSSOCSP приймають блоби DER; AddPAdESDSSVRI прив'язує обраний матеріал до одного підпису; EndPAdESDSSProcessToFile записує все як інкрементне оновлення. Складна частина лишається на вашому боці. Отримати матеріал про відкликання і зважити, чи він достатньо свіжий, щоб його варто було вбудовувати, — робота викликача. Бібліотека гарантує структурну відповідність словників; вона не може гарантувати, що ваш респондер OCSP сказав правду

Архівна кінцева точка, B-LTA, додає штамп часу документа: окреме поле підпису, тип якого — DocTimeStamp, а не Sig, створене через SetSignProcessDocTimeStamp із зарезервованою довжиною підпису. Воно не заміняє штамп часу підпису з кроку B-T. Штамп часу підпису доводить, коли існував один конкретний підпис; штамп часу документа захищає весь файл разом із доказами DSS і є тим елементом, який довготривалий архів поновлює кожні кілька років у міру ослаблення алгоритмів. Зрілий архівний профіль несе обидва. Для переглядачів, старших за ці структури, TPDFlibSignDoc.EnsurePAdESExtensions записує розширення розробника ESIC до каталогу документа, оголошуючи, що файл користується можливостями, визначеними ETSI

Одну реакцію на все це варто випередити, бо вона має вигляд помилки, а помилкою не є. Переглядач нерідко повідомляє «дійсність невідома» для файлу, структура PAdES якого цілком правильна. Довіра і структура — це незалежні осі. Переглядач просто не може вибудувати ланцюжок від підписувача до кореня, якому він довіряє на цій машині, що є звичною справою з приватними центрами сертифікації та тестовими сертифікатами, навіть коли аудит ByteRange і перевірка CMS обидва проходять. Ліки тут — належно поширити кореневий сертифікат або звірятися з довіреними списками ЄС, коли справжньою метою є кваліфікований статус за eIDAS, а не правити код підписання

Погляд з боку аудиту, тобто перелічування полів підпису по всьому корпусу документів, вивантаження розкладок ByteRange і масове читання рівнів DocMDP, розглянуто в супутньому матеріалі про середовище перевірки відповідності та підписання. Підписані документи, які мають ще й задовольняти архівну політику, належать до процесу, описаного в статті Перевірка відповідності PDF/A та PDF/UA у Delphi. Повна документація API та ознайомчі збірки лежать на сторінці продукту losLab PDF Library for Delphi