Техническа статия

Чист Pascal аритметика на NIST криви за подписване на PDF

HotPDF извършва key agreement върху елиптични криви и верификация на подписи за PDF на чист Object Pascal, без OpenSSL обвързване и без платформен крипто доставчик в пътя; Това покрива пет криви: P-256, P-384 и P-521 за NIST простите семейства, плюс X25519 и X448 за Montgomery-curve съгласуване на ключове; Причината да се напише този код, вместо да се линкне, е внедряването, не чистотата; Delphi или Free Pascal приложение, което доставя един изпълним файл и без криптографска DLL, няма разминаване на версии за управление, няма per-platform доставчик за откриване и нищо, което променя поведението, когато клиент кърпи своите системни библиотеки

Цената е, че сега вие притежавате аритметиката; Big-integer модулното умножение е непрощаващ код: то или произвежда байт-идентични резултати спрямо публикувани тестови вектори, или произвежда правдоподобно изглеждащ боклук, а разстоянието между тези две състояния може да бъде едно-единствено сравнение; Това е историята на това сравнение, защото формата на дефекта се обобщава за всеки Pascal пренос на аритметика в полето

Защо изобщо една PDF библиотека се нуждае от аритметика на криви?

Две функции го дърпата навътре; Първата е encryption на документи с публичен ключ: ISO 32000 recipient-list обработчикът увива per-document ключ за назовани сертификати и когато получател притежава EC ключ, увиването минава през key-agreement, а не RSA key transport; Без ECDH няма начин да се отвори такъв документ; Втората е валидация на подписи; Верифицирането на ECDSA подпис върху /ByteRange байтовете се нуждае от умножение на точка върху кривата на подписващия, а P-384 е често срещан в правителствени и qualified-signature профили, където P-256 се смята за пода, а не целта; HotPDF излага резултатите от тази работа през пътя за ECDSA и CMS верификация и през модела на включваеми signature доставчици

Диаграма къде се използва чистата Pascal аритметика на криви в HotPDF: recipient-list ECDH шифроване и ECDSA верификация на подписи върху ByteRange
Key agreement отваря EC-шифровани документи за назовани получатели, докато валидацията на подписи се нуждае от умножение на точка върху кривата на подписващия

CIOS и едното изваждане накрая

Montgomery умножението избягва делението, работейки в трансформирана област, където редукцията е отместване; Вариантът, който HotPDF използва, е Coarsely Integrated Operand Scanning, който преплита умножението и редукцията limb по limb, така че междинното никога не расте отвъд ширината на модула плюс един limb; Тялото на цикъла е праволинейно и лесно за тестване; Опашката не е: след преплетените проходи акумулаторът може да е някъде в диапазона до два пъти модула, така че алгоритъмът завършва с условно изваждане, което премахва едно копие на простото число, ако и само ако акумулаторът е по-голям или равен на него

Сравняването на две числа от много limbs означава обхождане от най-старшия limb надолу, докато носите borrow; Очевидният начин да го напишете е да сравните limb-а на акумулатора с limb-а на модула плюс входящия borrow; Този израз е грешен и е грешен по начин, който повечето криви крият

// Грешно: P[I] + Borrow може да wrap-не, когато P[I] е $FFFFFFFFFFFFFFFF
if T[I] < P[I] + Borrow then
begin
  Borrow := 1;
  Break;
end;

// Правилно: сравнявайте, без изобщо да събирате към limb
if (T[I] < P[I]) or ((T[I] = P[I]) and (Borrow = 1)) then
begin
  Borrow := 1;
  Break;
end;

Как всъщност изглежда borrow wrap-around?

Изглежда като крива, която работи навсякъде освен в продукцията; Простите числа за P-384 и P-521 съдържат limbs, които са изцяло единици, така че P[I] е равно на $FFFFFFFFFFFFFFFF; Добавете входящия borrow от едно към това и 64-битово unsigned wrap-ва до нула; Сравнението пита дали limb-ът на акумулатора е по-малък от нула, решава, че не е, и заключава, че не се нуждае от borrow; Един limb от резултата е грешен с едно

Диаграма на borrow wrap в Montgomery редукция, противопоставяща грешното limb сравнение на правилното borrow разпространение в HotPDF P-384 аритметиката
Добавянето на borrow към limb от само единици wrap-ва до нула, така че P-384 и P-521 не получават изваждане, докато P-256 крие дефекта

P-256 избягва, защото нито един от неговите limbs не е от само единици, така че събирането никога не прелива и дефектният израз случайно съвпада с правилния; Това е най-лошият възможен изход за тестов набор: най-тестваната крива минава, по-малко тестваните се провалят прекъсващо в зависимост от стойностите на операндите, а отказът излиза на повърхността като резултат от верификация „невалиден подпис“ върху документи, които са напълно валидни; HotPDF носеше изрична порта върху P-384 именно поради тази причина, връщайки недостъпен статус, а не грешен отговор, докато аритметиката не беше доказана спрямо reference вектори

