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

Віддалений PAdES-підпис PDFium VCL: HSM і хмарні ключі

PDFiumPas розділяє підписування PAdES на два виклики, тож приватному ключу ніколи не потрібно перебувати у вашому процесі. PreparePadesRemoteSignature записує інкрементне оновлення з порожнім заповнювачем фіксованої ширини /Contents і повертає запис запиту, що несе дайджест документа SHA-256, точний ByteRange і відбиток підготовленого файлу. CompletePadesRemoteSignature бере відокремлений CMS, що повертає ваша служба підпису, і вставляє його в цей зарезервований слот

Між цими двома викликами можуть минути хвилини чи години, процес може перезапуститися, а робота може перемістися на іншу машину. Саме ця прогалина — вся причина того, чому API влаштовано саме так

Чому віддалений ключ не може використовувати звичайний виклик підписування?

Тому що SignPadesBytes припускає, що операція підписування відбувається всередині самого виклику. Він будує інкрементне оновлення, обчислює дайджест над ByteRange, підписує його й записує результат — усе це до повернення з виклику. Це саме те, що потрібно, коли ключ живе в сховищі сертифікатів Windows або у завантаженому вами файлі PKCS#12

Це неможливо, коли ключ живе в мережевому HSM, кваліфікованому пристрої створення підпису, яким керує надавач довірчих послуг, або в хмарному API підпису, що вимагає підтвердження від користувача на телефоні. У цих випадках послідовність — це не виклик функції, а розмова: ви надсилаєте дайджест, щось інше автентифікує людину, і CMS повертається пізніше. Синхронний API не може виразити «пізніше» без блокування потоку на операції, яка може потребувати другого фактора

Двофазний протокол

Перша фаза готує документ. PDFiumPas додає поле підпису й словник значення, резервує ContentsSize байтів простору в шістнадцятковому кодуванні в /Contents, обчислює ByteRange навколо цього резервування й створює TPadesRemoteSigningRequest, що містить FormatVersion, PreparedFingerprint, DocumentDigest, чотириелементний ByteRange, ContentsHexOffset і ContentsSize

Єдине значення, яке потрібне вашій службі підпису, — DocumentDigest: SHA-256, який повернений CAdES SignedData має нести як свій дайджест повідомлення. Усе інше в записі існує для того, щоб друга фаза могла довести, що файл, який вона завершує, — це той самий файл, для якого було обчислено цей дайджест

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // байти в шістнадцятковому кодуванні, зарезервовані для CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Зберігаємо сесію, щоб пізніший запуск — або інша машина — могли її завершити
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Що відхиляє Complete і навіщо існує кожна перевірка?

Завершення — це саме те місце, де зазвичай ламається дизайн віддаленого підписування, тож перевірка навмисно непрощаюча. CompletePadesRemoteSignature відхиляє підготовлений PDF, чий відбиток більше не відповідає запиту, ByteRange, що не відповідає записаним координатам заповнювача, змінені роздільники /Contents, заповнювач, що вже не порожній, CMS, більший за резервування, CMS, що не є рівно одним значенням DER, непідтримувану форму SignedData, відсутній атрибут signing-certificate-v2, і CMS, чий дайджест повідомлення не дорівнює підготовленому дайджесту документа

Кожна з цих перевірок відповідає реальному збою. Перевірки відбитка та ByteRange ловлять випадок, коли хтось перегенерував підготовлений файл між фазами, що дало б підпис, який перевіряється проти байтів, яких ні в кого немає. Перевірка порожнього заповнювача ловить подвійне завершення, коли другий CMS записується поверх підпису, що вже існує. Перевірка дайджесту повідомлення ловить найнебезпечніший з усіх випадків: коректно сформований CMS, підписаний над іншим документом, — саме те, що ви отримуєте, коли черга плутає дві паралельні сесії підписування. Без неї ви отримали б файл, що виглядає підписаним і всюди провалює перевірку, або й гірше — несе схвалення когось іншого

Вимога signing-certificate-v2 — це питання відповідності PAdES, а не цілісності. ETSI EN 319 142 вимагає, щоб сертифікат підпису був зв'язаний із підписаними атрибутами, і CMS без цього атрибута — не підпис PAdES, навіть якщо він криптографічно проходить перевірку. Відхилення цього на етапі завершення означає, що ви дізнаєтеся про це тут, а не зі звіту валідатора від клієнта, — тема, розглянута докладніше в статті про те, чому валідатори відхиляють підписи PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // повернуто HSM або TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Кожна відмова несе конкретну причину; логуйте її дослівно
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Перетин меж процесів і машин

SavePadesRemoteSigningRequest і LoadPadesRemoteSigningRequest серіалізують сесію через стабільний версійний бінарний формат, і саме це робить дизайн практичним, а не лише коректним. Веб-застосунок може підготувати документ в одному запиті, зберегти підготовлений PDF і блок сесії, повернути дайджест браузеру для підпису смарт-картою й завершити файл у зовсім іншому обробнику запитів

Саме поле FormatVersion тримає це безпечним при оновленнях. Сесія, записана старшою збіркою й завантажена новішою, розпізнається або явно відхиляється, а не хибно зчитується як запис іншої форми. Якщо ваша черга може тримати сесії днями, ставтеся до версії формату як до операційного факту, вартого логування, а не деталі реалізації

Визначення розміру заповнювача

ContentsSize — це той єдиний параметр, над яким варто справді подумати, бо він фіксується ще до того, як CMS взагалі існує. Він рахує резервування в шістнадцятковому кодуванні, тож CMS DER на 6 КБ потребує щонайменше 12 КБ простору, а реалізація обмежує резервування 64 МіБ

Зарезервуйте замало — і завершення провалиться з помилкою завеликого CMS уже після того, як ваша служба підпису виконала свою роботу, а на тарифікованій службі кваліфікованого підпису це означає марну операцію. Зарезервуйте забагато — і кожен підписаний документ навічно несе це доповнення. Розумний підхід — виміряти: підпишіть один документ вашим справжнім ланцюжком сертифікатів, подивіться на довжину DER, подвойте її для шістнадцяткового кодування, а потім додайте щедрий запас для токена часової позначки, якщо плануєте перейти на підпис рівня T. Ланцюжки з кількома проміжними сертифікатами й довгою відповіддю OCSP зростають швидше, ніж очікують

Що відбувається після підпису

Завершений віддалений підпис — це PAdES B-B. Довгострокова перевірка потребує часової позначки й матеріалу для перевірки, що є окремим інкрементним оновленням, яке додає DSS і його словники VRI для кожного підпису, описане в статті про довгострокові підписи з часовими позначками RFC 3161 і DSS. Цей крок локальний: він додає сертифікати, відповіді OCSP і CRL, жоден з яких не потребує приватного ключа

Перш ніж випускати рішення в продакшн, перевірте те, що ви створили, тим самим шляхом коду, яким скористалася б довірча сторона, — розглянуто в статті про перевірку цифрових підписів і рівнів PAdES. Підписування й перевірка — це різний код, і конвеєр віддаленого підписування — саме те місце, де вони можуть розійтися непомітно для будь-кого, доки зовнішній валідатор не скаже про це прямо

PDFiumPas — це компонент для Delphi та Lazarus навколо рушія PDFium з нативним стеком PAdES на Pascal, тож підписування, встановлення часових позначок і перевірка працюють без зовнішніх інструментів командного рядка. Повна документація API та пробна збірка — на сторінці PDFium Delphi component