Artykuł techniczny

Weryfikacja podpisu ECDSA PDF w Delphi: DER do P1363

Wartość signatureValue ECDSA wewnątrz kontenera CMS to DER SEQUENCE { INTEGER r, INTEGER s }. Funkcja Windows CNG BCryptVerifySignature nie akceptuje żadnej z tych dwóch form: chce IEEE P1363 o stałej szerokości r || s, bez tagów i bez długości. HotPDF, natywny komponent VCL PDF dla Delphi i C++Builder, konwertuje między tymi dwoma formami zgodnie ze ścisłymi zasadami DER przed zaimportowaniem klucza

Awaria, której to zapobiega, jest konkretna i demoralizująca. Acrobat otwiera dokument i pokazuje zieloną fajkę. Twój własny weryfikator, przechodząc te same bajty, zwraca nieprawidłowy, albo CNG oddaje STATUS_INVALID_SIGNATURE bez dalszego wyjaśnienia. Z podpisem nic nie jest nie tak. Nie tak jest to, że mniej więcej siedemdziesiąt bajtów ASN.1 zostało przekazanych do API, które oczekiwało sześćdziesięciu czterech bajtów surowej liczby całkowitej, a niedopasowanie jest niewidoczne, chyba że wiesz, czego szukać

Dlaczego BCryptVerifySignature odrzuca prawidłowy podpis ECDSA?

Ponieważ obie strony wywołania mówią różnymi kodowaniami podpisu i żadna z nich tego nie zapowiada. ISO 32000-1 §12.8 mówi, że słownik podpisu niesie blob CMS w /Contents; RFC 5652 §5.3 mówi, że signatureValue w każdym SignerInfo jest OCTET STRING, którego zawartość jest tym, co definiuje algorytm podpisu. Dla ECDSA tą zawartością jest struktura DER SEC 1: SEQUENCE trzymająca dwa INTEGER. Ma zmienną długość z założenia, ponieważ r i s są liczbami całkowitymi, a DER usuwa wiodące zerowe oktety z liczb całkowitych

IEEE P1363 przyjmuje przeciwny pogląd. Definiuje podpis jako konkatenację dwóch współrzędnych, każdej dopełnionej z lewej strony zerami dokładnie do szerokości bajtowej pola krzywej. Podpis P-256 ma zawsze 64 bajty. Kodowanie DER tego samego podpisu ma zwykle 70 lub 71 bajtów i może mieć gdziekolwiek od około 8 do 72. Przekaż formę DER do BCryptVerifySignature, a samo sprawdzenie długości skazuje wywołanie na porażkę, dlatego HotPDF normalizuje przed weryfikacją, a nie po niej

uses
  HPDFECDSA;

// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

Zasady DER, których parser podpisów nie może rozluźnić

Każde wymienione tutaj odrzucenie to odrzucenie, które HotPDF wykonuje celowo, i każde z nich zamyka ścieżkę, którą pobłażliwy parser zostawiłby otwartą. Pokusą przy pisaniu konwertera jest znalezienie dwóch węzłów INTEGER, skopiowanie ich zawartości i przejście dalej. To działa na dobrze sformułowanych danych wejściowych i po cichu akceptuje rodzinę podatnych na malleability reenkodowań na wrogich danych. Więc HPDFECDSANormalizeSignature odrzuca ujemną liczbę całkowitą, czyli dowolne r lub s, którego pierwszy oktet zawartości ma ustawiony najwyższy bit, ponieważ prawidłowy skalar ECDSA jest dodatni. Odrzuca wartość całkowicie zerową, ponieważ r = 0 lub s = 0 nigdy nie jest prawomocnym podpisem. Odrzuca zbędny wiodący zerowy oktet: X.690 §8.3 zezwala dokładnie na jeden, i tylko wtedy, gdy następny oktet czytałby się inaczej jako ujemny, więc 00 po którym następuje oktet poniżej 0x80 to reenkodowanie, nie podpis. Odrzuca niemininalny nagłówek długości, ponieważ X.690 §10.1 wymaga postaci definite zakodowanej w najmniejszej liczbie oktetów, a długość w formie długiej, która mogłaby być krótką, to inny ciąg bajtów niosący to samo znaczenie. Odrzuca liczbę całkowitą szerszą niż rozmiar współrzędnej krzywej, ponieważ taka wartość nie może być elementem pola. I odrzuca dowolny węzeł końcowy po s, wraz z zewnętrznym SEQUENCE, którego całkowita długość nie równa się długości całego bloba

Te dwie ostatnie mają większe znaczenie, niż się wydaje. Bajty końcowe po SEQUENCE to klasyczna sztuczka malleability podpisu: dopisz śmieci, a pobłażliwy weryfikator wciąż powie prawidłowy, podczas gdy ciąg bajtów, który zwalidował, nie jest ciągiem bajtów, który został podpisany. Ten sam instynkt kieruje wzmocnieniem długości ASN.1 opisanym w notatce o parsowaniu PKCS#12, i to ten sam instynkt tutaj. W ścieżce weryfikacji zaakceptowana struktura, która nigdy nie została wyemitowana przez zgodnego sygnatariusza, jest defektem, nie uprzejmością