Как дефектът всъщност беше локализиран

Не с четене на кода; Продуктивната последователност беше механична и е преизползваема; Първо, елиминирайте константите: всеки limb на p, R и R^2 беше регенериран независимо и сравнен limb по limb, което изключва единствения най-чест източник на криви дефекти; Второ, инструментирайте аритметиката, а не API-то: временна dump процедура отпечатва междинните стойности на Montgomery умножението на R^2, на x^3 и на y^2 за известна точка, така че те да могат да бъдат проверени спрямо независимо изчислена истина

Това сравнение сочи направо към виновника; Веригата x беше правилна от край до край, докато y^2 се разминаваше в точно един limb с точно едно; Едно-limb разлика от едно не е дефект на умножение, дефект на carry разпространение или дефект на константа; тя е дефект на borrow верига, а единствената borrow верига в рутината е финалното условно изваждане; Един детайл почти дерайлира това: reference константата, използвана за dump-а, беше сама написана в грешния байтов ред при първия опит, което произведе разминаване в стойността y и за момент подсказа втори, несъществуващ дефект; Проверете endianness на вашата ground истина, преди да ѝ поверите обвинението на вашия код

Блок-схема как дефектът на кривата в HotPDF беше локализиран чрез регенериране на константи, dump на Montgomery междинни стойности и diff срещу огледална истина
Едно-limb разлика от точно едно сочеше направо към единствената borrow верига в рутината, а reference с разменени байтове почти заблуди издирването

Съседните капани в същата рутина

Още три режима на отказ живеят в рамките на няколко реда от това сравнение и и трите бяха живи в някакъв момент по време на разработката

// 1. Акумулаторът има един limb над ширината на модула. Сравняването само
//    на ниските L limbs пропуска случая, в който T е равно точно на p плюс
//    2^(64*L), което се случва за значителен дял от случайни входове,
//    защото 2p надвишава 2^256 за P-256 и 2^384 за P-384
if (T[L] <> 0) or NotLessThanModulus(T, P, L) then
  SubtractModulus(T, P, L);

// 2. Общото много-limb изваждане има същата wrap опасност: когато
//    Y[I] е $FFFFFFFFFFFFFFFF, Y[I] + Borrow wrap-ва до нула и borrow-ът
//    трябва да оцелее в следващия limb, вместо да бъде изчистен
Diff := X[I] - Y[I] - Borrow;
NextBorrow := Ord((X[I] < Y[I]) or ((X[I] = Y[I]) and (Borrow = 1)));

Третият не е код, а произход; Простото число за P-521 беше първоначално преписано със 130 шестнадесетични цифри вместо 131, с един F по-малко, а Montgomery константите бяха после изчислени от това грешно просто число, така че константите бяха самосъгласувани и съвместно грешни; Параметрите на кривата трябва да се извеждат, никога да се пишат: изчислете R като (1 shl (64 * L)) mod p от простото число, което действително използвате, после кросчекнете R * R mod p спрямо стойността, която вашата константа R^2 твърди; Двойка константи, които се съгласуват помежду си, не доказва нищо за нито една от тях

Стратегия за верификация, която мащабира отвъд една крива

Техниката, която направи X25519 и X448 управляеми, беше писане на огледална имплементация на език с неограничени цели числа и преписване на Pascal контролния поток в нея ред по ред; Когато огледалото произвежда правилния отговор, а Pascal не, дефектът е пропуск в транскрипцията и сондирането на същата междинна стойност в двете имплементации го намира за секунди; И трите класически грешки в RFC 7748 ladder-а бяха хванати така: constant-time swap, чийто втори ред преизползва вече разменената стойност, финално инвертиране, което върна z на степен минус едно вместо да го умножи в X, и умножение с малка константа, което сглоби half-word произведения с bitwise or и загуби преноса

За тестов материал вземайте векторите като байтове, а не като текст; Извличането на частен ключ с текстов модел е начинът, по който правилна имплементация бива обвинена в off-by-one-byte грешка, която живее изцяло в стъпката на извличане; Отрежете hex от DER кодирането на известни offsets и сравнявайте байтови масиви

С поправената borrow верига и петте криви съвпадат с публикуваните reference вектори байт по байт и HotPDF вече не огражда нито една от тях; Ако интегрирате подписване, базирано на сертификати, или recipient-list шифроване, практическият извод е, че изборът на крива вече е политическо решение, а не въпрос на възможност; профилите и капаните с байтов ред на страната за подписване са разгледани в прошетката за PAdES подписване; Детайлите на компонента и поддържаната алгоритмична матрица са на продуктовата страница HotPDF Delphi PDF component