Технічна стаття

Ed448 та Brainpool ECDSA у чистому Pascal для PDF

PDFlibPas підписує та перевіряє підписи Ed448 і трьох кривих Brainpool ECDSA чистим Object Pascal. Без зовнішньої криптографічної бібліотеки, без платформного постачальника, без DLL: PDFlibEd448 реалізує PureEdDSA з RFC 8032 на edwards448, а PDFlibBrainpool реалізує brainpoolP256r1, brainpoolP384r1 та brainpoolP512r1 з RFC 5639. Обидва створено однаково: проти векторів відомих відповідей, згенерованих незалежно до того, як було написано хоч рядок коду Pascal, а описати їх варто насамперед через вади

Арифметика поля — незвичайно чесний код. Вона або збігається з опублікованими векторами байт у байт, або не збігається, тож місця для «майже працює» тут немає. Складність у тому, що хибна реалізація все одно створює підписи, все одно перевіряє власні підписи і виглядає повністю правдоподібно

Навіщо ці криві і чому саме Pascal

Криві Brainpool фігурують у європейських профілях кваліфікованих підписів, тож бібліотека, що підписує документи для цього ринку, не може вважати їх екзотикою. Ed448 входить до набору алгоритмів, який ISO/TS 32002 додає до PDF, де внутрішнім дайджестом слугує SHAKE256, а не SHA-2. Жодна з цих родин не доступна в поширених криптографічних бібліотеках для Pascal, тож PDF-бібліотека, якій вони потрібні, мусить володіти власною реалізацією

Аргумент розгортання той самий, що стосується всієї криптографії цієї бібліотеки: застосунок, що постачається одним виконуваним файлом без криптографічних залежностей, не має провайдера, який треба виявляти, версії, яку треба зіставляти, і поведінки, що змінюється після латання хоста. Підписування — саме та сфера, де рухома залежність найменш бажана

Константи беруть із тексту специфікації, а не з пам'яті

Першу спробу записати базову точку edwards448 зроблено з пам'яті, і вона виявилася хибною. Це не виняткова помилка, але дуже дорога, бо хибна базова точка створює самосузгоджену систему: ваше створення ключів, підписування та перевірка узгоджуються між собою і розходяться з рештою світу

Робоча процедура: взяти кожен доменний параметр із тексту специфікації, а потім перехресно перевірити. Для edwards448 це означає просте число, константу кривої, порядок групи та обидва десяткові координати базової точки з RFC 8032, перетворені у внутрішнє подання limb, з подальшою звіркою з опублікованими тестовими векторами з того самого документа. Для кривих Brainpool — параметри з RFC 5639, окрему реалізацію, написану для генерації векторів, і двосторонню звірку із системною бібліотекою до того, як запустився хоч один рядок Pascal

Параметри кривих Ed448 і Brainpool надходять із тексту специфікацій RFC 8032 та RFC 5639 у подання limb і проходять перехресну перевірку до запуску будь-якого коду Pascal
Доменні параметри edwards448 та кривих Brainpool беруть із тексту RFC, перетворюють у limb і звіряють з незалежними векторами

Одне скорочення в обчисленні варте попередження, бо виглядає універсальним, але ним не є: відновлення базової точки з фіксованого значення y працює для кривої 25519 і не працює для edwards448, де це значення не має квадратного кореня. Скрипт спростував це за секунди, що набагато дешевше, ніж виявити це за допомогою налагоджувача

Метод: дзеркальна реалізація на рівні limb до будь-якого Pascal

Техніка, яка зробила обидва модулі керованими, — дзеркальна реалізація мовою з необмеженими цілими числами, побудована знизу вгору. Спершу лише арифметичний шар: множення в полі, віднімання та поширення переносів зі стрес-тестами проти алгебраїчних інваріантів на кількох сотнях випадкових випадків. Потім повне створення ключів усередині дзеркала — саме там живуть семантичні вади і де їх дешево знаходити. І лише потім транскрипція Pascal

Робочий процес дзеркальної реалізації з необмеженими цілими числами, що перевіряє арифметику поля та створення ключів Pascal для Ed448 і Brainpool
Висхідний дзеркальний процес: спершу арифметика, потім створення ключів усередині дзеркала, далі транскрипція Pascal і порівняння проміжних значень