Szerokość współrzędnej należy do krzywej, nie do podpisu

HotPDF wyprowadza szerokość wyjścia z nazwanego OID krzywej, nigdy z długości DER, który właśnie sparsował. To druga połowa konwersji i połowa łatwa do subtelnego zepsucia. RFC 5480 §2.1.1 identyfikuje krzywą w parametrach SubjectPublicKeyInfo certyfikatu, a HPDFECDSACurveFromOID mapuje trzy OID-y wspierane przez HotPDF: 1.2.840.10045.3.1.7 dla P-256, 1.3.132.0.34 dla P-384 i 1.3.132.0.35 dla P-521. HPDFECDSACoordinateSize zwraca wtedy 32, 48 lub 66 bajtów, a bufor P1363 to podwójnie tyle: 64, 96 lub 132. Każda zdekodowana liczba całkowita jest wyrównana do prawej w swojej połowie, więc krótkie r jest dopełniane zerami z lewej, a nie przesuwane. P-521 to ta, która łapie ludzi, ponieważ 521 bitów to 65,125 bajta i zaokrągla się w górę do 66, dając 132-bajtowy podpis, którego żadna intuicja potęgi dwójki by nie przewidziała. Klucz publiczny podróżuje obok jako nieskompresowany punkt EC zgodnie z RFC 5480 §2.2, czyli 0x04 po którym następuje X i Y, więc HotPDF sprawdza, że ma dokładnie 1 + 2 * CoordinateSize bajtów i zaczyna się od 0x04, zanim dotknie CNG

var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
  Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);

  case Res of
    evrValid:
      Memo1.Lines.Add('signature verifies');
    evrInvalid:
      Memo1.Lines.Add('signature does not match the digest');
    evrMalformed:
      Memo1.Lines.Add('DER encoding or public point rejected');
    evrUnsupported:
      Memo1.Lines.Add('curve or algorithm not supported here');
    evrProviderUnavailable:
      Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
    evrProviderError:
      Memo1.Lines.Add('CNG returned an unexpected status');
  end;
end;

Zwróć uwagę na ostatni parametr. HPDFECDSAVerifyDigest akceptuje też eseP1363 dla wywołujących, którzy już posiadają podpis o stałej szerokości, z tokena sprzętowego lub zdalnej usługi podpisującej, która zwraca surowe r || s. Ta ścieżka nadal wymusza sprawdzenie długości i niezerowości na obu połowach, więc bufor odpowiedniego rozmiaru pełen zer jest odrzucany, a nie przepuszczany do dostawcy

Dlaczego generyczna nazwa algorytmu ECDSA zawodzi na starszym Windows?

Ponieważ generyczna nazwa jest nowsza niż baza wdrożeniowa, do której wysyłasz. CNG udostępnia identyfikator algorytmu ECDSA, który wnioskuje krzywą z zaimportowanego klucza, i to czysty sposób napisania tego kodu, ale BCryptOpenAlgorithmProvider jest gwarantowane, że rozwiąże go tylko na nowszych wersjach Windows. Na starszej maszynie wywołanie otwierające zawodzi, uchwyt dostawcy pozostaje nil, i każda weryfikacja ECDSA w twojej aplikacji raportuje niewspierane na podpisie, który jest zupełnie dobry. HotPDF unika tego urwiska, otwierając zamiast tego identyfikatory per krzywa. Rozwiązuje ECDSA_P256, ECDSA_P384 i ECDSA_P521 raz, cache'uje jeden uchwyt dostawcy na krzywą i zamyka je w finalizacji jednostki. Każda weryfikacja wykonuje wtedy tylko tanią pracę: zaimportuj tymczasowy klucz publiczny z ECCPUBLICBLOB, wywołaj BCryptVerifySignature, zniszcz klucz. Żadnych powtórzonych LoadLibrary, żadnych powtórzonych GetProcAddress, żadnego otwierania i zamykania dostawcy na podpis. Weryfikacja wsadowa kilkuset dokumentów odczuwa różnicę, podobnie jak proces usługi, który w przeciwnym razie przemielałby uchwyty dostawcy pod obciążeniem

Kody wyniku pozostają szczere co do rozróżnienia. evrProviderUnavailable oznacza, że maszyna nie mogła dać HotPDF dostawcy; evrInvalid oznacza, że CNG odpowiedział STATUS_INVALID_SIGNATURE. Zwinięcie tych dwóch w jedną porażkę to sposób, w jaki problem wdrożeniowy zostaje błędnie zaraportowany jako sfałszowany dokument. To samo rozdzielenie między awarią środowiska a awarią kryptograficzną przebiega przez obsługę CNG i CAPI po stronie podpisywania, omówioną w artykule o podpisywaniu z magazynu certyfikatów i kolejności bajtów

Który certyfikat to podpisał? SignerIdentifier to dwie różne rzeczy

