PDFium Component w wersji 3.114.20 naprawia kodowanie RSASSA-PSS-params we wszystkich trzech backendach podpisywania PAdES: Windows CNG, macOS Keychain i PKCS#11. RFC 4055 §3.1 nadaje każdemu polu RSASSA-PSS-params jawny tag kontekstowy, od [0] do [3], a backendy zapisywały saltLength jako gołą uniwersalną liczbę INTEGER, wypisując przy tym trailerField równy swojej wartości domyślnej. Bajty podpisu były przez cały czas poprawne. Niepoprawny był AlgorithmIdentifier, który je opisuje, a to samo wystarcza, by weryfikator odrzucił podpis
Frustrujące jest to, gdzie ten błąd się ukrywa. Podpis CMS ma dwie połowy: operację kryptograficzną oraz ASN.1, który mówi weryfikatorowi, jak ta operacja została wykonana. Zrób pierwszą dobrze, a drugą źle, a wynikiem jest dokument, którego żadne narzędzie trzymające się specyfikacji nie odróżni od fałszerstwa. Ten artykuł dotyczy wyłącznie tej drugiej połowy: jak RSASSA-PSS-params musi być otagowane, jak trzy backendy pomyliły się w ten sam sposób i jak wygląda poprawiony DER w kategoriach TDerWriter
Dlaczego weryfikator odrzuca podpis RSASSA-PSS, którego bajty są poprawne?
Bo RSASSA-PSS to ten jeden schemat RSA, w którym weryfikator nie potrafi odzyskać parametrów z samego podpisu. Dopełnienie PKCS#1 v1.5 jest w pełni określone przez OID sha256WithRSAEncryption, więc jego parametrami jest goły NULL i nie ma tu czego zepsuć. PSS jest parametryzowany funkcją skrótu, funkcją generowania maski z własnym skrótem i długością soli, a RFC 8017 §A.2.3 zostawia wszystkie trzy otwarte. Podpisujący je wybiera, AlgorithmIdentifier je niesie, a weryfikator musi je odtworzyć dokładnie, zanim EMSA-PSS-VERIFY w ogóle się zacznie
Więc gdy PDFium Component podpisuje przez SHA-256, MGF1 na SHA-256 i 32-bajtową sól, te trzy fakty muszą przetrwać kodowanie DER tutaj i dekodowanie DER w innej implementacji. Blok parametrów, którego weryfikator nie potrafi sparsować, kończy weryfikację, zanim dojdzie do jakiegokolwiek potęgowania modularnego. Blok, który parsuje inaczej, jest gorszy, bo RFC 4055 §3.1 nadaje saltLength wartość domyślną 20. Dekoder, który pomija nierozpoznane pole, ląduje na tej wartości domyślnej, uruchamia EMSA-PSS-VERIFY z 20-bajtową solą przeciw podpisowi policzonemu z 32-bajtową i raportuje zły podpis, nie dając żadnej wskazówki, że problem jest w metadanych, a nie w kluczu. Oba wyniki produkowało kodowanie z 3.114.19, zależnie od tego, jak surowy był weryfikator, i żaden z nich nie wskazuje na AlgorithmIdentifier
Czego RFC 4055 §3.1 faktycznie wymaga od RSASSA-PSS-params
RFC 4055 §3.1 definiuje RSASSA-PSS-params jako SEQUENCE czterech pól, z których każde niesie jawny tag kontekstowy i wartość DEFAULT:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
Jawne tagowanie w DER oznacza, że każde pole jest opakowane w skonstruowany tag kontekstowy TLV — A0 dla [0], A1 dla [1], A2 dla [2] i A3 dla [3] — z uniwersalnym kodowaniem wartości zagnieżdżonym w środku. Każde pole jest otagowane właśnie dlatego, że każde jest opcjonalne przez swoją wartość domyślną. Bez tagów dekoder nie potrafiłby stwierdzić, czy SEQUENCE trzymająca jeden AlgorithmIdentifier niesie hashAlgorithm, czy maskGenAlgorithm, bo oba są typami SEQUENCE; z tagami numer tagu identyfikuje pole niezależnie od tego, których sąsiadów brakuje. Wartości emitowane przez PDFium Component idą za profilem z ETSI TS 119 312 §7 — SHA-256, MGF1 z SHA-256 i sól równa długości skrótu — i odzwierciedlają dokładnie to, co dostaje każde platformowe wywołanie podpisywania: BCRYPT_PSS_PADDING_INFO z cbSalt 32 dla NCryptSignHash, CK_RSA_PKCS_PSS_PARAMS z sLen 32 dla mechanizmu PKCS#11 oraz algorytm PSS podpisywania skrótu SHA-256 w frameworku Security
Jak trzy backendy popełniły ten sam błąd
Kodowanie z 3.114.19 tagowało dwa pierwsze pola, a dwa ostatnie zostawiało gołe — identycznie w TWinCmsSigner, TKeychainCmsSigner i TPkcs11CmsSigner. Ta symetria nie jest przypadkiem: wszystkie trzy implementują interfejs ICmsSigner z FPdfCms.pas, a ich ciała GetSignatureAlgorithmParams powstały z jednego szablonu. Szablon wyglądał tak:
// Przed 3.114.20: [0] i [1] otagowane, [2] i [3] nie
Result := W.Sequence(Concat4(
W.ContextSpecific(0, W.AlgId(OID_SHA256), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID(OID_MGF1), W.AlgId(OID_SHA256))), True),
W.IntegerOf(32), // goły INTEGER tam, gdzie wymagane było [2] EXPLICIT
W.IntegerOf(1))); // trailerField równy DEFAULT musi być nieobecny
Dekoder przechodzący tę SEQUENCE widzi A0, czyta algorytm skrótu, widzi A1, czyta funkcję generowania maski, a potem napotyka 02 01 20. To uniwersalna liczba INTEGER, a RSASSA-PSS-params nie ma nigdzie żadnego nieotagowanego elementu INTEGER. Surowy dekoder zatrzymuje się w tym miejscu. Pobłażliwy pomija nierozpoznany element, nigdy nie znajduje A2, przypisuje saltLength wartość domyślną 20, a potem trafia na drugą zabłąkaną liczbę, 02 01 01, i ma ten sam problem znowu. Żadna z tych ścieżek nie dochodzi do 32-bajtowej soli. Wspólny szablon jest wydajny, gdy jest poprawny, i równie wydajnym sposobem na trzykrotny błąd, gdy nie jest — dlatego poprawka weszła do wszystkich trzech unitów w jednym commicie i dlatego te trzy ciała metod pozostają po niej identyczne strukturalnie. Przyszły backend powinien skopiować ten blok z jednego z nich, a nie wyprowadzać go od nowa, bo właśnie przy wyprowadzaniu popełniono błąd
Dlaczego trailerField jest pomijany, a nie otagowany jako [3]?
Bo X.690 §11.5 mówi, że koder DER nie może zakodować składowej, której wartość równa się jej DEFAULT, a trailerField ma DEFAULT trailerFieldBC, czyli liczbę całkowitą 1. Oczywista poprawka starego kodu — zastąpienie gołego W.IntegerOf(1) przez W.ContextSpecific(3, W.IntegerOf(1), True) — daje blok, który pobłażliwy dekoder BER przyjmie, a surowy dekoder DER ma prawo odrzucić. Wartość nie jest błędna. Błędna jest jej obecność. Ta sama reguła sprawia, że pozostałe trzy pola są obecne: SHA-256 nie jest domyślnym sha1, MGF1 z SHA-256 nie jest domyślnym mgf1SHA1, a 32 nie jest domyślnym 20. Gdyby backend podpisywał przez SHA-1 i 20-bajtową sól, RFC 4055 §3.1 zwijałoby parametry do pustej SEQUENCE, 30 00, i to tej pustej SEQUENCE, a nie NULL, oczekuje weryfikator. PDFium Component nigdy nie emituje takiego kształtu, bo nigdy nie podpisuje tymi wartościami, ale to właśnie ten przypadek łapie każdego, kto zakłada, że „brak parametrów” zawsze zapisuje się jako 05 00
To jest ta różnica między DER i BER, która liczy się szczególnie przy podpisach. BER pozwala koderowi dołączyć składową o wartości domyślnej; DER tego zabrania, bo istnieje po to, by jedna wartość miała dokładnie jedno kodowanie, a podpis nad strukturą o dwóch legalnych kodowaniach to podpis, o który można się spierać. Wszystko wewnątrz CMS signedAttrs jest z tego powodu DER, a blok parametrów wędruje wewnątrz signedAttrs przez atrybut cmsAlgorithmProtection oraz w zewnętrznym signatureAlgorithm, więc nie dostaje żadnego zwolnienia
Poprawione kodowanie w TDerWriter
PDFium Component buduje teraz parametry trzema wywołaniami TDerWriter.ContextSpecific z FPdfAsn1.pas, po jednym na pole niebędące domyślnym, każde z argumentem Constructed ustawionym na True, by powstało opakowanie jawnego tagu, i bez ani jednej linii dla pola trailer. To ciało TWinCmsSigner.GetSignatureAlgorithmParams z wypisanymi OID-ami; unity Keychain i PKCS#11 zapisują te same wartości jako OID_SHA256, OID_MGF1 i OID_RSASSA_PSS:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1 taguje wszystkie cztery pola. saltLength to [2]; goły
// INTEGER jest tu czytany jako początek kolejnego pola. trailerField
// to [3] z DEFAULT 1, a X.690 11.5 zabrania kodowania wartości
// równej wartości domyślnej, więc jest pomijany całkowicie
Result := W.Sequence(Concat3(
W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID('1.2.840.113549.1.1.8'), // id-mgf1
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.ContextSpecific(2, W.IntegerOf(32), True)));
finally
W.Free;
end;
end
else
Result := nil; // PKCS#1 v1.5 i ECDSA: AlgIdWithParams zapisuje NULL
end;
Znaczące są dwa szczegóły otaczającej maszynerii. TDerWriter.AlgId produkuje AlgorithmIdentifier z parametrami NULL, co jest tym, co RFC 4055 §2.1 każe koderom generować dla zagnieżdżonego hashAlgorithm i dla wewnętrznego skrótu MGF1. A budowniczy CMS w FPdfCms.pas łączy OID podpisu z tymi bajtami przez TDerWriter.AlgIdWithParams, które podstawia NULL, gdy parametrami jest nil; dlatego psRsaPkcs1v15 i psEcdsa po prostu zwracają nil i nigdy nie były tym dotknięte, a także dlatego 1.2.840.113549.1.1.10, id-RSASSA-PSS, jest jedynym z tych trzech OID-ów podpisu niosącym prawdziwy blok parametrów. Wynikowe bajty dla profilu SHA-256 są ustalone i dość krótkie, by sprawdzić je okiem: zewnętrzna SEQUENCE 30 34 trzymająca A0 0F wokół 15-bajtowego AlgorithmIdentifier SHA-256, A1 1C wokół 28-bajtowego AlgorithmIdentifier MGF1, którego własnymi parametrami jest ten sam AlgorithmIdentifier SHA-256, oraz A2 03 02 01 20 dla soli. Jeśli zrzut twojego signatureAlgorithm pokazuje 02 01 20 na najwyższym poziomie SEQUENCE parametrów, a nie wewnątrz A2, patrzysz na kodowanie z 3.114.19
Dlaczego zestaw testów nie wyłapał źle zbudowanego AlgorithmIdentifier?
Bo testy PAdES prowadzą budowniczego CMS przez atrapę podpisującego, która raportuje sha256WithRSAEncryption i zwraca nil z GetSignatureAlgorithmParams, więc blok parametrów PSS nie był w teście budowany ani razu. To rozsądny projekt dla testów, które muszą działać bez magazynu certyfikatów, Keychainu czy tokenu, i ma martwy punkt o precyzyjnym kształcie: cokolwiek produkuje wyłącznie prawdziwy backend, jest ćwiczone wyłącznie przez prawdziwy backend. Ciekawsza jest druga warstwa. PDFium Component umieszcza też AlgorithmIdentifier podpisu, wraz z parametrami, wewnątrz podpisanego atrybutu cmsAlgorithmProtection z RFC 6211, a weryfikator porównuje tę kopię z zewnętrznym signatureAlgorithm. Obie kopie pochodziły z tego samego wywołania, więc zgadzały się doskonale i każde wewnętrzne sprawdzenie spójności przechodziło. Kodowanie było samospójne i błędne — kategoria błędów, której żadne porównywanie struktury z samą sobą nie ujawni — a tę samą lekcję na innej strukturze opowiada CMS signedAttrs i sortowanie DER SET OF, gdzie SET haszowany w jednej kolejności, a emitowany w innej, wyglądał dobrze, dopóki obcy weryfikator nie policzył skrótu ponownie
To, co faktycznie łapie tę klasę błędów, to dekoder nienapisany przez autora kodera, uruchomiony na prawdziwym wyniku prawdziwego backendu. Ścieżka weryfikacji w Windows w PDFium Component idzie przez CryptoAPI, a nie przez własny czytnik biblioteki, i to odrzucony tam podpis PSS doprowadził z powrotem do parametrów. Każdy ASN.1, który implementacja emituje do czytania przez inne implementacje, zasługuje na co najmniej jeden round-trip przez dekoder, którego nie kontroluje, a im więcej w tej strukturze wartości domyślnych i tagów, tym ten round-trip jest więcej wart
Gdzie to pasuje do reszty historii PSS
Ta poprawka jest niezależna od dwóch pozostałych miejsc, w których PSS może pójść źle w podpisie PAdES, a trzymanie ich osobno skraca debugowanie. Backend macOS może odkryć, że konkretny klucz albo starszy system odmawia PSS, i zejść do PKCS#1 v1.5, a AlgorithmIdentifier musi pójść za tym zejściem; to pytanie o możliwości, omówione w podpisywaniu PAdES tożsamością z macOS Keychain. Backend PKCS#11 może podać tokenowi CK_RSA_PKCS_PSS_PARAMS, którego układ token czyta inaczej z powodu niezgodności szerokości liczby całkowitej; to pytanie o ABI, omówione w CK_ULONG i pułapce pakowania w PKCS#11. Ten artykuł dotyczy trzeciej awarii: klucz był chętny, token policzył właściwe bajty, a DER opisujący wynik nie zgadzał się z RFC 4055 §3.1
Podpisujący, który deklaruje PSS, bierze na siebie zobowiązanie, jakiego podpisujący v1.5 nigdy nie miał: opisać własne parametry w formie, którą inna implementacja zdekoduje do tych samych trzech wartości. RFC 4055 §3.1 ustala tagi, X.690 §11.5 ustala, które pola mogą wystąpić, a ETSI TS 119 312 §7 ustala wartości warte wybrania. Wszystkie trzy backendy PDFium Delphi component są dostarczane jako źródło, więc przytoczone wyżej ciało GetSignatureAlgorithmParams jest tym, które możesz przeczytać, zrzucić i porównać z własnym weryfikatorem, zamiast brać je na wiarę