Вигода тут діагностична, а не розробницька. Коли дзеркало відоме як правильне, будь-яка розбіжність між дзеркалом і Pascal — це описка транскрипції, і зондування того самого проміжного значення в обох реалізаціях миттєво її локалізує. Це перетворює клас вад, який інакше майже не піддається налагодженню, — один хибний limb глибоко всередині скалярного множення — на п'ятихвилинне порівняння

Чотири першопричини в Ed448

Усі чотири знайдено зондуванням проміжних значень, і всі чотири належать до типу, що дає на вигляд правильний результат

Перша — пастка нотації. Більшість опублікованих формул уніфікованого додавання Едвардса припускає константу кривої мінус одиниця, а edwards448 має плюс одиницю. Перенесені без змін, вони дають чисельник координати y, записаний як сума там, де має бути різниця. Виправлення полягає не в латанні знака, а в повторному виведенні добуткової форми без інверсії з афінного закону додавання для правильної кривої, що дає чотири координатні вирази і не лишає місця для знака, успадкованого з неправильного джерела

Друга — у декомпресії точки. Відновлення афінного x з проєктивних координат вимагає одного множення на обернене до Z. Множення на обернений квадрат дає значення, яке залишається правильним проєктивним поданням і водночас є хибною афінною координатою, тож симптом — правильний y із неправильним x. Щоразу, коли одна координата правильна, а інша ні, вада криється в нормалізації, а не в арифметиці

Третя — звичка, перенесена з коротшої кривої. І скаляр на підпис, і скаляр виклику мають редукуватися з повного дайджесту, який для Ed448 становить 114 байт, а не з його перших 57. Крива з 32 байтами також використовує повний дайджест у 64 байти, тож правило послідовне; неправильним є лише припущення, що «половина дайджесту — це ширина скаляра»

Четверта — порядок. Префікс відокремлення доменів іде першим, до префікса контексту та повідомлення, а це не той порядок, який навіює інтуїтивне читання R і A у специфікації. Помилка тут дає підписи, що перевіряються лише вашою реалізацією і нічим іншим, — найбільш оманливий із можливих збоїв

// Проєктування перенесів поля: чисте поширення з семантикою floor,
// тож працюють і додатні, і від'ємні limb, а відніманню не потрібне
// зміщення. Верхній перенос згортається назад через 2^448 = 2^224 + 1
// (mod p), що торкається limb 0 та limb 8. Обмежено чотирма раундами;
// на практиці спостерігалося два
procedure FeCarry(var A: TFe448);
var
  I, Round: Integer;
  Carry: Int64;
begin
  for Round := 1 to 4 do
  begin
    Carry := 0;
    for I := 0 to 15 do
    begin
      A[I] := A[I] + Carry;
      Carry := Floor28(A[I]);          // floor, а не відкидання
      A[I] := A[I] - (Carry shl 28);
    end;
    if Carry = 0 then
      Break;
    A[0] := A[0] + Carry;              // 2^448 == 1
    A[8] := A[8] + Carry;              // 2^448 == 2^224
  end;
end;

Рання версія цієї підпрограми застосовувала зміщення перед поширенням, і на великих вхідних даних вона згортала хибний перенос неправильної величини в молодші limb. Схеми перенесів на основі зміщення — стійке джерело вад цього класу; семантика floor з обмеженим повторюваним циклом легша для міркування та вимірювано достатньо швидка

Дві першопричини в Brainpool

Перша взагалі не стосується криптографії. Робоче подання — 33 limb, тож добуток двох значень потребує 66, а масив добутку було оголошено на 64. Запис за межами кінця пошкодив суміжну пам'ять, що спершу виявлялося неправильними результатами і лише після додавання ширшого сканування стало збоєм. Правило, що звідси випливає, варто застосовувати до кожного числового буфера фіксованого розміру: визначати розмір за шириною добутку в найгіршому випадку з запасом, а потім більше про це не думати. Масив у коді, що постачається, — 68 limb