RFC 5652 §5.3 czyni SignerIdentifier typem CHOICE, a weryfikator obsługujący tylko jedno ramię po cichu zweryfikuje przeciwko niewłaściwemu kluczowi. Pierwsze ramię to issuerAndSerialNumber, SEQUENCE trzymające Name wystawcy w surowym DER i INTEGER numeru seryjnego, a dopasowanie go to porównanie bajtowe z każdym certyfikatem w zbiorze certificates CMS. Drugie ramię to [0] subjectKeyIdentifier, niejawnie otagowany OCTET STRING, a dopasowanie go wymaga zagłębienia się w certyfikat, zamiast porównywania jego pól nagłówka

Zagłębienie ma warstwę, która zaskakuje ludzi. Identyfikator klucza żyje w rozszerzeniu X.509v3, więc HotPDF przechodzi pole rozszerzeń [3] tbsCertificate, znajduje rozszerzenie, którego OID to 2.5.29.14, pomija opcjonalny BOOLEAN critical i bierze OCTET STRING extnValue. Ten ciąg oktetów nie jest identyfikatorem. Zgodnie z RFC 5280 §4.2.1.2 jego zawartość sama jest DER, a typ KeyIdentifier to kolejny OCTET STRING, więc parsujesz drugi raz, żeby dotrzeć do rzeczywistych bajtów. Zatrzymaj się o jedną warstwę za wcześnie, a porównasz 22-bajtowy wrapper z 20-bajtowym identyfikatorem, żaden certyfikat nigdy nie pasuje, a weryfikator wraca do jakiejkolwiek heurystyki, którą napisałeś jako kolejną — to prawdziwe zagrożenie. Wzięcie pierwszego certyfikatu ze zbioru to kuszący skrót i jest błędny zawsze, gdy CMS niesie łańcuch, co jest większością przypadków, ponieważ liść nie musi być pierwszy. HotPDF akceptuje niedopasowany certyfikat tylko wtedy, gdy kontener trzyma dokładnie jeden; przy wielu obecnych certyfikatach wymagane jest dokładne dopasowanie SignerIdentifier. Weryfikacja skrótu przeciwko kluczowi publicznemu pośredniego CA nie produkuje przyjaznego błędu, produkuje pewne siebie nieprawidłowy na dokumencie, który jest w porządku

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('signed.pdf') > 0 then
      for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
        if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
          Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
            [String(Info.FieldName), String(Info.PublicKeyAlgorithm),
             String(Info.CurveName), String(Info.HashAlgorithm),
             String(Info.SignerName)]))
        else
          Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
  finally
    Pdf.Free;
  end;
end;

THPDFSignatureInfo.CurveName raportuje P-256, P-384 lub P-521, więc dziennik audytu zapisuje, jaka krzywa została faktycznie użyta, zamiast samego słowa ECDSA. Instalacja na poziomie dokumentu wokół tego wywołania, w szczególności to, jak segmenty /ByteRange są haszowane i dlaczego skrót musi być liczony po pliku, a nie po sparsowanym drzewie obiektów, jest tematem artykułu towarzyszącego o weryfikacji podpisów PDF

Czego to nie daje

Zielony wynik z HPDFECDSAVerifyDigest odpowiada na jedno pytanie: te bajty zostały podpisane przez klucz prywatny pasujący do tego klucza publicznego. Nie mówi nic o tym, czy ten klucz należy do kogoś, komu powinieneś ufać. Budowanie łańcucha do kotwicy zaufania, odwołanie przez CRL lub OCSP i sprawdzenia polityki to osobna praca, a każdy produkt, który raportuje prawidłowy podpis bez nich, raportuje mniej, niż użytkownik zakłada. Daty ważności certyfikatu są wystawiane osobno w THPDFSignatureInfo dokładnie z tego powodu: podpis może zweryfikować się kryptograficznie, podczas gdy certyfikat, który go stworzył, wygasł dwa lata temu. Wsparcie krzywych jest też celowo wąskie. Obsługiwane są trzy krzywe NIST prime, a podpis nad dowolną inną krzywą zwraca niewspierane, a nie zgadywanie. Ścieżka CNG jest tylko dla Windows, co jest właściwym kompromisem dla komponentu VCL, ale warto to powiedzieć, zanim zaplanujesz wokół niego usługę wieloplatformową. A ścisłość nie jest konfigurowalna: nie ma trybu pobłażliwego, który akceptowałby niemininalną długość DER, ponieważ jakiś przestarzały sygnatariusz taki wyemitował. Jeśli spotkasz taki plik w produkcji, uczciwą odpowiedzią jest zapisać go i dopytać producenta, nie poszerzać parsera, aż plik przejdzie

Ścieżka weryfikacji ECDSA opisana tutaj jest dostarczana jako część standardowego HotPDF Component dla Delphi i C++Builder, obok ścieżek RSA PKCS#1 v1.5 i RSA-PSS oraz pełnego rekordu informacji o podpisie; strona produktu zawiera kompletne odniesienie do podpisu cyfrowego