Walidacja jednego podpisu PAdES oznacza sprawdzenie trzech niezależnych rzeczy, a zielony znacznik zaakceptowania w przeglądarce mówi tylko o trzeciej z nich. Po pierwsze, tablica /ByteRange musi pokrywać właściwe bajty: odcinki, które nazywa, muszą odtworzyć dokładnie te dane wejściowe, z których wzięto skrót CMS, bez żadnych podpisanych bajtów pozostawionych poza nimi. Po drugie, certyfikat wewnątrz CMS musi łańcuchować się do zaufanego korzenia i nieść podpisany atrybut certyfikatu podpisującego, którego wymaga PAdES. Po trzecie, jeśli profil deklaruje znacznik czasu, token RFC 3161 musi wiązać wartość podpisu z punktem w czasie sprzed wygaśnięcia certyfikatu. Acrobat zwija wszystkie trzy w jedną ikonę; sprawdzian zgodności trzyma je osobno, i tak samo powinien robić kod, który produkuje te pliki. losLab PDF Library (PDF Library for Delphi) daje ci stronę podpisywania tego wszystkiego, ponowne osadzanie znacznika czasu i wywołania audytowe do zbadania ByteRange, zanim mu zaufasz
Jedno rozróżnienie potyka niemal każdą pierwszą implementację PAdES, więc warto je powiedzieć przed jakimkolwiek kodem. Podpis zapisany z /SubFilter /adbe.pkcs7.detached to całkowicie solidny podpis ISO 32000-1 §12.8, który Acrobat zgłosi jako ważny. Ale to nie jest podpis PAdES, bo ETSI EN 319 142-1 wymaga ETSI.CAdES.detached na każdym poziomie bazowym. Sprawdzian zgodności eIDAS odrzuca pierwszy i akceptuje drugi, mimo że kryptografia jest identyczna. Profil to deklaracja, którą dokument składa o sobie samym, a właściwe jej ustawienie to jedno wywołanie w PDF Library for Delphi
Co zmienia podpis PDF w podpis PAdES
ETSI EN 319 142-1 definiuje cztery poziomy bazowe ułożone na formacie CMS. PAdES-B-B to punkt wejścia: podpis CAdES w polu podpisu PDF z SubFilter ETSI.CAdES.detached i podpisanym atrybutem certyfikatu podpisującego. PAdES-B-T dodaje znacznik czasu RFC 3161 nad wartością podpisu, dowodząc, że podpis istniał przed punktem w czasie, którego nikt nie potrafi cofnąć. PAdES-B-LT osadza certyfikaty, CRL-e i odpowiedzi OCSP potrzebne do walidacji w Document Security Store, więc plik pozostaje weryfikowalny po tym, jak wystawiający CA wycofa swoją infrastrukturę. PAdES-B-LTA wieńczy stos znacznikiem czasu dokumentu, który ponownie chroni zgromadzone dowody w miarę słabnięcia algorytmów
PDF Library for Delphi odwzorowuje te pojęcia na swoje API procesu podpisywania. Znacznikiem profilu jest SetSignProcessCustomSubFilter. Jeśli twoja polityka potrzebuje wskazania typu zobowiązania (dowód pochodzenia, dowód zatwierdzenia, albo jeden z innych identyfikatorów ETSI numerowanych od 1 do 6), przechodzi to przez SetSignProcessCommitmentType. Jawna polityka podpisu dołącza się przez SetSignProcessSignaturePolicy, która przyjmuje OID polityki i jej skrót. Jedno ustawienie domyślne zasługuje na uwagę: przy algorytmie skrótu pozostawionym na auto biblioteka wybiera SHA-256 dla podpisów ETSI i adbe.pkcs7.detached, a wraca do SHA-1 tylko na przestarzałej ścieżce adbe.pkcs7.sha1. I tak ustaw go jawnie. Audytorzy pytają, jakiego skrótu użyłeś, a jawna wartość w kodzie łatwiej się broni niż wartość domyślna, którą trzeba iść wyjaśnić w podręczniku
Produkowanie podpisu bazowego
Płaskie API napędza podpisywanie jako jednorazowy automat stanów: otwórz proces na pliku źródłowym, skonfiguruj go, zakończ do pliku wyjściowego, odczytaj kod wyniku. Poniższa sekwencja produkuje podpis PAdES-B-B z SHA-256. Linia, która ma tu największe znaczenie, nie ma nic wspólnego z samym podpisem. To celowo zawyżona rezerwacja /Contents, bo to jedyna rzecz, której nie da się później zmienić, jeśli kiedykolwiek trzeba będzie dodać do tego podpisu znacznik czasu
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); // miejsce na późniejszy znacznik czasu
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 zwraca 0, gdy źródła w ogóle nie da się otworzyć. Potem GetSignProcessResult rozdziela tryby awarii, które faktycznie zdarzają się na produkcji: 4 oznacza błędne hasło PDF, 7 błędne hasło PFX, 9 plik certyfikatu bez klucza prywatnego, 10 ścieżkę wyjściową, do której nie da się zapisać, 11 błąd podczas nakładania bajtów podpisu. Logowanie kodu liczbowego obok nazwy pliku wejściowego zamienia mgliste zgłoszenie wsparcia w jednominutową diagnozę
Dodawanie znacznika czasu RFC 3161, którego biblioteka za ciebie nie pobierze
PDF Library for Delphi nie dostarcza klienta TSA, i to celowa granica, a nie luka. Biblioteka oblicza skrót, który urząd znakowania czasem musi kontrasygnować, i ponownie osadza rozszerzony CMS później; wymiana HTTP i chirurgia CMS pomiędzy nimi należą do wywołującego. Za tym podziałem stoi twardy powód techniczny. Kontrolka Windows CryptoAPI, która nominalnie dodaje atrybuty niepodpisane, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, zawodzi z CRYPT_E_INVALID_INDEX na odłączonym układzie SignedData, którego używa PAdES. Więc wzbogacony CMS musi pochodzić z kodera CMS pod twoją własną kontrolą. Żadna biblioteka nie potrafi po cichu wpleść tokenu jednym wywołaniem systemowym, a każda, która twierdzi inaczej, robi tę chirurgię gdzieś, gdzie tego nie widzisz
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);
// oba poniższe wywołania to kod aplikacji: POST HTTP do twojego TSA,
// i ponowne zakodowanie CMS, które dołącza token jako atrybut niepodpisany
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;
Miej oko na kody wyniku tutaj: 12 oznacza, że nazwane pole podpisu nie istnieje, 11, że istniejącego CMS nie dało się sparsować, a 13, że wzbogacony CMS już nie mieści się w zarezerwowanym symbolu zastępczym /Contents. Kod 13 to ten, który boli, bo jedyną naprawą jest ponowne podpisanie: typowy token znacznika czasu z łańcuchem certyfikatów zajmuje od 4 do 6 KB, a rezerwacja 8192 bajtów zrobiona podczas kroku B-B istnieje właśnie po to, żeby ten krok miał gdzie wylądować
Walidacja zaczyna się od ByteRange, nie od łańcucha certyfikatów
Zielony znacznik zaakceptowania w przeglądarce to decyzja zaufania wobec magazynu certyfikatów tej maszyny, nie strukturalny werdykt o pliku. Walidacja programowa powinna zaczynać się niżej, od pytania, które przyrostowe aktualizacje czynią subtelnym: które bajty faktycznie pokrywa każdy podpis? Każde ulepszenie omawiane tutaj, czy to drugi podpis, słownik DSS, czy znacznik czasu dokumentu, przychodzi przez przyrostową aktualizację, a każda aktualizacja dopisuje bajty poza /ByteRange wcześniejszego podpisu. Te dopisane bajty są legalne. Walidator wciąż musi je sklasyfikować wobec polityki modyfikacji dokumentu, a poziom DocMDP per pole, w którym mieszka ta polityka, jest czytelny przez GetSignatureDocMDPLevelByName
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // przed Open: SignDoc trzyma blokadę współdzielenia
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 oznacza faktycznie podpisane
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;
W tej ścieżce audytowej żyją dwie pułapki. TPDFlibSignDoc.Open trzyma plik pod wyłączną blokadą współdzielenia, więc walidator, który chce też zhaszować surowe bajty pliku do weryfikacji CMS, musi wczytać plik do pamięci, zanim otworzy go do audytu. Odwróć tę kolejność, a odczyt zawiedzie na blokadzie, którą sam ustawiłeś. Druga pułapka jest cicha, a nie głośna: odpowiednik z płaskiego API, GetSignProcessByteRange, zwraca Integer, podczas gdy leżące u podstaw przesunięcia są typu Int64, więc powyżej 2 GB płaskie wywołanie obcina bez skargi, dlatego ten przykład zamiast tego wyciąga przesunięcia przez klasę audytową. Warto nazwać też jeden brak. Warstwa płaska w ogóle nie ma otoczki VerifySignature. Werdykty kryptograficzne pochodzą z klasy TPDFlibSignatureVerifier, która zwraca vsValid, vsInvalid albo vsUnknown, albo z zewnętrznego walidatora, któremu twoja polityka zgodności już ufa
Walidacja długoterminowa: DSS, VRI i znacznik czasu dokumentu
PAdES-B-LT istnieje, bo infrastruktura unieważnień jest śmiertelna. ETSI EN 319 142-1 §5.4.2.2 określa Document Security Store: słownik na poziomie dokumentu, niosący certyfikaty, CRL-e i odpowiedzi OCSP, opcjonalnie indeksowany per podpis przez wpisy VRI kluczowane skrótem /Contents każdego podpisu. Przepływ PDF Library for Delphi odzwierciedla projekt znacznika czasu. NewPAdESDSSProcessFromFile otwiera proces; AddPAdESDSSCertificate, AddPAdESDSSCRL i AddPAdESDSSOCSP przyjmują blob-y DER; AddPAdESDSSVRI wiąże wybrany materiał z jednym podpisem; EndPAdESDSSProcessToFile zapisuje wszystko jako przyrostową aktualizację. Trudna część zostaje po twojej stronie. Pobranie materiału o unieważnieniu i osądzenie, czy jest wystarczająco świeży, by warto go osadzić, to zadanie wywołującego. Biblioteka gwarantuje, że słowniki są strukturalnie zgodne; nie może zagwarantować, że twój respondent OCSP powiedział prawdę
Archiwalny punkt końcowy, B-LTA, dodaje znacznik czasu dokumentu: osobne pole podpisu, którego typ to DocTimeStamp, a nie Sig, produkowane przez SetSignProcessDocTimeStamp z zarezerwowaną długością podpisu. Nie zastępuje on znacznika czasu podpisu z kroku B-T. Znacznik czasu podpisu dowodzi, kiedy istniał jeden konkretny podpis; znacznik czasu dokumentu chroni cały plik, łącznie z dowodami DSS, i jest elementem, który długoterminowe archiwum odnawia co kilka lat, w miarę słabnięcia algorytmów. Dojrzały profil archiwalny niesie oba. Dla czytników sprzed tych struktur, TPDFlibSignDoc.EnsurePAdESExtensions zapisuje rozszerzenie deweloperskie ESIC w katalogu dokumentu, ogłaszając, że plik używa funkcji zdefiniowanych przez ETSI
Jedną reakcję na to wszystko warto uprzedzić, bo wygląda jak błąd, a nim nie jest. Przeglądarka często zgłasza "ważność nieznana" na pliku, którego struktura PAdES jest całkowicie poprawna. Zaufanie i struktura to niezależne osie. Przeglądarka po prostu nie potrafi połączyć łańcuchem podpisującego z korzeniem, któremu ufa na tej maszynie, co jest rutyną przy prywatnych CA i certyfikatach testowych, nawet gdy audyt ByteRange i weryfikacja CMS obie przechodzą. Naprawą jest właściwe dystrybuowanie certyfikatu głównego, albo ocena wobec unijnych list zaufanych, gdy prawdziwym celem jest kwalifikowany status eIDAS, a nie dotykanie kodu podpisującego
Po perspektywę audytową, czyli wyliczanie pól podpisu w całym korpusie, zrzucanie układów ByteRange i masowe odczytywanie poziomów DocMDP, zobacz towarzyszący tekst o stanowisku roboczym zgodności i podpisywania. Podpisane dokumenty, które muszą też spełniać politykę archiwalną, należą do przepływu opisanego w preflight PDF/A i PDF/UA w Delphi. Pełna dokumentacja API i pobrania wersji ewaluacyjnej znajdują się na stronie produktu losLab PDF Library for Delphi