PDF Library for Delphi (PDFlibPas) wyciąga certyfikaty z wnętrza podpisu PDF czystym przejściem DER po strukturze CMS SignedData zapisanej w /Contents, bez udziału CryptoAPI. Od v3.539.10 każdy zagnieżdżony odczyt jest ograniczony swoim elementem nadrzędnym, zerowy padding za CMS-em jest ucinany na długości deklarowanej przez samego CMS-a, a identyfikatory obiektów kodują swój scalony pierwszy subidentyfikator w base-128. Reguła granic i poprawka OID wymieniły kod, który bez zgłaszania błędu podawał złe odpowiedzi, a reguła paddingu pilnuje, by surowszy czytnik nie odrzucał prawdziwych podpisów
Strona odczytu jest ważniejsza, niż się wydaje. Narzędzia walidacji długoterminowej muszą najpierw wyciągnąć certyfikat podpisującego i jego wystawców z istniejącego podpisu, zanim pobiorą dane o unieważnieniach, raport audytowy musi powiedzieć, kto podpisał, a budowa Lazarusa na Linuksie nie ma do dyspozycji funkcji komunikatów Windows. Parser w takiej pozycji rzadko wywala się na złym wejściu. Trybem awarii, który boli, jest liczba certyfikatów wliczająca bajty należne sąsiadowi, dopasowanie podpisującego do złego pola albo OID, który po cichu staje się innym OID-em. Potok podpisów zbudowany na takim fundamencie raportuje z pełnym przekonaniem bzdury
Wyciąganie certyfikatów podpisującego z podpisanego PDF-a
Pięć metod TPDFlib obsługuje stronę odczytu, a wszystkie przyjmują InputFile, Password, FieldName: każde wywołanie otwiera plik tylko do odczytu, odpowiada i zamyka go ponownie. GetSignatureEmbeddedCertificateCount i GetSignatureEmbeddedCertificateDER wyliczają certyfikaty ze zbioru w kolejności zapisu, GetSignatureSignerCertificateDER zwraca certyfikat, który wyprodukował dany SignerInfo, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER idą od tego podpisującego w stronę najdalszego wystawcy, jakiego sam podpis przy sobie wozi. Indeksy liczą się od zera. Wyniki trzymaj w AnsiString i właśnie tak zwraca je biblioteka: blob DER przepuszczony przez string albo TStrings przechodzi przez konwersję zestawu znaków i wraca uszkodzony
uses
SysUtils, Classes, PDFlibrary;
procedure SaveDer(const FileName: string; const Der: AnsiString);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if Der <> '' then
Fs.WriteBuffer(Der[1], Length(Der));
finally
Fs.Free;
end;
end;
const
Src = 'contract-signed.pdf';
Field = 'Signature1';
var
Pdf: TPDFlib;
ChainLen, I: Integer;
SignerDer, LastDer: AnsiString;
begin
Pdf := TPDFlib.Create;
try
WriteLn('Certificates in the CMS: ',
Pdf.GetSignatureEmbeddedCertificateCount(Src, '', Field));
SignerDer := Pdf.GetSignatureSignerCertificateDER(Src, '', Field, 0);
if SignerDer = '' then
raise Exception.Create('signer certificate missing or not matched');
SaveDer('signer.cer', SignerDer);
ChainLen := Pdf.GetSignatureCertificateChainLength(Src, '', Field, 0);
for I := 0 to ChainLen - 1 do
SaveDer(Format('chain-%d.cer', [I]),
Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, I));
if ChainLen > 0 then
begin
LastDer := Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, ChainLen - 1);
WriteLn('Next issuer, if any: ', Pdf.GetCertificateIssuerURLs(LastDer));
end;
finally
Pdf.Free;
end;
end;
Dwie rzeczy w tym wyjściu wymagają uwagi. Licznik równy 0 to nie diagnoza: brakujące pole, złe hasło, blob, który nie jest DER, i SignedData po prostu pomijające opcjonalny zbiór certyfikatów — wszystko to wraca jako 0 albo pusty łańcuch, więc loguj nazwę pola obok tej liczby. Łańcuch kończący się przed certyfikatem samowystawionym też nie jest błędem. Budowniczy łańcucha korzysta wyłącznie z certyfikatów osadzonych w podpisie, więc pozostałych wystawców trzeba dociągnąć przez adresy, które raportuje GetCertificateIssuerURLs
Ile z /Contents to właściwie CMS?
Do CMS-a należy wyłącznie prefiks deklarowany przez zewnętrzny SEQUENCE, a PLTrimCMSPadding ucina wszystko, co jest za nim. Podpisujący rezerwuje szesnastkowy ciąg /Contents zanim CMS w ogóle powstanie, bo /ByteRange opisany w ISO 32000-1 §12.8.1 musi być ustalony najpierw, więc slot dostaje hojny rozmiar, a niewykorzystany ogon to zera. PLTrimCMSPadding czyta pierwszy TLV, wymaga znacznika $30 i zwraca bajty aż do końca tego elementu; wszystko, co nie zaczyna się od poprawnego SEQUENCE, wraca puste. Ten szczytowy poziom to jedyne miejsce, gdzie bajty na końcu są legalne, a rozróżnienie ma znaczenie dla następnej sekcji: restrykcyjna reguła „element musi zużyć cały bufor" odrzuciłaby każdy prawdziwy podpis, podczas gdy pobłażliwa reguła stosowana na każdej głębokości pozwala zagnieżdżonym polom czytać bajty, które nie są ich
Po co czytnikowi DER offset końca rodzica?
Zagnieżdżony element jest poprawny tylko wtedy, gdy kończy się wewnątrz rodzica, a sprawdzenie wobec końca bufora tego nie dowodzi. Niskopoziomowy DERReadTLV w PDFlibASN1 ogranicza każdy element całym łańcuchem, co jest właściwym sprawdzianem dla obiektu najwyżej i złym dla wszystkiego pod nim. Wyobraź sobie SignerInfo, którego issuerAndSerialNumber deklaruje 40 bajtów, podczas gdy leżąca w środku nazwa wystawcy rości sobie 60. Każdy bajt jest wciąż w buforze, więc czytnik ograniczony buforem przyjmie nazwę, odczyta numer seryjny z następującego po niej algorytmu skrótu, a potem porówna tę parę z osadzonymi certyfikatami. Przed v3.539.10 walker CMS czytał dokładnie tak. Poprawka to mały wrapper, który wnosi pozycję końca rodzica do każdego odczytu
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// w rodzicu nic nie zostało: odmowa rozpoczęcia odczytu
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset stoi teraz za elementem; nie może przekroczyć rodzica
Result := Offset <= ParentEnd;
end;
// każdy poziom zapamiętuje swój koniec i przekazuje go w dół:
// OuterEnd := koniec ContentInfo (RFC 5652 sekcja 3)
// ExplicitEnd := koniec content [0] EXPLICIT
// ContentEnd := koniec SignedData (RFC 5652 sekcja 5.1)
// SignerEnd / InnerEnd dla SignerInfo i issuerAndSerialNumber
Jednostka PDFlibCMSRead przeprowadza teraz te końce przez ContentInfo, wrapper [0] EXPLICIT, pola SignedData aż po signerInfos, SignerIdentifier w obu jego postaciach, issuerAndSerialNumber i [0] subjectKeyIdentifier (RFC 5652 §5.3), oraz pola tbsCertificate czytane z każdego osadzonego certyfikatu przy dopasowywaniu podpisującego. Wewnątrz zbioru certificates i zbioru signerInfos element, który przebiegnie za koniec zbioru, zatrzymuje pętlę: PLExtractCMSCertificates zwraca już przyjęte certyfikaty i nigdy nie dokleja następujących po nich bajtów crls albo signerInfos do ostatniego. Dopasowanie issuer-and-serial wymaga też obu połówek, bo numer seryjny jest unikalny tylko w obrębie jednego wystawcy
Dlaczego 2.999.3 wyszło jako 1.15.3?
Dwa pierwsze łuki OID scalają się w jeden subidentyfikator, nie w jeden bajt, i ten subidentyfikator jest kodowany w base-128 jak każdy inny łuk. X.690 §8.19.4 definiuje go jako 40 * arc1 + arc2; wcześniejszy DER_OID zapisywał tę wartość przez Byte(...), co działa poprawnie tylko do 127, czyli wartości 2.47. Dla 2.999 suma wynosi 1079, rzutowanie na bajt zostawia 55, a 55 dekoduje się jako 1.15, więc identyfikator po cichu wskazuje inną gałąź drzewa. Wartości od 128 do 255 psują się inaczej: emitują jeden bajt z ustawionym bitem kontynuacji, który połyka następny łuk. Większość identyfikatorów PKI (1.2.840..., 2.5.29..., 0.4.0...) nigdy nie dochodzi do granicy, stąd ten błąd przetrwał; łuki joint-iso-itu-t od 2.48 wzwyż — już tak. DER_OID obsługuje zarówno koder podpisanych atrybutów, jak i dopasowywanie w DERFindExtensionByOID oraz sprawdzaniu content-type SignedData, więc złe kodowanie psuło i zapis, i wyszukiwanie
uses
SysUtils, PDFlibASN1;
function Hex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2) + ' ';
Result := Trim(Result);
end;
begin
WriteLn(Hex(DER_OID('2.999.3'))); // 06 03 88 37 03
WriteLn(Hex(DER_OID('2.47.1'))); // 06 02 7F 01
WriteLn(Hex(DER_OID('2.48.1'))); // 06 03 81 00 01
WriteLn(Hex(DER_OID('2.5.29.14'))); // 06 03 55 1D 0E
WriteLn(Hex(DER_OID('1.2.840.113549.1.7.2'))); // 06 09 2A 86 48 86 F7 0D 01 07 02
end.
Scalona wartość siedzi w UInt64 nie bez powodu. DER_OID parsuje łuki do Int64, więc legalny drugi łuk może mieć rozmiar Int64.MaxValue, a dodanie 80 dla arc1 = 2 przepełnia 64-bitową liczbę ze znakiem. UInt64 unosi Int64.MaxValue + 80 bez zawinięcia, a dziesięciobajtowy bufor roboczy mieści dziesięć grup po 7 bitów, jakich potrzebuje wartość 64-bitowa. Wektory testowe, które warto zachować, to te po obu stronach granicy: 2.47 musi zostać jednym bajtem, a 2.48 musi stać się dwoma
Co gwarantuje walker CMS po stronie odczytu?
PDFlibCMSRead gwarantuje strukturę i nic poza tym: zwraca bajty leżące tam, gdzie każe RFC 5652, i nie weryfikuje ani podpisu, ani skrótu, ani okresu ważności. Walker przyjmuje wyłącznie DER, więc DERReadTLV odrzuca długości niedookreślone i wielobajtowe numery znaczników, a CMS zakodowany w BER od niepoprawnego podpisującego raportuje zero certyfikatów zamiast częściowego zgadywania. Certyfikaty atrybutowe i pozostałe warianty CertificateChoices są pomijane, bo nic dalej w potoku nie umie ich użyć. Weryfikacja kryptograficzna zostaje przy kodzie, do którego należy, a zaczyna się od sprawdzania pokrycia bajtów opisanego w artykule o podpisywaniu PAdES i walidacji ByteRange w Delphi, a kontynuuje klasyfikowaniem tego, co zmieniło się po podpisaniu PDF-a
Szersza lekcja przenosi się na każdy format binarny: „w buforze" to własność bezpieczeństwa pamięci, „w rodzicu" to własność poprawności, a parser potrzebuje obu. To samo myślenie o wrogich długościach przewija się przez artykuł o hartowaniu parsera PDF w Pascalu przeciw złośliwym plikom. Ekstrakcja certyfikatów, budowa łańcuchów i API walidacji długoterminowej omawiane tutaj są częścią losLab PDF Library for Delphi, dla Delphi, C++Buildera i Lazarusa