Друга — переплутана схема піднесення до степеня. Існують дві правильні форми square-and-multiply, і вони споживають експоненту в протилежних напрямках: форма справа ліворуч множить, а потім підносить основу до квадрата і має читати біти з молодшого кінця, тоді як форма зліва праворуч підносить до квадрата, а потім множить і читає зі старшого кінця. Цикл модульного обернення мав тіло справа ліворуч із проходом бітів від старших. Обидві половини підручникові, їхнє поєднання — ні, а результат — хибне обернене, що все одно виглядає як правдоподібний елемент поля

Дві схеми піднесення до степеня square-and-multiply з протилежними напрямками бітів та змішана форма, що обчислювала неправильні модульні обернені Brainpool
Обидві форми square-and-multiply правильні самі собою; поєднання тіла справа ліворуч із проходом від старших бітів дає правдоподібне хибне обернене
// Подвоєння та додавання Якобі, коли запис-призначення може бути
// тим самим змінним, що й джерело. Копіювання всього запису на вході —
// єдиний надійний захист: запис limb змінної R забруднює подальші
// читання P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
  const Curve: TBPCurve);
var
  Pin: TBPPoint;
begin
  Pin := P;        // спершу копіюємо, потім обчислюємо лише з Pin
  // ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;

Два уроки процесу, що коштували дорожче за вади

Інкрементальне гаряче латання не сходиться для криптографічного модуля. Один проєкт латали знову і знову, доки він не набрав 32 дубльовані підпрограми та пошкоджену структуру, і виправити його вдалося лише переписуванням. Взірець варто брати такий: або написати один раз із перевіреного дзеркала, або переписати; низка локальних виправлень арифметики, якої ви ще не розумієте, накопичується швидше, ніж виправляє

І перевіряйте мітку часу виконуваного файла, перш ніж вірити результату тесту. Інкрементальна збірка, що компілює, але не перелінковує, запускає попередній бінарник, що породив цілий раунд хибних слідів про відсутні зонди та дубльований вивід. Коли налагоджуєте криптографію, незрозумілий результат має спершу навести на питання «чи це бінарник, який я щойно зібрав», і лише потім «чи алгоритм неправильний»

Продуктивність, охоплення та як його викликати

Модульна редукція в модулі Brainpool — це побітова зсувно-віднімальна редукція від старшого встановленого біта добутку, тож множення коштує приблизно порядку бітової ширини. Перевірка P-256 вкладається в кілька сотень мілісекунд, що непомітно для підписування чи перевірки документів і було б недостатньо для термінатора TLS. Редукція Барретта — очевидне покращення, але потребує ширшого робочого значення, ніж поточне подання, тож це зміна на випадок, коли її вимагатиме навантаження, а не завчасна

uses
  PDFlibEd448, PDFlibBrainpool;

var
  PublicKey, Signature: AnsiString;
  Curve: TBPCurve;
  R, S, PubX, PubY: TBPValue;
begin
  // Ed448: PureEdDSA, внутрішньо SHAKE256, ключі з 57 байтів
  if Ed448PublicKeyFromSeed(Seed, PublicKey) and
     Ed448Sign(DocumentDigest, Seed, Signature) then
    Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));

  // Brainpool: викликач постачає nonce на кожен підпис, тож політика
  // nonce лишається в застосунку
  Curve := BPLoadCurve(bpP256r1);
  if BPKeyGen(PubX, PubY, PrivateD, Curve) and
     BPSignFixedK(R, S, Hash, PrivateD, Nonce, Curve) then
    Assert(BPVerify(R, S, Hash, PubX, PubY, Curve));
end;

Зверніть увагу: точка входу підписування Brainpool приймає nonce, а не генерує його. Це навмисно: генерація nonce — найкатастрофічніша річ, яку можна зробити неправильно в ECDSA, бо повторене чи передбачуване значення розкриває приватний ключ, а рішення про джерело випадковості належить застосунку та його режиму відповідності, а не PDF-бібліотеці

Ці криві сусідять із постквантовою роботою, описаною в статті про FIPS 204 ML-DSA, і вмикаються в той самий конвеєр підписування та перевірки, охоплений у підписуванні та перевірці PAdES. Тестові сертифікати на цих кривих локально описано в самопідписаних сертифікатах із CryptoAPI. Повна матриця алгоритмів наведена на сторінці продукту losLab PDF Developer Library