Artykuł techniczny

PDFium VCL: zdalne podpisywanie PAdES kluczami HSM

PDFiumPas dzieli podpisywanie PAdES na dwa wywołania, dzięki czemu klucz prywatny nigdy nie musi znaleźć się w twoim procesie. PreparePadesRemoteSignature zapisuje aktualizację przyrostową z pustym, o stałej szerokości symbolem zastępczym /Contents i zwraca rekord żądania niosący skrót dokumentu SHA-256, dokładny ByteRange oraz odcisk palca przygotowanego pliku. CompletePadesRemoteSignature przyjmuje odłączony CMS zwrócony przez twoją usługę podpisującą i wstawia go w zarezerwowane miejsce

Między tymi dwoma wywołaniami mogą minąć minuty albo godziny, proces może się zrestartować, a praca może przenieść się na inną maszynę. Ta luka to cały powód, dla którego API ma taki kształt

Dlaczego zdalny klucz nie może użyć zwykłego wywołania podpisującego?

Ponieważ SignPadesBytes zakłada, że operacja podpisywania odbywa się wewnątrz wywołania. Buduje aktualizację przyrostową, oblicza skrót nad ByteRange, podpisuje go i zapisuje wynik, wszystko przed zwróceniem sterowania. To dokładnie właściwe zachowanie, gdy klucz mieszka w magazynie certyfikatów Windows albo w pliku PKCS#12, który wczytałeś

Jest niemożliwe, gdy klucz mieszka w sieciowym HSM, kwalifikowanym urządzeniu do składania podpisu obsługiwanym przez dostawcę usług zaufania, albo w chmurowym API podpisującym, które wymaga potwierdzenia przez użytkownika na telefonie. W takich przypadkach sekwencja to nie funkcja, tylko rozmowa: wysyłasz skrót, coś innego uwierzytelnia człowieka, a CMS wraca później. Synchroniczne API nie potrafi wyrazić „później” bez blokowania wątku na operacji, która może wymagać drugiego składnika

Protokół dwufazowy

Faza pierwsza przygotowuje dokument. PDFiumPas dopisuje pole podpisu i słownik wartości podpisu, rezerwuje ContentsSize bajtów miejsca zakodowanego szesnastkowo w /Contents, oblicza ByteRange wokół tej rezerwacji i tworzy TPadesRemoteSigningRequest zawierający FormatVersion, PreparedFingerprint, DocumentDigest, czteroelementowy ByteRange, ContentsHexOffset i ContentsSize

Jedyna wartość, której potrzebuje twoja usługa podpisująca, to DocumentDigest: SHA-256, który zwrócony CAdES SignedData musi nieść jako swój skrót wiadomości. Wszystko inne w rekordzie istnieje po to, aby faza druga mogła udowodnić, że plik, który kończy, jest plikiem, z którego wyliczono ten skrót

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;   // bajty szesnastkowe zarezerwowane dla 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;

  // Zachowaj sesję, aby późniejsze uruchomienie albo inna maszyna mogły ją dokończyć
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Co odrzuca Complete i dlaczego istnieje każda kontrola?

Dokończenie to miejsce, gdzie projekt zdalnego podpisywania zwykle idzie źle, więc walidacja jest celowo bezkompromisowa. CompletePadesRemoteSignature odrzuca przygotowany plik PDF, którego odcisk palca już nie pasuje do żądania, ByteRange, który nie pasuje do zapisanych współrzędnych symbolu zastępczego, zmodyfikowane ograniczniki /Contents, symbol zastępczy, który nie jest już pusty, CMS większy niż rezerwacja, CMS, który nie jest dokładnie jedną wartością DER, nieobsługiwany kształt SignedData, brakujący atrybut signing-certificate-v2 oraz CMS, którego skrót wiadomości nie jest równy skrótowi przygotowanego dokumentu

Każde z nich odpowiada realnej awarii. Kontrole odcisku palca i ByteRange wychwytują przypadek, w którym ktoś ponownie wygenerował przygotowany plik między fazami, co dałoby podpis weryfikujący się względem bajtów, których nikt nie ma. Kontrola pustego symbolu zastępczego wychwytuje podwójne dokończenie, gdy drugi CMS zostaje zapisany nad podpisem, który już istnieje. Kontrola skrótu wiadomości wychwytuje najgroźniejszy przypadek ze wszystkich: poprawnie sformowany CMS podpisany nad innym dokumentem, czyli to, co dostajesz, gdy kolejka miesza dwie równoległe sesje podpisywania. Bez niej wyprodukowałbyś plik, który wygląda na podpisany i wszędzie zawodzi walidację, albo, co gorsza, niesie czyjąś cudzą zgodę

Wymóg signing-certificate-v2 to kwestia zgodności z PAdES, nie integralności. ETSI EN 319 142 wymaga, aby certyfikat podpisujący był związany z podpisanymi atrybutami, a CMS bez tego atrybutu nie jest podpisem PAdES, nawet jeśli weryfikuje się kryptograficznie. Odrzucenie go przy dokończeniu oznacza, że dowiadujesz się o tym tutaj, a nie z raportu walidatora od klienta, temat rozwinięty dalej w dlaczego walidatory odrzucają podpisy 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;   // zwrócone przez HSM albo 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
        // Każde odrzucenie niesie konkretny powód; zaloguj go dosłownie
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Przekraczanie granic procesów i maszyn

SavePadesRemoteSigningRequest i LoadPadesRemoteSigningRequest serializują sesję przez stabilny, wersjonowany format binarny, co czyni projekt praktycznym, a nie tylko poprawnym. Aplikacja webowa może przygotować dokument w jednym żądaniu, zapisać przygotowany plik PDF i blob sesji, zwrócić skrót do przeglądarki dla podpisu kartą inteligentną i dokończyć plik w zupełnie innym obsłudze żądania

Pole FormatVersion to to, co utrzymuje to bezpieczne między aktualizacjami. Sesja zapisana przez starszą kompilację i wczytana przez nowszą jest jawnie rozpoznana albo odrzucona, zamiast zostać błędnie odczytana jako rekord o innym kształcie. Jeśli twoja kolejka może przechowywać sesje przez wiele dni, traktuj wersję formatu jako fakt operacyjny wart logowania, nie szczegół implementacyjny

Rozmiar miejsca zarezerwowanego

ContentsSize to jedyny parametr, nad którym musisz się zastanowić, ponieważ jest ustalany, zanim CMS w ogóle powstanie. Liczy zarezerwowane miejsce zakodowane szesnastkowo, więc CMS DER o rozmiarze 6 KB potrzebuje co najmniej 12 KB miejsca, a implementacja ogranicza rezerwację do 64 MiB

Zarezerwuj za mało, a dokończenie zawodzi błędem zbyt dużego CMS już po tym, jak twoja usługa podpisująca wykonała swoją pracę, co w przypadku licznikowanej usługi podpisu kwalifikowanego oznacza zmarnowaną operację. Zarezerwuj za dużo, a każdy podpisany dokument na zawsze niesie wypełnienie. Rozsądnym podejściem jest zmierzenie: podpisz jeden dokument swoim prawdziwym łańcuchem certyfikatów, sprawdź długość DER, podwój ją dla zapisu szesnastkowego, a potem dodaj hojny zapas na token znacznika czasu, jeśli zamierzasz przejść na podpis poziomu T. Łańcuchy z kilkoma pośrednikami i długą odpowiedzią OCSP rosną szybciej, niż się ludziom wydaje

Co następuje po podpisie

Dokończony podpis zdalny to PAdES B-B. Długoterminowa walidacja wymaga znacznika czasu i materiału walidacyjnego, co jest osobną aktualizacją przyrostową dodającą DSS i jego per-podpisowe słowniki VRI, opisaną w długoterminowych podpisach ze znacznikami czasu RFC 3161 i DSS. Ten krok jest lokalny: dodaje certyfikaty, odpowiedzi OCSP i CRL-e, z których żaden nie potrzebuje klucza prywatnego

Przed wysyłką zweryfikuj to, co wyprodukowałeś, tą samą ścieżką kodu, której użyłaby strona ufająca, omówioną w inspekcji podpisów cyfrowych PDF i poziomów PAdES. Podpisywanie i weryfikacja to różny kod, a potok zdalnego podpisywania to dokładnie miejsce, w którym te dwa mogą się rozjechać, zanim ktokolwiek to zauważy, dopóki nie powie tego zewnętrzny walidator

PDFiumPas to komponent Delphi i Lazarus wokół silnika PDFium z natywnym stosem PAdES w Pascalu, więc podpisywanie, znakowanie czasem i walidacja działają bez zewnętrznych narzędzi wiersza poleceń. Pełna dokumentacja API i wersja próbna są na stronie komponentu PDFium dla Delphi